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.
DateKeys/cmd/datekeys/extract.go

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)
}
}

Powered by TurnKey Linux.