package main import ( "fmt" "os" "path/filepath" "strings" ) // VaultCheckResult contains the diagnostic report for vault integrity. type VaultCheckResult struct { TotalNodes int `json:"total_nodes"` TotalFiles int `json:"total_files"` NodesWithFsPath int `json:"nodes_with_fs_path"` FilesOnDisk int `json:"files_on_disk"` FilesMissing int `json:"files_missing"` PathEscapeCount int `json:"path_escape_count"` PathMismatchCount int `json:"path_mismatch_count"` Errors []string `json:"errors,omitempty"` Details []string `json:"details,omitempty"` Healthy bool `json:"healthy"` } func (a *App) VaultCheck() (*VaultCheckResult, error) { result := &VaultCheckResult{Healthy: true} // Check all root nodes roots, err := a.nodes.ListRoots(true) if err != nil { return nil, fmt.Errorf("list roots: %w", err) } var checkNode func(id string) checkNode = func(id string) { n, err := a.nodes.GetActive(id) if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("node %s: get: %v", id, err)) result.Healthy = false return } result.TotalNodes++ // Check fs_path if n.FsPath != "" { result.NodesWithFsPath++ physPath := filepath.Join(a.vault, n.FsPath) rel, err := filepath.Rel(a.vault, physPath) if err != nil || strings.HasPrefix(rel, "..") { result.PathEscapeCount++ result.Errors = append(result.Errors, fmt.Sprintf("node %s (%s): fs_path escapes vault: %s", n.ID, n.Title, n.FsPath)) result.Healthy = false return } if _, err := os.Stat(physPath); os.IsNotExist(err) { result.FilesMissing++ result.Details = append(result.Details, fmt.Sprintf("node %s (%s): folder missing on disk: %s", n.ID, n.Title, physPath)) } } // Check children children, _ := a.nodes.ListChildren(n.ID, true) for _, c := range children { checkNode(c.ID) } } for _, n := range roots { checkNode(n.ID) } // Check file records // We'll query files table and verify each vault-mode file exists rows, err := a.db.Query(`SELECT id, node_id, path, storage_mode FROM files`) if err != nil { result.Errors = append(result.Errors, fmt.Sprintf("query files: %v", err)) result.Healthy = false return result, nil } defer rows.Close() for rows.Next() { var id, nodeID, path, mode string if err := rows.Scan(&id, &nodeID, &path, &mode); err != nil { continue } result.TotalFiles++ if mode == "vault" { absPath := filepath.Join(a.vault, path) rel, err := filepath.Rel(a.vault, absPath) if err != nil || strings.HasPrefix(rel, "..") { result.PathEscapeCount++ result.Errors = append(result.Errors, fmt.Sprintf("file %s: path escapes vault: %s", id, path)) result.Healthy = false continue } if _, err := os.Stat(absPath); err == nil { result.FilesOnDisk++ } else { result.FilesMissing++ result.Details = append(result.Details, fmt.Sprintf("file %s (%s): missing on disk: %s", id, path, absPath)) } } } if result.FilesMissing > 0 { result.Healthy = false } return result, nil }