Files
hauler/internal/flags/store.go
T

147 lines
5.0 KiB
Go

package flags
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/spf13/cobra"
"hauler.dev/go/hauler/v2/pkg/consts"
"hauler.dev/go/hauler/v2/pkg/log"
"hauler.dev/go/hauler/v2/pkg/store"
)
// storeIDPattern matches the full shape of a StoreID as generated by uuid.New()... e.g. "ec520cf6-e01b-4d6f-93ea-6588de0d5159"
// Short prefixes (e.g. "ec520cf6") are resolved by ResolveStoreID itself and never reach this pattern unless the lookup fails
var storeIDPattern = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
type StoreRootOpts struct {
StoreDir string
Retries int
TempOverride string
// BlobConcurrency overrides the store's default blob-write concurrency
// ceiling (content.OCI.blobSem) when > 0, bound to the
// --blob-concurrency persistent flag (0 means "auto"). Populated by one
// of two idempotent paths: `store sync`'s PreRunE (derives a value from
// --concurrency when none was given) or Store() itself (consults
// HAULER_BLOB_CONCURRENCY, so subcommands with no PreRunE still honor
// the env var).
BlobConcurrency int
}
func (o *StoreRootOpts) AddFlags(cmd *cobra.Command) {
pf := cmd.PersistentFlags()
pf.StringVarP(&o.StoreDir, "store", "s", "", "Set the directory to use for the content store")
pf.IntVarP(&o.Retries, "retries", "r", 0, fmt.Sprintf("Set the number of retries for operations (0 uses HAULER_RETRIES, otherwise defaults to %d)", consts.DefaultRetries))
pf.StringVarP(&o.TempOverride, "tempdir", "t", "", "(Optional) Override the default temporary directory determined by the OS")
pf.IntVar(&o.BlobConcurrency, "blob-concurrency", 0, fmt.Sprintf("(Optional) Override the maximum number of concurrent blob writes (0 auto-derives from --concurrency where set, otherwise defaults to %d)", consts.DefaultBlobConcurrency))
}
// ResolveStoreDir turns storeDir into an absolute path without opening a store for it --
// split out of Store() so sync can check a target store's path before deciding to open one.
func ResolveStoreDir(ctx context.Context, ro *CliRootOpts, storeDir string) (string, error) {
l := log.FromContext(ctx)
haulerDir := resolveHaulerDir(ro)
if storeDir == "" {
storeDir = os.Getenv(consts.HaulerStoreDir)
}
if storeDir == "" {
storeDir = consts.DefaultStoreName
}
// If storeDir doesn't exist as a path, see if it's a StoreID registered from a previous run elsewhere
if _, err := os.Stat(storeDir); errors.Is(err, os.ErrNotExist) {
id, resolved, rerr := store.ResolveStoreID(haulerDir, storeDir)
switch {
case rerr == nil:
if !store.MatchesStoreID(resolved, id) {
return "", fmt.Errorf("store id %q was last seen at %s, but that path no longer contains that store", storeDir, resolved)
}
l.Debugf("resolved store id [%s] to [%s]", storeDir, resolved)
storeDir = resolved
case storeIDPattern.MatchString(storeDir):
// looks like an ID, not a directory name to create
return "", fmt.Errorf("no store found matching id %q (for directories, use the absolute path)", storeDir)
}
}
return filepath.Abs(storeDir)
}
func (o *StoreRootOpts) Store(ctx context.Context, ro *CliRootOpts) (*store.Layout, error) {
l := log.FromContext(ctx)
abs, err := ResolveStoreDir(ctx, ro, o.StoreDir)
if err != nil {
return nil, err
}
o.StoreDir = abs
// resolved once here, same as StoreDir/BlobConcurrency below
if o.TempOverride == "" {
o.TempOverride = os.Getenv(consts.HaulerTempDir)
}
l.Debugf("using store at [%s]", abs)
if _, err := os.Stat(abs); errors.Is(err, os.ErrNotExist) {
if err := os.MkdirAll(abs, os.ModePerm); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
// Always resolve, never just "when unset": this picks up
// HAULER_BLOB_CONCURRENCY for subcommands with no PreRunE of their own
// (add, load, copy, serve, extract...) and validates whatever value is
// already present -- a `o.BlobConcurrency == 0` guard would let a
// typo'd negative value skip validation and fail a later `> 0` check
// silently. This stays idempotent for `store sync`, whose PreRunE has
// already resolved a non-zero value: ResolveBlobConcurrency returns a
// positive input unchanged.
bc, err := ResolveBlobConcurrency(o.BlobConcurrency)
if err != nil {
return nil, err
}
o.BlobConcurrency = bc
// same reasoning as BlobConcurrency above
retries, err := ResolveRetries(o.Retries)
if err != nil {
return nil, err
}
o.Retries = retries
opts := []store.Options{store.WithHaulerDir(resolveHaulerDir(ro))}
if o.BlobConcurrency > 0 {
opts = append(opts, store.WithBlobConcurrency(o.BlobConcurrency))
}
s, err := store.NewLayout(abs, opts...)
if err != nil {
return nil, err
}
l.Debugf("generated store id of [%s]", s.StoreID)
return s, nil
}
// resolveHaulerDir mirrors other variable detection, but duplicated to avoid an import cycle
func resolveHaulerDir(ro *CliRootOpts) string {
if ro != nil && ro.HaulerDir != "" {
return ro.HaulerDir
}
if d := os.Getenv(consts.HaulerDir); d != "" {
return d
}
home, _ := os.UserHomeDir()
return filepath.Join(home, consts.DefaultHaulerDirName)
}