mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-27 14:07:17 +00:00
147 lines
5.0 KiB
Go
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)
|
|
}
|