verstak/cmd/verstak-gui/vault_check.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
}