135 lines
3.3 KiB
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 ""
|
|
}
|
|
}
|