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