You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
111 lines
2.6 KiB
111 lines
2.6 KiB
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)
|
|
}
|
|
}
|