sync_apply FS-first rewrite; CreateNodeFromTemplate rollback; DeleteNodeAndChildren fail on trash errors; PLAN.md update
- applyRemoteNodeUpdate: FS-first with SafeVaultPath validation, must-fail os.Rename - applyRemoteNodeMove: FS-first for folders and notes/files - moveNodeFiles: rewritten FS-first with atomic DB transaction - applyRemoteNoteMove: delegates to moveNodeFiles - CreateNodeFromTemplate: rollbackChildren on any child creation failure - DeleteToTrash: skip rename if source file already missing - DeleteNodeAndChildren: fail on deleteFileRecords errors and trash move failures - docs/PLAN.md: update step 14 status with known gaps
This commit is contained in:
+24
-12
@@ -245,7 +245,9 @@ func (s *Service) DeleteToTrash(id string) error {
|
||||
if _, err := s.vaultPath(filepath.Join(".verstak", "trash", rec.ID+"_"+rec.Filename)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(src, dest); err != nil {
|
||||
if _, statErr := os.Stat(src); os.IsNotExist(statErr) {
|
||||
// File already gone — just remove the DB record.
|
||||
} else if err := os.Rename(src, dest); err != nil {
|
||||
return fmt.Errorf("move to trash: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -473,11 +475,17 @@ func (s *Service) DeleteNodeAndChildren(nodeID string) error {
|
||||
}
|
||||
collect(nodeID)
|
||||
|
||||
// Phase 1: FS trash moves (best-effort, collect errors).
|
||||
var trashErrors []string
|
||||
var movedTrash []struct{ src, dst string }
|
||||
// Phase 1: FS trash moves (with rollback on partial failure).
|
||||
type trashMove struct{ src, dst string }
|
||||
var movedTrash []trashMove
|
||||
for _, n := range toDelete {
|
||||
_ = s.deleteFileRecords(n.ID)
|
||||
if err := s.deleteFileRecords(n.ID); err != nil {
|
||||
// Undo completed trash moves.
|
||||
for _, mt := range movedTrash {
|
||||
_ = os.Rename(mt.dst, mt.src)
|
||||
}
|
||||
return fmt.Errorf("delete file records for %s: %w", n.ID, err)
|
||||
}
|
||||
if n.FsPath == "" {
|
||||
continue
|
||||
}
|
||||
@@ -494,15 +502,20 @@ func (s *Service) DeleteNodeAndChildren(nodeID string) error {
|
||||
}
|
||||
dst := filepath.Join(trashDir, n.ID+"_"+templates.SafeDisplayNameToPathSegment(n.Title))
|
||||
if err := os.Rename(src, dst); err != nil {
|
||||
trashErrors = append(trashErrors, fmt.Sprintf("node %s: %v", n.ID, err))
|
||||
} else {
|
||||
movedTrash = append(movedTrash, struct{ src, dst string }{src, dst})
|
||||
for _, mt := range movedTrash {
|
||||
_ = os.Rename(mt.dst, mt.src)
|
||||
}
|
||||
return fmt.Errorf("trash move node %s: %w", n.ID, err)
|
||||
}
|
||||
movedTrash = append(movedTrash, trashMove{src, dst})
|
||||
}
|
||||
|
||||
// Phase 2: DB soft-deletes in a single transaction.
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
for _, mt := range movedTrash {
|
||||
_ = os.Rename(mt.dst, mt.src)
|
||||
}
|
||||
return fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
@@ -513,12 +526,14 @@ func (s *Service) DeleteNodeAndChildren(nodeID string) error {
|
||||
`UPDATE nodes SET deleted_at=?, updated_at=? WHERE id=? AND deleted_at IS NULL`,
|
||||
t, t, n.ID)
|
||||
if err != nil {
|
||||
for _, mt := range movedTrash {
|
||||
_ = os.Rename(mt.dst, mt.src)
|
||||
}
|
||||
return fmt.Errorf("soft-delete %s: %w", n.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
// Rollback trash moves (best-effort).
|
||||
for _, mt := range movedTrash {
|
||||
if rerr := os.Rename(mt.dst, mt.src); rerr != nil {
|
||||
log.Printf("rollback trash move failed: %v", rerr)
|
||||
@@ -527,9 +542,6 @@ func (s *Service) DeleteNodeAndChildren(nodeID string) error {
|
||||
return fmt.Errorf("commit tx: %w", err)
|
||||
}
|
||||
|
||||
if len(trashErrors) > 0 {
|
||||
log.Printf("warn: trash errors during delete (DB was updated): %v", trashErrors)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user