package packed import ( "crypto/rand" "errors" "fmt" "io/fs" "os" "slices" "strings" "lindenii.org/go/furgit/object/store" ) var ( _ store.PackQuarantiner = (*Packed)(nil) _ store.PackQuarantine = (*packQuarantine)(nil) ) var errQuarantineNamesExhausted = errors.New("object/store/packed: exhausted quarantine directory names") // packQuarantine is one quarantined packed store // rooted privately beneath a destination pack root. type packQuarantine struct { *Packed parent *Packed tempName string tempRoot *os.Root } // BeginPackQuarantine creates one quarantined packed store // rooted privately beneath the destination pack root. // // Labels: Deps-Borrowed, Life-Parent. func (packed *Packed) BeginPackQuarantine(_ store.PackQuarantineOptions) (store.PackQuarantine, error) { //nolint:ireturn tempName, tempRoot, err := createPackQuarantineRoot(packed.root) if err != nil { return nil, err } quarantineStore, err := New(tempRoot, packed.objectFormat) if err != nil { _ = tempRoot.Close() _ = packed.root.RemoveAll(tempName) return nil, err } return &packQuarantine{ Packed: quarantineStore, parent: packed, tempName: tempName, tempRoot: tempRoot, }, nil } // Promote publishes the quarantined pack artifacts into the parent store, // refreshes the parent so the objects become available, // and invalidates the receiver. func (quarantine *packQuarantine) Promote() error { closeErr := quarantine.Close() promoteErr := quarantine.promoteAll() var refreshErr error if promoteErr == nil { refreshErr = quarantine.parent.Refresh() } tempRootErr := quarantine.tempRoot.Close() removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) return errors.Join(closeErr, promoteErr, refreshErr, tempRootErr, removeErr) } // Discard removes the quarantine and invalidates the receiver. func (quarantine *packQuarantine) Discard() error { closeErr := quarantine.Close() tempRootErr := quarantine.tempRoot.Close() removeErr := quarantine.parent.root.RemoveAll(quarantine.tempName) return errors.Join(closeErr, tempRootErr, removeErr) } // promoteAll links every pack artifact in the quarantine into the parent store, // in pack/rev/idx dependency order. func (quarantine *packQuarantine) promoteAll() error { entries, err := fs.ReadDir(quarantine.tempRoot.FS(), ".") if err != nil { return fmt.Errorf("object/store/packed: %w", err) } slices.SortFunc(entries, func(left, right fs.DirEntry) int { return packPromotionPriority(left.Name()) - packPromotionPriority(right.Name()) }) for _, entry := range entries { err := quarantine.promoteFile(entry.Name()) if err != nil { return err } } return nil } // promoteFile links one quarantined artifact into the parent store, // treating an already-present destination as success. func (quarantine *packQuarantine) promoteFile(name string) error { src := quarantine.tempName + "/" + name err := quarantine.parent.root.Link(src, name) if err != nil && !errors.Is(err, fs.ErrExist) { return fmt.Errorf("object/store/packed: promoting %q: %w", name, err) } _ = quarantine.parent.root.Remove(src) return nil } // createPackQuarantineRoot creates a private quarantine directory beneath parent // and returns its name and an os.Root over it. func createPackQuarantineRoot(parent *os.Root) (string, *os.Root, error) { for range 32 { name := "tmp_packq_" + rand.Text() err := parent.Mkdir(name, 0o700) if err != nil { if errors.Is(err, fs.ErrExist) { continue } return "", nil, fmt.Errorf("object/store/packed: %w", err) } root, err := parent.OpenRoot(name) if err != nil { _ = parent.RemoveAll(name) return "", nil, fmt.Errorf("object/store/packed: %w", err) } return name, root, nil } return "", nil, errQuarantineNamesExhausted } // packPromotionPriority orders pack artifacts // so that data files are linked before the index that publishes them. func packPromotionPriority(name string) int { switch { case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".pack"): return 1 case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".rev"): return 2 case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".bloom"): return 2 case strings.HasPrefix(name, "pack-") && strings.HasSuffix(name, ".idx"): return 3 default: return 0 } }