package cmd
import (
"context"
"fmt"
"io"
"os"
"github.com/spf13/cobra"
"go.bigb.es/cacher/internal/archive"
)
var dirCmd = &cobra.Command{
Use: "dir",
Short: "Directory cache (tar+zstd of a tree)",
Long: `Cache resolved trees keyed by content hash (e.g. ~/go/pkg/mod
keyed by go.sum, or .rocks/ keyed by rockspec hash). Closes the biggest
gap left by the single-file shell helper this binary replaces.`,
}
var dirDownloadCmd = &cobra.Command{
Use: "download <key> <local-dir>",
Short: "Extract a cached directory into <local-dir>",
Long: `Streams s3://bucket/key through zstd-decode into <local-dir>.
With --exec, a cache miss falls back to running the given script with
` + "`sh -c`" + ` and then seeds the cache from <local-dir>. That collapses the
restore-or-build if/fi block CI manifests otherwise repeat per cache:
cacher dir download "$KEY" ~/scss --exec 'git clone … && cp -r …'
<local-dir> is created before the script runs, so the script can write
straight into it. With --optional, a miss exits 0 instead of 1 — for
caches whose absence just means a cold build.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cli, cfg, err := client()
if err != nil {
return err
}
key, err := resolveKey(args[0], cfg)
if err != nil {
return err
}
dest := args[1]
ctx := context.Background()
ok, err := cli.Exists(ctx, key)
if err != nil {
return err
}
if ok {
if err := os.MkdirAll(dest, 0o755); err != nil {
return err
}
fmt.Fprintf(cmd.ErrOrStderr(), "Cache HIT — %s → extract %s\n", key, dest)
body, err := cli.Get(ctx, key)
if err != nil {
return err
}
defer body.Close()
return archive.DecodeDir(body, dest)
}
fmt.Fprintf(cmd.ErrOrStderr(), "Cache MISS — %s\n", key)
switch {
case flagExec != "":
// Created up front so the script can `cp` into it without
// repeating a mkdir -p of its own.
if err := os.MkdirAll(dest, 0o755); err != nil {
return err
}
if err := runExecFallback(ctx, cmd, flagExec); err != nil {
return err
}
// Seed the cache so the next run hits. Best-effort: the
// script already produced the tree locally, and an S3
// hiccup is no reason to fail a build that has its
// content. Same trade-off as `docker download --pull`.
if err := uploadDir(ctx, cli, key, dest, cmd.ErrOrStderr()); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "warn: seed upload failed: %v\n", err)
return nil
}
fmt.Fprintf(cmd.ErrOrStderr(), "Cached → %s\n", key)
return nil
case flagOptional:
return nil
default:
return fmt.Errorf("%w: %s", ErrNotFound, key)
}
},
}
var dirUploadCmd = &cobra.Command{
Use: "upload <key> <local-dir>",
Short: "Pack a local directory (tar+zstd) and upload",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cli, cfg, err := client()
if err != nil {
return err
}
key, err := resolveKey(args[0], cfg)
if err != nil {
return err
}
src := args[1]
ctx := context.Background()
if !flagForce {
ok, err := cli.Exists(ctx, key)
if err != nil {
return err
}
if ok {
fmt.Fprintf(cmd.ErrOrStderr(), "Skipped — %s already present (use --force)\n", key)
return nil
}
}
return uploadDir(ctx, cli, key, src, cmd.ErrOrStderr())
},
}
// uploadDir packs src (tar+zstd) straight into an S3 upload — the
// encoder writes into one end of a pipe while the uploader reads the
// other, so nothing lands on disk. Shared by `dir upload` and the
// --exec seed path of `dir download`.
func uploadDir(ctx context.Context, cli s3Putter, key, src string, w io.Writer) error {
st, err := os.Stat(src)
if err != nil {
return err
}
if !st.IsDir() {
return fmt.Errorf("%s is not a directory", src)
}
fmt.Fprintf(w, "Packing %s → %s\n", src, key)
pr, pw := io.Pipe()
go func() {
err := archive.EncodeDir(pw, src)
pw.CloseWithError(err)
}()
return cli.Put(ctx, key, pr)
}
func init() {
for _, c := range []*cobra.Command{dirDownloadCmd, dirUploadCmd} {
addS3Flags(c)
addKeyFlags(c)
}
dirUploadCmd.Flags().BoolVar(&flagForce, "force", false, "overwrite if key already exists")
addExecFlag(dirDownloadCmd, "<local-dir>")
addOptionalFlag(dirDownloadCmd)
dirDownloadCmd.MarkFlagsMutuallyExclusive("exec", "optional")
dirCmd.AddCommand(dirDownloadCmd, dirUploadCmd)
rootCmd.AddCommand(dirCmd)
}