111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
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
|
|
}
|