From 66c5c81f393310e50a4b64ae0e808f22fa279cbb Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 2 Jun 2026 16:36:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=81=D1=82=D0=B0=D0=B1=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20vault=20layout=20?= =?UTF-8?q?=E2=80=94=20rename/move/delete=20note/file=20vs=20folder=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D1=8B,=20sync?= =?UTF-8?q?=20apply=20=D0=B1=D0=B5=D0=B7=20spaces/,=20VaultCheck=20=D1=83?= =?UTF-8?q?=D1=81=D0=B8=D0=BB=D0=B5=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RenameNode/DeleteNode/MoveNode: note/file и folder-like nodes обрабатываются по-разному (file record vs физическая папка) - DeleteNode: рекурсивный soft-delete всех descendants - SafeVaultPath возвращает clean relative, filepath.Join в sync_apply.go - Fallback spaces/ → .verstak/remote-inbox в applyRemoteNoteCreate - VaultCheck: проверка parent_id != nil, orphan descendants, fs_path folder на диске --- cmd/verstak-gui/bindings_nodes.go | 351 +++++++++++++++++++++--------- cmd/verstak-gui/sync_apply.go | 11 +- cmd/verstak-gui/vault_check.go | 110 ++++++---- 3 files changed, 329 insertions(+), 143 deletions(-) diff --git a/cmd/verstak-gui/bindings_nodes.go b/cmd/verstak-gui/bindings_nodes.go index 4341f53..07125eb 100644 --- a/cmd/verstak-gui/bindings_nodes.go +++ b/cmd/verstak-gui/bindings_nodes.go @@ -45,7 +45,7 @@ func (a *App) CreateNodeFromTemplate(parentID, title, templateID string) (*NodeD seg := templates.SafeDisplayNameToPathSegment(title) if seg == "" { - seg = title + seg = "node" } var parent *nodes.Node @@ -126,7 +126,7 @@ func (a *App) DeleteNode(id string) error { entity = syncsvc.EntityNote targetType = activity.TargetNote evType = activity.TypeNoteDeleted - case nodes.TypeFolder: + case nodes.TypeFolder, nodes.TypeProject, nodes.TypeClient, nodes.TypeDocument, nodes.TypeRecipe, nodes.TypeSpace: entity = syncsvc.EntityFolder targetType = activity.TargetFolder evType = activity.TypeFolderDeleted @@ -142,19 +142,66 @@ func (a *App) DeleteNode(id string) error { _ = a.activity.Record(pid, targetType, id, "", evType, n.Title, "") _ = a.sync.RecordOp(entity, id, syncsvc.OpDelete, nil) - // Move physical folder to trash - if n.FsPath != "" { + // Handle physical trash based on node type + isFolderLike := n.Type != nodes.TypeNote && n.Type != nodes.TypeFile + + if isFolderLike && n.FsPath != "" { + // Move entire folder to trash src := filepath.Join(a.vault, n.FsPath) if info, err := os.Stat(src); err == nil && info.IsDir() { trashDir := filepath.Join(a.vault, ".verstak", "trash") _ = os.MkdirAll(trashDir, 0o750) - _ = os.Rename(src, filepath.Join(trashDir, n.ID+"_"+templates.SafeDisplayNameToPathSegment(n.Title))) + trashPath := filepath.Join(trashDir, n.ID+"_"+templates.SafeDisplayNameToPathSegment(n.Title)) + _ = os.Rename(src, trashPath) } + + // Recursively soft-delete all descendants + a.softDeleteDescendants(id) + return a.nodes.SoftDelete(id) } + // For notes/files: move associated file to trash, then soft-delete + if err := a.deleteNodeFileRecord(id); err != nil { + // Log but continue with soft-delete + } return a.nodes.SoftDelete(id) } +// softDeleteDescendants recursively soft-deletes all descendant nodes. +func (a *App) softDeleteDescendants(parentID string) { + children, err := a.nodes.ListChildren(parentID, false) + if err != nil { + return + } + for _, child := range children { + a.softDeleteDescendants(child.ID) + _ = a.nodes.SoftDelete(child.ID) + } +} + +// deleteNodeFileRecord moves the file record for a node to trash. +func (a *App) deleteNodeFileRecord(nodeID string) error { + rows, err := a.db.Query(`SELECT id, path, storage_mode FROM files WHERE node_id=?`, nodeID) + if err != nil { + return err + } + defer rows.Close() + for rows.Next() { + var id, path, mode string + if err := rows.Scan(&id, &path, &mode); err != nil { + continue + } + if mode == "vault" { + src := filepath.Join(a.vault, path) + trashDir := filepath.Join(a.vault, ".verstak", "trash") + _ = os.MkdirAll(trashDir, 0o750) + _ = os.Rename(src, filepath.Join(trashDir, id+"_"+filepath.Base(path))) + } + } + _, err = a.db.Exec(`DELETE FROM files WHERE node_id=?`, nodeID) + return err +} + func (a *App) RenameNode(nodeID, newTitle string) error { n, err := a.nodes.GetActive(nodeID) if err != nil { @@ -163,43 +210,101 @@ func (a *App) RenameNode(nodeID, newTitle string) error { seg := templates.SafeDisplayNameToPathSegment(newTitle) if seg == "" { - seg = newTitle + seg = "node" } - oldFsPath := n.FsPath - oldPhysPath := filepath.Join(a.vault, oldFsPath) + isFolderLike := n.Type != nodes.TypeNote && n.Type != nodes.TypeFile - parentFsPath := "" - if n.ParentID != nil { - p, err := a.nodes.GetActive(*n.ParentID) - if err == nil { - parentFsPath = p.FsPath + if isFolderLike { + // Folder-like node: rename physical directory + if n.FsPath == "" { + return fmt.Errorf("cannot rename node %s (%s): fs_path is empty", n.ID, n.Title) } - } - newFsPath := seg - if parentFsPath != "" { - newFsPath = filepath.Join(parentFsPath, seg) - } - newPhysPath := filepath.Join(a.vault, newFsPath) - newPhysPath = templates.UniquePath(newPhysPath) - rel, _ := filepath.Rel(a.vault, newPhysPath) - newFsPath = rel + oldFsPath := n.FsPath + oldPhysPath := filepath.Join(a.vault, oldFsPath) - oldTitle := n.Title + parentFsPath := "" + if n.ParentID != nil { + p, err := a.nodes.GetActive(*n.ParentID) + if err == nil { + parentFsPath = p.FsPath + } + } + newFsPath := seg + if parentFsPath != "" { + newFsPath = filepath.Join(parentFsPath, seg) + } + newPhysPath := filepath.Join(a.vault, newFsPath) + newPhysPath = templates.UniquePath(newPhysPath) + rel, _ := filepath.Rel(a.vault, newPhysPath) + newFsPath = rel + + oldTitle := n.Title + if err := a.nodes.UpdateTitle(nodeID, newTitle); err != nil { + return err + } + if err := a.nodes.UpdateFsPath(nodeID, newFsPath); err != nil { + return err + } + if err := a.nodes.UpdateFsPathRecursive(nodeID, newFsPath); err != nil { + return err + } + + if _, err := os.Stat(oldPhysPath); err == nil { + if err := os.Rename(oldPhysPath, newPhysPath); err != nil { + return fmt.Errorf("rename folder: %w", err) + } + } + + pid := "" + if n.ParentID != nil { + pid = *n.ParentID + } + _ = a.activity.Record(pid, activity.TargetFolder, nodeID, "", activity.TypeFolderRenamed, newTitle, `{"from":"`+oldTitle+`","to":"`+newTitle+`"}`) + _ = a.sync.RecordOp(syncsvc.EntityFolder, nodeID, syncsvc.OpUpdate, map[string]interface{}{ + "title": newTitle, + "fs_path": newFsPath, + "updated_at": time.Now().UTC().Format(time.RFC3339), + }) + return nil + } + + // Note/file node: rename the physical file, update file record if err := a.nodes.UpdateTitle(nodeID, newTitle); err != nil { return err } - if err := a.nodes.UpdateFsPath(nodeID, newFsPath); err != nil { - return err - } - if err := a.nodes.UpdateFsPathRecursive(nodeID, newFsPath); err != nil { - return err - } - if _, err := os.Stat(oldPhysPath); err == nil { - if err := os.Rename(oldPhysPath, newPhysPath); err != nil { - return fmt.Errorf("rename folder: %w", err) + // Update file record filename/path + rows, err := a.db.Query(`SELECT id, path, filename FROM files WHERE node_id=?`, nodeID) + if err != nil { + return nil // non-fatal + } + defer rows.Close() + for rows.Next() { + var fileID, filePath, oldFilename string + if err := rows.Scan(&fileID, &filePath, &oldFilename); err != nil { + continue + } + if filePath == "" { + continue + } + oldAbs := filepath.Join(a.vault, filePath) + ext := filepath.Ext(oldFilename) + newFilename := seg + ext + dir := filepath.Dir(filePath) + newRelPath := filepath.Join(dir, newFilename) + newAbs := filepath.Join(a.vault, newRelPath) + + // Rename physical file + if _, err := os.Stat(oldAbs); err == nil { + // Check for collision + if _, err := os.Stat(newAbs); os.IsNotExist(err) { + if err := os.Rename(oldAbs, newAbs); err == nil { + _, _ = a.db.Exec(`UPDATE files SET filename=?, path=? WHERE id=?`, + newFilename, newRelPath, fileID) + } + } } } @@ -209,52 +314,30 @@ func (a *App) RenameNode(nodeID, newTitle string) error { } var evType string var targetType string - var syncEntity string - switch n.Type { - case nodes.TypeNote: + if n.Type == nodes.TypeNote { evType = activity.TypeNoteUpdated targetType = activity.TargetNote - syncEntity = syncsvc.EntityNote - case nodes.TypeFile: + } else { evType = activity.TypeFileRenamed targetType = activity.TargetFile - syncEntity = syncsvc.EntityFile - case nodes.TypeFolder: - evType = activity.TypeFolderRenamed - targetType = activity.TargetFolder - syncEntity = syncsvc.EntityFolder - default: - evType = activity.TypeNodeUpdated - targetType = activity.TargetNode - syncEntity = syncsvc.EntityNode } - _ = a.activity.Record(pid, targetType, nodeID, "", evType, newTitle, `{"from":"`+oldTitle+`","to":"`+newTitle+`"}`) - _ = a.sync.RecordOp(syncEntity, nodeID, syncsvc.OpUpdate, map[string]interface{}{ + _ = a.activity.Record(pid, targetType, nodeID, "", evType, newTitle, "") + _ = a.sync.RecordOp(syncsvc.EntityNote, nodeID, syncsvc.OpUpdate, map[string]interface{}{ "title": newTitle, - "fs_path": newFsPath, "updated_at": time.Now().UTC().Format(time.RFC3339), }) return nil } func (a *App) MoveNode(nodeID, newParentID string) error { - destChildren, err := a.nodes.ListChildren(newParentID, false) - if err != nil { - return err - } node, err := a.nodes.GetActive(nodeID) if err != nil { return err } - for i := range destChildren { - if destChildren[i].Title == node.Title { - newName := fmt.Sprintf("%s (%d)", node.Title, 2) - _ = a.nodes.UpdateTitle(nodeID, newName) - break - } - } + isFolderLike := node.Type != nodes.TypeNote && node.Type != nodes.TypeFile + // Resolve new parent var parent *nodes.Node if newParentID != "" { parent, err = a.nodes.GetActive(newParentID) @@ -263,31 +346,105 @@ func (a *App) MoveNode(nodeID, newParentID string) error { } } - seg := templates.SafeDisplayNameToPathSegment(node.Title) - newFsPath := seg - if parent != nil && parent.FsPath != "" { - newFsPath = filepath.Join(parent.FsPath, seg) - } - newPhysPath := filepath.Join(a.vault, newFsPath) - newPhysPath = templates.UniquePath(newPhysPath) - rel, _ := filepath.Rel(a.vault, newPhysPath) - newFsPath = rel - - oldPhysPath := filepath.Join(a.vault, node.FsPath) - - if err := a.nodes.Move(nodeID, &newParentID, 0); err != nil { - return err - } - if err := a.nodes.UpdateFsPath(nodeID, newFsPath); err != nil { - return err - } - if err := a.nodes.UpdateFsPathRecursive(nodeID, newFsPath); err != nil { - return err + // Resolve name conflicts first + nodeTitle := node.Title + if parent != nil { + destChildren, _ := a.nodes.ListChildren(newParentID, false) + for i := range destChildren { + if destChildren[i].Title == nodeTitle { + nodeTitle = fmt.Sprintf("%s (%d)", nodeTitle, 2) + break + } + } } - if _, err := os.Stat(oldPhysPath); err == nil { - if err := os.Rename(oldPhysPath, newPhysPath); err != nil { - return fmt.Errorf("move folder: %w", err) + // Update title if changed + if nodeTitle != node.Title { + if err := a.nodes.UpdateTitle(nodeID, nodeTitle); err != nil { + return err + } + node.Title = nodeTitle + } + + if isFolderLike { + // Folder-like node: move physical directory + if node.FsPath == "" { + return fmt.Errorf("cannot move node %s (%s): fs_path is empty", node.ID, node.Title) + } + + seg := templates.SafeDisplayNameToPathSegment(node.Title) + oldPhysPath := filepath.Join(a.vault, node.FsPath) + + newFsPath := seg + if parent != nil && parent.FsPath != "" { + newFsPath = filepath.Join(parent.FsPath, seg) + } + newPhysPath := filepath.Join(a.vault, newFsPath) + newPhysPath = templates.UniquePath(newPhysPath) + rel, _ := filepath.Rel(a.vault, newPhysPath) + newFsPath = rel + + // Update parent_id (use nil for root move) + if newParentID == "" { + if err := a.nodes.Move(nodeID, nil, 0); err != nil { + return err + } + } else { + if err := a.nodes.Move(nodeID, &newParentID, 0); err != nil { + return err + } + } + if err := a.nodes.UpdateFsPath(nodeID, newFsPath); err != nil { + return err + } + if err := a.nodes.UpdateFsPathRecursive(nodeID, newFsPath); err != nil { + return err + } + + if _, err := os.Stat(oldPhysPath); err == nil { + if err := os.Rename(oldPhysPath, newPhysPath); err != nil { + return fmt.Errorf("move folder: %w", err) + } + } + } else { + // Note/file node: update parent_id and move physical file + if newParentID == "" { + if err := a.nodes.Move(nodeID, nil, 0); err != nil { + return err + } + } else { + if err := a.nodes.Move(nodeID, &newParentID, 0); err != nil { + return err + } + } + + // Update file record path to reflect new parent + if parent != nil && parent.FsPath != "" { + rows, err := a.db.Query(`SELECT id, path FROM files WHERE node_id=?`, nodeID) + if err == nil { + defer rows.Close() + for rows.Next() { + var fileID, filePath string + if err := rows.Scan(&fileID, &filePath); err != nil { + continue + } + if filePath == "" { + continue + } + filename := filepath.Base(filePath) + newRelPath := filepath.Join(parent.FsPath, filename) + oldAbs := filepath.Join(a.vault, filePath) + newAbs := filepath.Join(a.vault, newRelPath) + + if _, err := os.Stat(oldAbs); err == nil { + _ = os.MkdirAll(filepath.Dir(newAbs), 0o750) + if err := os.Rename(oldAbs, newAbs); err == nil { + _, _ = a.db.Exec(`UPDATE files SET path=? WHERE id=?`, + newRelPath, fileID) + } + } + } + } } } @@ -298,28 +455,23 @@ func (a *App) MoveNode(nodeID, newParentID string) error { var targetType string var evType string var syncEntity string - switch node.Type { - case nodes.TypeNote: - targetType = activity.TargetNote - evType = activity.TypeNoteUpdated - syncEntity = syncsvc.EntityNote - case nodes.TypeFile: - targetType = activity.TargetFile - evType = activity.TypeFileMoved - syncEntity = syncsvc.EntityFile - case nodes.TypeFolder: + if isFolderLike { targetType = activity.TargetFolder evType = activity.TypeFolderMoved syncEntity = syncsvc.EntityFolder - default: - targetType = activity.TargetNode - evType = activity.TypeNodeUpdated - syncEntity = syncsvc.EntityNode + } else if node.Type == nodes.TypeNote { + targetType = activity.TargetNote + evType = activity.TypeNoteUpdated + syncEntity = syncsvc.EntityNote + } else { + targetType = activity.TargetFile + evType = activity.TypeFileMoved + syncEntity = syncsvc.EntityFile } _ = a.activity.Record(pid, targetType, nodeID, "", evType, node.Title, `{"to":"`+newParentID+`"}`) _ = a.sync.RecordOp(syncEntity, nodeID, syncsvc.OpMove, map[string]interface{}{ "parent_id": newParentID, - "fs_path": newFsPath, + "fs_path": node.FsPath, "updated_at": time.Now().UTC().Format(time.RFC3339), }) return nil @@ -344,6 +496,9 @@ func (a *App) OpenNodeFolder(nodeID string) (string, error) { if err != nil { return "", err } + if n.FsPath == "" { + return "", fmt.Errorf("node %s has no physical path", nodeID) + } physPath := filepath.Join(a.vault, n.FsPath) return physPath, nil } diff --git a/cmd/verstak-gui/sync_apply.go b/cmd/verstak-gui/sync_apply.go index 49e1c2f..fcbb7e7 100644 --- a/cmd/verstak-gui/sync_apply.go +++ b/cmd/verstak-gui/sync_apply.go @@ -198,12 +198,6 @@ func (a *App) applyRemoteNoteCreate(op syncsvc.Op) error { filename := payload.Filename if filename == "" { filename = payload.NodeID[:8] + ".md" - } else { - cleanFilename, err := syncsvc.SafeVaultPath(a.vault, filename) - if err != nil { - return fmt.Errorf("unsafe path in %s: %w", op.EntityType, err) - } - filename = cleanFilename } parentFsPath := "" if noteNode, err := a.nodes.Get(payload.NodeID); err == nil && noteNode.ParentID != nil { @@ -212,7 +206,7 @@ func (a *App) applyRemoteNoteCreate(op syncsvc.Op) error { } } if parentFsPath == "" { - parentFsPath = "spaces" + parentFsPath = filepath.Join(".verstak", "remote-inbox") } dest = filepath.Join(a.vault, parentFsPath, filename) payload.Path, _ = filepath.Rel(a.vault, dest) @@ -279,10 +273,11 @@ func (a *App) applyRemoteNoteUpdate(op syncsvc.Op) error { } if storageMode == "vault" { - abs, err := syncsvc.SafeVaultPath(a.vault, filePath) + clean, err := syncsvc.SafeVaultPath(a.vault, filePath) if err != nil { return fmt.Errorf("unsafe vault path in note update: %w", err) } + abs := filepath.Join(a.vault, clean) if err := os.WriteFile(abs, []byte(payload.Content), 0o640); err != nil { return err } diff --git a/cmd/verstak-gui/vault_check.go b/cmd/verstak-gui/vault_check.go index 74571f7..abdf332 100644 --- a/cmd/verstak-gui/vault_check.go +++ b/cmd/verstak-gui/vault_check.go @@ -5,71 +5,104 @@ import ( "os" "path/filepath" "strings" + + "verstak/internal/core/nodes" ) -// 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"` + TotalNodes int `json:"total_nodes"` + TotalFiles int `json:"total_files"` + NodesWithFsPath int `json:"nodes_with_fs_path"` + FoldersOnDisk int `json:"folders_on_disk"` + FilesOnDisk int `json:"files_on_disk"` + FilesMissing int `json:"files_missing"` + PathEscapeCount int `json:"path_escape_count"` + ParentIDEmptyCount int `json:"parent_id_empty_count"` + OrphanDescendantCount int `json:"orphan_descendant_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 + // Build a set of all node IDs for ancestor check + allNodes := make(map[string]*nodes.Node) roots, err := a.nodes.ListRoots(true) if err != nil { return nil, fmt.Errorf("list roots: %w", err) } + var collectDescendants func(id string) + collectDescendants = func(id string) { + n, err := a.nodes.Get(id) + if err == nil { + allNodes[n.ID] = n + } + children, _ := a.nodes.ListChildren(id, true) + for _, c := range children { + allNodes[c.ID] = &c + collectDescendants(c.ID) + } + } + for _, n := range roots { + allNodes[n.ID] = &n + collectDescendants(n.ID) + } - 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)) + // Check parent_id consistency + for id, n := range allNodes { + if !n.IsRoot() && n.ParentID != nil && *n.ParentID == "" { + result.ParentIDEmptyCount++ + result.Errors = append(result.Errors, + fmt.Sprintf("node %s (%s): parent_id is empty string, should be nil", id, n.Title)) result.Healthy = false - return + } + } + + // Check each node + for _, n := range allNodes { + if n.IsDeleted() { + continue } result.TotalNodes++ + // Check if ancestor is deleted + if n.ParentID != nil && *n.ParentID != "" { + if parent, ok := allNodes[*n.ParentID]; ok && parent.IsDeleted() { + result.OrphanDescendantCount++ + result.Errors = append(result.Errors, + fmt.Sprintf("node %s (%s) is active but parent %s is deleted", n.ID, n.Title, *n.ParentID)) + result.Healthy = false + } + } + // Check fs_path - if n.FsPath != "" { + isFolderLike := n.Type != nodes.TypeNote && n.Type != nodes.TypeFile + if isFolderLike && 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.Errors = append(result.Errors, + fmt.Sprintf("node %s (%s): fs_path escapes vault: %s", n.ID, n.Title, n.FsPath)) result.Healthy = false - return + continue } - if _, err := os.Stat(physPath); os.IsNotExist(err) { + if info, err := os.Stat(physPath); err == nil { + if info.IsDir() { + result.FoldersOnDisk++ + } + } else { result.FilesMissing++ - result.Details = append(result.Details, fmt.Sprintf("node %s (%s): folder missing on disk: %s", n.ID, n.Title, physPath)) + 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)) @@ -90,7 +123,8 @@ func (a *App) VaultCheck() (*VaultCheckResult, error) { 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.Errors = append(result.Errors, + fmt.Sprintf("file %s: path escapes vault: %s", id, path)) result.Healthy = false continue } @@ -98,12 +132,14 @@ func (a *App) VaultCheck() (*VaultCheckResult, error) { result.FilesOnDisk++ } else { result.FilesMissing++ - result.Details = append(result.Details, fmt.Sprintf("file %s (%s): missing on disk: %s", id, path, absPath)) + result.Details = append(result.Details, + fmt.Sprintf("file %s (%s): missing on disk: %s", id, path, absPath)) } } } - if result.FilesMissing > 0 { + if result.FilesMissing > 0 || result.PathEscapeCount > 0 || + result.ParentIDEmptyCount > 0 || result.OrphanDescendantCount > 0 { result.Healthy = false } return result, nil