// Atomic manifest committer. // // Commit() walks the sandbox "upper" directory and migrates every produced // file into the live root using the "safe swap" pattern: // // 1. Copy (or reflink) the new file to .sorcery_tmp // 2. os.Rename() — atomic at the kernel level — replaces the old file // // On success, the file list is written to the Manifests bucket in the same // bbolt transaction so the disk and the database always agree. If the // commit fails part-way, the journal entry is marked StateFailed and the // .sorcery_tmp leftovers are cleaned up on the next engine start. package state import ( "bytes" "fmt" "log" bolt "go.etcd.io/bbolt" ) // Commit migrates files from the sandbox upper directory into the live // root atomically. upperDir is the Box.WorkDir; relPath is computed // against "/" so a sandbox-produced /usr/bin/wget becomes /usr/bin/wget // on the host. // // If dryRun is true, no files are written — only the manifest is recorded. // Used by `sorcery cast --dry-run`. func (m *Manager) Commit(spellName, variant, upperDir string, dryRun bool) ([]string, error) { var installed []string err := filepath.Walk(upperDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil } relPath, e := filepath.Rel(upperDir, path) if e != nil { return e } target := filepath.Join("/", relPath) if !dryRun { if err := copyFileAtomic(path, target, info.Mode()); err != nil { return fmt.Errorf("commit: %s: %w", target, err) } } installed = append(installed, target) return nil }) if err != nil { return nil, err } // Persist the manifest + reverse index in a single transaction. if err := m.SaveManifest(spellName, variant, installed); err != nil { return nil, err } return installed, nil } // copyFileAtomic uses reflink (btrfs/xfs FICLONE) when available, falling // back to a plain io.Copy + atomic rename. The intermediate .sorcery_tmp // file is cleaned up on error. func copyFileAtomic(src, dst string, mode os.FileMode) error { if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { return err } tmp := dst + ".sorcery_tmp" // Try reflink first (instant on btrfs/xfs, zero extra disk). if err := reflinkOrCopy(src, tmp, mode); err != nil { return err } // Atomic rename — replaces dst atomically even if it already exists. if err := os.Rename(tmp, dst); err != nil { _ = os.Remove(tmp) return fmt.Errorf("rename %s -> %s: %w", tmp, dst, err) } return nil } // reflinkOrCopy tries the Linux FICLONE ioctl first; on any error it // falls back to a buffered io.Copy. func reflinkOrCopy(src, dst string, mode os.FileMode) error { if err := reflink(src, dst); err == nil { // Reflink preserves mode/owner but we set them explicitly to be safe. _ = os.Chmod(dst, mode) return nil } // Fallback: plain copy. in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) if err != nil { return err } if _, err := io.Copy(out, in); err != nil { out.Close() return err } return out.Close() } // Dispel removes every file recorded in the spell's manifest and deletes // the manifest + reverse-index entries. Used to "banish" a spell from the // live system. func (m *Manager) Dispel(spellName, variant string) error { files, err := m.GetManifest(spellName, variant) if err != nil { return err } // Remove files. Empty parent dirs are pruned best-effort. for _, f := range files { if err := os.Remove(f); err != nil { log.Printf("dispel: warning: remove %s: %v", f, err) } // Walk up pruning empty dirs (stop at /). dir := filepath.Dir(f) for dir != "/" && dir != "." { if err := os.Remove(dir); err != nil { break // not empty — stop } dir = filepath.Dir(dir) } } // Drop manifest + reverse-index entries. // Collect index keys first to avoid modifying the bucket while iterating. return m.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte("Manifests")) if err := b.Delete([]byte(spellName + ":" + variant)); err != nil { return err } idx := tx.Bucket([]byte("Index")) c := idx.Cursor() owner := []byte(spellName + ":" + variant) var toDelete [][]byte for k, v := c.First(); k != nil; k, v = c.Next() { if bytes.Equal(v, owner) { // Copy the key since bbolt reuses the slice on Next(). keyCopy := make([]byte, len(k)) copy(keyCopy, k) toDelete = append(toDelete, keyCopy) } } for _, k := range toDelete { if err := idx.Delete(k); err != nil { log.Printf("dispel: warning: index delete %s: %v", string(k), err) } } return nil }) }