package main import ( "crypto/rand" "encoding/hex" "errors" "io" "os" "path" "path/filepath" "strings" "time" "g.activething.com/go/DateKeys/capsule" ) // dirSink writes the files of a format 3 capsule to a new directory (spec // ยง56). Begin claims dir with os.Mkdir, which fails if it exists, and stages // the tree in dir/.datekeys-*, through an os.Root, which follows no link out // of dir, with O_EXCL and mode 0600; R10 keeps every entry of the head off // that name. Commit sets the mtimes and moves each entry of the first level // into dir. Abort removes dir. A capsule without files creates nothing. type dirSink struct { dir string head *capsule.Head root *os.Root stage string created bool } func (s *dirSink) Begin(h *capsule.Head) error { s.head = h if len(h.Files) == 0 { return nil } if err := os.Mkdir(s.dir, 0o700); err != nil { if errors.Is(err, os.ErrExist) { return errors.New(s.dir + " already exists; outputs are never overwritten") } return err } s.created = true root, err := os.OpenRoot(s.dir) if err != nil { return err } s.root = root var id [8]byte _, _ = rand.Read(id[:]) // never fails since Go 1.24 s.stage = ".datekeys-" + hex.EncodeToString(id[:]) return root.Mkdir(s.stage, 0o700) } // staged is the name, within the root, of file i while it is staged. func (s *dirSink) staged(i int) string { return filepath.FromSlash(s.stage + "/" + s.head.Files[i].Path) } func (s *dirSink) Create(i int) (io.WriteCloser, error) { name := s.staged(i) if dir := filepath.Dir(name); dir != s.stage { if err := s.root.MkdirAll(dir, 0o700); err != nil { return nil, err } } return s.root.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) } // Commit runs at step 18, once every check of step 17 has passed. func (s *dirSink) Commit() error { if s.root == nil { return nil } for i, f := range s.head.Files { if f.HasMTime { t := time.Unix(int64(f.MTime), 0) if err := s.root.Chtimes(s.staged(i), t, t); err != nil { return err } } } moved := map[string]bool{} for _, f := range s.head.Files { first, _, _ := strings.Cut(f.Path, "/") if moved[first] { continue } if err := s.root.Rename(filepath.FromSlash(path.Join(s.stage, first)), first); err != nil { return err } moved[first] = true } if err := s.root.Remove(s.stage); err != nil { return err } // The files are in place: closing the root changes nothing of them. s.root.Close() s.root = nil return nil } // Abort removes everything the sink created: dir itself. func (s *dirSink) Abort() { if s.root != nil { s.root.Close() } if s.created { os.RemoveAll(s.dir) } }