package loose import ( "crypto/rand" "errors" "fmt" "io/fs" "os" "path/filepath" "lindenii.org/go/furgit/object/store" ) var ( _ store.ObjectQuarantiner = (*Loose)(nil) _ store.ObjectQuarantine = (*objectQuarantine)(nil) ) // objectQuarantine is one quarantined loose store // rooted privately beneath a destination loose root. type objectQuarantine struct { *Loose parent *Loose tempName string tempRoot *os.Root } // BeginObjectQuarantine creates one quarantined loose store rooted privately // beneath the destination loose root. // // Labels: Deps-Borrowed, Life-Parent, Close-No. func (loose *Loose) BeginObjectQuarantine(_ store.ObjectQuarantineOptions) (store.ObjectQuarantine, error) { //nolint:ireturn tempName, tempRoot, err := createLooseQuarantineRoot(loose.root) if err != nil { return nil, err } quarantineStore, err := New(tempRoot, loose.objectFormat) if err != nil { _ = tempRoot.Close() _ = loose.root.RemoveAll(tempName) return nil, err } return &objectQuarantine{ Loose: quarantineStore, parent: loose, tempName: tempName, tempRoot: tempRoot, }, nil } // Discard removes the quarantine and invalidates the receiver. func (quarantine *objectQuarantine) Discard() error { closeErr := quarantine.Close() tempRootErr := quarantine.tempRoot.Close() removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) if closeErr != nil { return closeErr } if tempRootErr != nil { return fmt.Errorf("object/store/loose: %w", tempRootErr) } if removeErr != nil { return fmt.Errorf("object/store/loose: %w", removeErr) } return nil } // Promote publishes all quarantined loose objects into the parent loose store // and invalidates the receiver. func (quarantine *objectQuarantine) Promote() error { closeErr := quarantine.Close() promoteErr := promoteLooseQuarantine(quarantine.parent, quarantine.tempName, quarantine.tempRoot) tempRootErr := quarantine.tempRoot.Close() removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) if closeErr != nil { return closeErr } if promoteErr != nil { return promoteErr } if tempRootErr != nil { return fmt.Errorf("object/store/loose: %w", tempRootErr) } if removeErr != nil { return fmt.Errorf("object/store/loose: %w", removeErr) } return nil } func createLooseQuarantineRoot(parent *os.Root) (string, *os.Root, error) { var lastErr error for range 32 { name := "tmp_looseq_" + rand.Text() err := parent.Mkdir(name, 0o700) if err == nil { root, err := parent.OpenRoot(name) if err == nil { return name, root, nil } _ = parent.RemoveAll(name) return "", nil, fmt.Errorf("object/store/loose: %w", err) } lastErr = err if errors.Is(err, fs.ErrExist) { continue } return "", nil, fmt.Errorf("object/store/loose: %w", err) } return "", nil, fmt.Errorf("object/store/loose: failed to create quarantine directory: %w", lastErr) } func promoteLooseQuarantine(parent *Loose, tempName string, tempRoot *os.Root) error { entries, err := fs.ReadDir(tempRoot.FS(), ".") if err != nil && !errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("object/store/loose: %w", err) } for _, entry := range entries { if !entry.IsDir() { return fmt.Errorf("%w: quarantine contains unexpected file %q", store.ErrInvalidObject, entry.Name()) } err := promoteLooseQuarantineShard(parent, tempName, tempRoot, entry.Name()) if err != nil { return err } } return nil } func promoteLooseQuarantineShard(parent *Loose, tempName string, tempRoot *os.Root, shard string) error { entries, err := fs.ReadDir(tempRoot.FS(), shard) if err != nil { return fmt.Errorf("object/store/loose: %w", err) } for _, entry := range entries { if entry.IsDir() { return fmt.Errorf("%w: quarantine shard %q contains unexpected directory %q", store.ErrInvalidObject, shard, entry.Name()) } objectID, err := parent.objectFormat.FromString(shard + entry.Name()) if err != nil { return fmt.Errorf("%w: quarantine shard %q contains invalid object %q: %w", store.ErrInvalidObject, shard, entry.Name(), err) } dst, err := parent.objectPath(objectID) if err != nil { return err } err = parent.root.MkdirAll(shard, 0o755) if err != nil { return fmt.Errorf("object/store/loose: %w", err) } err = promoteLooseQuarantineObject(parent.root, filepath.Join(tempName, shard, entry.Name()), dst) if err != nil { return err } } return nil } func promoteLooseQuarantineObject(root *os.Root, src, dst string) error { err := root.Link(src, dst) if err == nil { _ = root.Remove(src) return nil } if errors.Is(err, fs.ErrExist) { _ = root.Remove(src) return nil } return fmt.Errorf("object/store/loose: promote quarantine %q -> %q: %w", src, dst, err) }