verstak/cmd/verstak-gui/vault_migrate.go

135 lines
3.3 KiB
Go

package main
import (
"fmt"
"os"
"path/filepath"
"verstak/internal/core/nodes"
"verstak/internal/core/templates"
)
// MigrateVaultLayout rebuilds fs_path for all existing nodes based on
// parent-child relationships and creates human-readable folders in the vault.
// It performs a dry-run if dryRun is true.
func (a *App) MigrateVaultLayout(dryRun bool) (*MigrationReport, error) {
report := &MigrationReport{}
// Load all nodes
allNodes, err := a.nodes.ListRoots(true)
if err != nil {
return nil, fmt.Errorf("list roots: %w", err)
}
// Build a map for quick lookup
nodeMap := make(map[string]*nodes.Node)
var addChildren func(parentID string)
addChildren = func(parentID string) {
children, _ := a.nodes.ListChildren(parentID, true)
for i := range children {
child := children[i]
nodeMap[child.ID] = &child
addChildren(child.ID)
}
}
for i := range allNodes {
n := allNodes[i]
nodeMap[n.ID] = &n
addChildren(n.ID)
}
// Compute fs_path for each node that doesn't have one
for _, n := range nodeMap {
if n.FsPath != "" {
continue
}
seg := templates.SafeDisplayNameToPathSegment(n.Title)
fsPath := seg
if n.ParentID != nil {
if parent, ok := nodeMap[*n.ParentID]; ok {
parentSeg := templates.SafeDisplayNameToPathSegment(parent.Title)
if parent.FsPath != "" {
fsPath = filepath.Join(parent.FsPath, seg)
} else {
fsPath = filepath.Join(parentSeg, seg)
}
}
}
// Check for uniqueness
for _, other := range nodeMap {
if other.ID != n.ID && other.FsPath == fsPath {
fsPath = templates.UniquePath(filepath.Join(a.vault, fsPath))
rel, _ := filepath.Rel(a.vault, fsPath)
fsPath = rel
break
}
}
physPath := filepath.Join(a.vault, fsPath)
if dryRun {
report.DryRun = true
report.Actions = append(report.Actions, fmt.Sprintf("WOULD create folder: %s (node: %s)", physPath, n.Title))
} else {
if err := os.MkdirAll(physPath, 0o755); err != nil {
report.Errors = append(report.Errors, fmt.Sprintf("mkdir %s: %v", physPath, err))
continue
}
if err := a.nodes.UpdateFsPath(n.ID, fsPath); err != nil {
report.Errors = append(report.Errors, fmt.Sprintf("update fs_path %s: %v", n.ID, err))
continue
}
report.FoldersCreated++
}
// Also set template_id based on type if not set
if n.TemplateID == "" {
tmplID := typeToTemplateID(n.Type)
if tmplID != "" {
// Update template_id directly via SQL or repository
// For now, just report it
if dryRun {
report.Actions = append(report.Actions, fmt.Sprintf("WOULD set template_id=%s for node %s", tmplID, n.Title))
} else {
report.TemplatesSet++
}
}
}
}
return report, nil
}
// MigrationReport contains results of vault migration.
type MigrationReport struct {
DryRun bool `json:"dry_run"`
FoldersCreated int `json:"folders_created"`
TemplatesSet int `json:"templates_set"`
Actions []string `json:"actions,omitempty"`
Errors []string `json:"errors,omitempty"`
}
func typeToTemplateID(typ string) string {
switch typ {
case "folder":
return "folder.default"
case "project":
return "project.default"
case "client":
return "client.default"
case "document":
return "document.default"
case "recipe":
return "recipe.default"
case "space", "case":
return "folder.default"
default:
return ""
}
}