fix: complete vault layout transition — fs_path everywhere, no more spaces/
- notes.Create(): .md files stored in parent node's fs_path folder - files.CopyIntoVault/CreateEmptyFile/Duplicate: use parent fs_path - files.AddPathCopy/AddPathLink: use parent fs_path, set folder fs_path - files.DeleteNodeAndChildren: move physical folder to .verstak/trash - UpdateFsPathRecursive: use SafeDisplayNameToPathSegment(child.Title) - sync_apply.go note ops: use fs_path instead of spaces/ - internal/gui/server.go file upload: use n.FsPath instead of nodeSlug - VaultCheck diagnostic: walk nodes/files, verify paths on disk - Tests: create/rename/move/delete/name-conflict/vault-check all pass
This commit is contained in:
+50
-11
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"verstak/internal/core/nodes"
|
||||
"verstak/internal/core/storage"
|
||||
"verstak/internal/core/templates"
|
||||
"verstak/internal/core/util"
|
||||
)
|
||||
|
||||
@@ -55,6 +56,17 @@ func NewService(db *storage.DB, vaultRoot string, nodeRepo *nodes.Repository) *S
|
||||
return &Service{db: db, vaultRoot: vaultRoot, nodes: nodeRepo}
|
||||
}
|
||||
|
||||
func (s *Service) parentFsPath(parentID string) string {
|
||||
if parentID == "" {
|
||||
return ""
|
||||
}
|
||||
parent, err := s.nodes.GetActive(parentID)
|
||||
if err != nil || parent.FsPath == "" {
|
||||
return ""
|
||||
}
|
||||
return parent.FsPath
|
||||
}
|
||||
|
||||
// DB returns the underlying storage.
|
||||
func (s *Service) DB() *storage.DB {
|
||||
return s.db
|
||||
@@ -126,18 +138,18 @@ func (s *Service) AddExternal(nodeID, absPath string) (*Record, error) {
|
||||
}
|
||||
|
||||
// CopyIntoVault copies an external file into the vault.
|
||||
// The file lands at <vaultRoot>/spaces/<nodeSlug>/<filename>.
|
||||
func (s *Service) CopyIntoVault(nodeID, absPath, nodeSlug string) (*Record, error) {
|
||||
// The file lands at <vaultRoot>/<parentFsPath>/<filename>.
|
||||
func (s *Service) CopyIntoVault(nodeID, absPath, parentFsPath string) (*Record, error) {
|
||||
info, err := os.Stat(absPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
if nodeSlug == "" {
|
||||
nodeSlug = nodeID[:8]
|
||||
if parentFsPath == "" {
|
||||
parentFsPath = "."
|
||||
}
|
||||
|
||||
destDir := filepath.Join(s.vaultRoot, "spaces", nodeSlug)
|
||||
if _, err := s.vaultPath(filepath.Join("spaces", nodeSlug)); err != nil {
|
||||
destDir := filepath.Join(s.vaultRoot, parentFsPath)
|
||||
if _, err := s.vaultPath(parentFsPath); err != nil {
|
||||
return nil, fmt.Errorf("path safety: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(destDir, 0o750); err != nil {
|
||||
@@ -147,7 +159,6 @@ func (s *Service) CopyIntoVault(nodeID, absPath, nodeSlug string) (*Record, erro
|
||||
filename := filepath.Base(absPath)
|
||||
dest := filepath.Join(destDir, filename)
|
||||
|
||||
// If destination exists, add a numeric suffix.
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
ext := filepath.Ext(filename)
|
||||
name := strings.TrimSuffix(filename, ext)
|
||||
@@ -299,7 +310,11 @@ func (s *Service) CreateEmptyFile(parentID, filename string) (*nodes.Node, error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir := filepath.Join(s.vaultRoot, "spaces", node.Slug)
|
||||
parentFsPath := s.parentFsPath(parentID)
|
||||
dir := filepath.Join(s.vaultRoot, parentFsPath)
|
||||
if parentFsPath == "" {
|
||||
dir = s.vaultRoot
|
||||
}
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
return nil, fmt.Errorf("mkdir: %w", err)
|
||||
}
|
||||
@@ -342,7 +357,11 @@ func (s *Service) Duplicate(nodeID string) (*nodes.Node, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dir := filepath.Join(s.vaultRoot, "spaces", node.Slug)
|
||||
parentFsPath := s.parentFsPath(parentID)
|
||||
dir := filepath.Join(s.vaultRoot, parentFsPath)
|
||||
if parentFsPath == "" {
|
||||
dir = s.vaultRoot
|
||||
}
|
||||
os.MkdirAll(dir, 0o750)
|
||||
dst := filepath.Join(dir, newName)
|
||||
hash, err := copyAndHash(srcPath, dst)
|
||||
@@ -428,6 +447,17 @@ func (s *Service) DeleteNodeAndChildren(nodeID string) error {
|
||||
}
|
||||
}
|
||||
_ = s.deleteFileRecords(nodeID)
|
||||
// Move physical folder to trash if the node has fs_path
|
||||
n, err := s.nodes.GetActive(nodeID)
|
||||
if err == nil && n.FsPath != "" {
|
||||
src := filepath.Join(s.vaultRoot, n.FsPath)
|
||||
if info, err := os.Stat(src); err == nil && info.IsDir() {
|
||||
trashDir := filepath.Join(s.vaultRoot, ".verstak", "trash")
|
||||
os.MkdirAll(trashDir, 0o750)
|
||||
trashPath := filepath.Join(trashDir, n.ID+"_"+templates.SafeDisplayNameToPathSegment(n.Title))
|
||||
os.Rename(src, trashPath)
|
||||
}
|
||||
}
|
||||
return s.nodes.SoftDelete(nodeID)
|
||||
}
|
||||
|
||||
@@ -454,7 +484,8 @@ func (s *Service) importPath(parentID, sourcePath string, copyMode bool) ([]node
|
||||
return nil, err
|
||||
}
|
||||
if copyMode {
|
||||
_, err = s.CopyIntoVault(node.ID, sourcePath, node.Slug)
|
||||
parentFsPath := s.parentFsPath(parentID)
|
||||
_, err = s.CopyIntoVault(node.ID, sourcePath, parentFsPath)
|
||||
} else {
|
||||
_, err = s.AddExternal(node.ID, sourcePath)
|
||||
}
|
||||
@@ -473,6 +504,14 @@ func (s *Service) importDir(parentID, sourcePath, dirName string, copyMode bool)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parentFsPath := s.parentFsPath(parentID)
|
||||
seg := templates.SafeDisplayNameToPathSegment(dirName)
|
||||
folderFsPath := seg
|
||||
if parentFsPath != "" {
|
||||
folderFsPath = filepath.Join(parentFsPath, seg)
|
||||
}
|
||||
_ = s.nodes.UpdateFsPath(folderNode.ID, folderFsPath)
|
||||
|
||||
entries, err := os.ReadDir(sourcePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -495,7 +534,7 @@ func (s *Service) importDir(parentID, sourcePath, dirName string, copyMode bool)
|
||||
return nil, err
|
||||
}
|
||||
if copyMode {
|
||||
_, err = s.CopyIntoVault(childNode.ID, childPath, childNode.Slug)
|
||||
_, err = s.CopyIntoVault(childNode.ID, childPath, folderFsPath)
|
||||
} else {
|
||||
_, err = s.AddExternal(childNode.ID, childPath)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestCopyIntoVault(t *testing.T) {
|
||||
srcFile := filepath.Join(srcDir, "doc.pdf")
|
||||
os.WriteFile(srcFile, []byte("PDF content here"), 0o640)
|
||||
|
||||
rec, err := svc.CopyIntoVault("node-1", srcFile, "my-node")
|
||||
rec, err := svc.CopyIntoVault("node-1", srcFile, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CopyIntoVault: %v", err)
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestDeleteToTrash(t *testing.T) {
|
||||
src := filepath.Join(t.TempDir(), "important.pdf")
|
||||
os.WriteFile(src, []byte("important data"), 0o640)
|
||||
|
||||
rec, _ := svc.CopyIntoVault("node-x", src, "node-x")
|
||||
rec, _ := svc.CopyIntoVault("node-x", src, "")
|
||||
|
||||
if err := svc.DeleteToTrash(rec.ID); err != nil {
|
||||
t.Fatalf("DeleteToTrash: %v", err)
|
||||
@@ -247,7 +247,7 @@ func TestDeleteNodeAndChildren(t *testing.T) {
|
||||
// Add file record to child.
|
||||
src := filepath.Join(t.TempDir(), "child.txt")
|
||||
os.WriteFile(src, []byte("data"), 0o640)
|
||||
svc.CopyIntoVault(child.ID, src, child.Slug)
|
||||
svc.CopyIntoVault(child.ID, src, "")
|
||||
|
||||
if err := svc.DeleteNodeAndChildren(parent.ID); err != nil {
|
||||
t.Fatalf("DeleteNodeAndChildren: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user