147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"verstak/internal/core/nodes"
|
|
)
|
|
|
|
type VaultCheckResult struct {
|
|
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}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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
|
|
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.Healthy = false
|
|
continue
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check file records
|
|
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.PathEscapeCount > 0 ||
|
|
result.ParentIDEmptyCount > 0 || result.OrphanDescendantCount > 0 {
|
|
result.Healthy = false
|
|
}
|
|
return result, nil
|
|
}
|