diff --git a/client.go b/client.go index 6dbe30e..3a73784 100644 --- a/client.go +++ b/client.go @@ -62,7 +62,7 @@ func ListKeySlots(ctx context.Context, rawStore store.ObjectStore) ([]KeySlot, e if err := requireEncryptedRepo(ctx, rawStore); err != nil { return nil, err } - slots, err := keychain.LoadKeySlots(rawStore) + slots, err := keychain.LoadKeySlots(ctx, rawStore) if err != nil { return nil, fmt.Errorf("load key slots: %w", err) } @@ -75,7 +75,7 @@ func ChangePassword(ctx context.Context, rawStore store.ObjectStore, kc keychain if err := requireEncryptedRepo(ctx, rawStore); err != nil { return err } - slots, err := keychain.LoadKeySlots(rawStore) + slots, err := keychain.LoadKeySlots(ctx, rawStore) if err != nil { return fmt.Errorf("load key slots: %w", err) } @@ -87,7 +87,7 @@ func ChangePassword(ctx context.Context, rawStore store.ObjectStore, kc keychain if err != nil { return err } - return keychain.ChangePasswordSlot(rawStore, masterKey, newPassword) + return keychain.ChangePasswordSlot(ctx, rawStore, masterKey, newPassword) } // AddRecoveryKey generates a BIP39 recovery key for the repository, @@ -97,7 +97,7 @@ func AddRecoveryKey(ctx context.Context, rawStore store.ObjectStore, kc keychain if err := requireEncryptedRepo(ctx, rawStore); err != nil { return "", err } - slots, err := keychain.LoadKeySlots(rawStore) + slots, err := keychain.LoadKeySlots(ctx, rawStore) if err != nil { return "", fmt.Errorf("load key slots: %w", err) } @@ -105,7 +105,7 @@ func AddRecoveryKey(ctx context.Context, rawStore store.ObjectStore, kc keychain if err != nil { return "", fmt.Errorf("unlock repository: %w", err) } - return keychain.AddRecoverySlot(rawStore, masterKey) + return keychain.AddRecoverySlot(ctx, rawStore, masterKey) } // LoadRepoConfig reads the repository marker from a raw (undecorated) store. @@ -224,7 +224,7 @@ type Client struct { reporter ui.Reporter } -func NewClient(base store.ObjectStore, opts ...ClientOption) (*Client, error) { +func NewClient(ctx context.Context, base store.ObjectStore, opts ...ClientOption) (*Client, error) { c := &Client{ enablePackfile: true, // Packfile is enabled by default reporter: ui.NewNoOpReporter(), @@ -235,7 +235,7 @@ func NewClient(base store.ObjectStore, opts ...ClientOption) (*Client, error) { // Auto-detect encryption from the repo config if no explicit key is set. if len(c.encryptionKey) == 0 { - encKey, err := c.resolveKeyFromConfig(base) + encKey, err := c.resolveKeyFromConfig(ctx, base) if err != nil { return nil, err } @@ -277,8 +277,7 @@ func NewClient(base store.ObjectStore, opts ...ClientOption) (*Client, error) { // resolveKeyFromConfig reads the repo config and, if the repository is // encrypted, uses the Keychain to resolve the master key and derive the encryption key. -func (c *Client) resolveKeyFromConfig(base store.ObjectStore) ([]byte, error) { - ctx := context.Background() +func (c *Client) resolveKeyFromConfig(ctx context.Context, base store.ObjectStore) ([]byte, error) { cfg, err := LoadRepoConfig(ctx, base) if err != nil { return nil, fmt.Errorf("read repo config: %w", err) @@ -289,7 +288,7 @@ func (c *Client) resolveKeyFromConfig(base store.ObjectStore) ([]byte, error) { if !cfg.Encrypted { return nil, nil } - slots, err := keychain.LoadKeySlots(base) + slots, err := keychain.LoadKeySlots(ctx, base) if err != nil { return nil, fmt.Errorf("load key slots: %w", err) } diff --git a/client_test.go b/client_test.go index 1911c66..8e93719 100644 --- a/client_test.go +++ b/client_test.go @@ -152,7 +152,7 @@ func TestChangePassword(t *testing.T) { t.Fatalf("ChangePassword: %v", err) } - slots, _ := keychain.LoadKeySlots(s) + slots, _ := keychain.LoadKeySlots(ctx, s) if _, err := (keychain.Chain{keychain.WithPassword("old-pass")}).Resolve(ctx, slots); err == nil { t.Error("old password should no longer work") } @@ -191,7 +191,7 @@ func TestAddRecoveryKey(t *testing.T) { t.Error("expected non-empty mnemonic") } - slots, _ := keychain.LoadKeySlots(s) + slots, _ := keychain.LoadKeySlots(ctx, s) hasRecovery := false for _, slot := range slots { if slot.SlotType == "recovery" { @@ -241,7 +241,7 @@ func TestClient_Cat_SingleObject(t *testing.T) { t.Fatalf("Failed to put config: %v", err) } - client, err := NewClient(base) + client, err := NewClient(context.Background(), base) if err != nil { t.Fatalf("Failed to create client: %v", err) } @@ -289,7 +289,7 @@ func TestClient_Cat_MultipleObjects(t *testing.T) { } } - client, err := NewClient(base) + client, err := NewClient(context.Background(), base) if err != nil { t.Fatalf("Failed to create client: %v", err) } @@ -323,7 +323,7 @@ func TestClient_Cat_ObjectNotFound(t *testing.T) { t.Fatalf("Failed to setup mock data: %v", err) } - client, err := NewClient(base) + client, err := NewClient(context.Background(), base) if err != nil { t.Fatalf("Failed to create client: %v", err) } @@ -347,7 +347,7 @@ func TestClient_Cat_NoKeys(t *testing.T) { t.Fatalf("Failed to setup mock data: %v", err) } - client, err := NewClient(base) + client, err := NewClient(context.Background(), base) if err != nil { t.Fatalf("Failed to create client: %v", err) } @@ -385,7 +385,7 @@ func TestClient_Cat_WithEncryption(t *testing.T) { } // Create client with encryption - it will wrap the store - client, err := NewClient(base, WithEncryptionKey(encKey)) + client, err := NewClient(context.Background(), base, WithEncryptionKey(encKey)) if err != nil { t.Fatalf("Failed to create client: %v", err) } @@ -438,7 +438,7 @@ func TestClient_Cat_WithCompression(t *testing.T) { t.Fatalf("Failed to setup mock data: %v", err) } - client, err := NewClient(base) + client, err := NewClient(context.Background(), base) if err != nil { t.Fatalf("Failed to create client: %v", err) } diff --git a/cmd/cloudstic/cmd_auth.go b/cmd/cloudstic/cmd_auth.go index a30489e..409c084 100644 --- a/cmd/cloudstic/cmd_auth.go +++ b/cmd/cloudstic/cmd_auth.go @@ -13,7 +13,7 @@ import ( "github.com/cloudstic/cli/internal/paths" ) -func (r *runner) runAuth() int { +func (r *runner) runAuth(ctx context.Context) int { if len(os.Args) < 3 { _, _ = fmt.Fprintln(r.errOut, "Usage: cloudstic auth [options]") _, _ = fmt.Fprintln(r.errOut, "") @@ -23,19 +23,19 @@ func (r *runner) runAuth() int { switch os.Args[2] { case "list": - return r.runAuthList() + return r.runAuthList(ctx) case "show": - return r.runAuthShow() + return r.runAuthShow(ctx) case "new": - return r.runAuthNew() + return r.runAuthNew(ctx) case "login": - return r.runAuthLogin() + return r.runAuthLogin(ctx) default: return r.fail("Unknown auth subcommand: %s", os.Args[2]) } } -func (r *runner) runAuthList() int { +func (r *runner) runAuthList(ctx context.Context) int { fs := flag.NewFlagSet("auth list", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") _ = fs.Parse(reorderArgs(fs, os.Args[3:])) @@ -52,7 +52,7 @@ func (r *runner) runAuthList() int { return 0 } -func (r *runner) runAuthShow() int { +func (r *runner) runAuthShow(ctx context.Context) int { fs := flag.NewFlagSet("auth show", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") _ = fs.Parse(reorderArgs(fs, os.Args[3:])) @@ -73,7 +73,7 @@ func (r *runner) runAuthShow() int { return r.fail("usage: cloudstic auth show [-profiles-file ] ") } names := sortedKeys(cfg.Auth) - picked, pickErr := r.promptSelect("Select auth entry", names) + picked, pickErr := r.promptSelect(ctx, "Select auth entry", names) if pickErr != nil { return r.fail("Failed to select auth entry: %v", pickErr) } @@ -88,7 +88,7 @@ func (r *runner) runAuthShow() int { return 0 } -func (r *runner) runAuthNew() int { +func (r *runner) runAuthNew(ctx context.Context) int { fs := flag.NewFlagSet("auth new", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") name := fs.String("name", "", "Auth reference name") @@ -101,7 +101,7 @@ func (r *runner) runAuthNew() int { if *name == "" { if r.canPrompt() { - v, err := r.promptLine("Auth reference name", "") + v, err := r.promptLine(ctx, "Auth reference name", "") if err != nil { return r.fail("Failed to read auth reference name: %v", err) } @@ -116,7 +116,7 @@ func (r *runner) runAuthNew() int { } if *provider != "google" && *provider != "onedrive" { if r.canPrompt() { - picked, err := r.promptSelect("Select auth provider", []string{"google", "onedrive"}) + picked, err := r.promptSelect(ctx, "Select auth provider", []string{"google", "onedrive"}) if err != nil { return r.fail("Failed to read auth provider: %v", err) } @@ -132,7 +132,7 @@ func (r *runner) runAuthNew() int { if *googleTokenFile == "" { if r.canPrompt() { def := defaultAuthTokenPath("google", *name) - v, err := r.promptLine("Google token file path", def) + v, err := r.promptLine(ctx, "Google token file path", def) if err != nil { return r.fail("Failed to read google token file path: %v", err) } @@ -152,7 +152,7 @@ func (r *runner) runAuthNew() int { if *onedriveTokenFile == "" { if r.canPrompt() { def := defaultAuthTokenPath("onedrive", *name) - v, err := r.promptLine("OneDrive token file path", def) + v, err := r.promptLine(ctx, "OneDrive token file path", def) if err != nil { return r.fail("Failed to read onedrive token file path: %v", err) } @@ -183,7 +183,7 @@ func (r *runner) runAuthNew() int { return 0 } -func (r *runner) runAuthLogin() int { +func (r *runner) runAuthLogin(ctx context.Context) int { fs := flag.NewFlagSet("auth login", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") name := fs.String("name", "", "Auth reference name") @@ -197,7 +197,7 @@ func (r *runner) runAuthLogin() int { if *name == "" { if r.canPrompt() { names := sortedKeys(cfg.Auth) - picked, pickErr := r.promptSelect("Select auth entry", names) + picked, pickErr := r.promptSelect(ctx, "Select auth entry", names) if pickErr != nil { return r.fail("Failed to select auth entry: %v", pickErr) } @@ -214,7 +214,6 @@ func (r *runner) runAuthLogin() int { } g := newAuthGlobalFlags() - ctx := context.Background() switch auth.Provider { case "google": diff --git a/cmd/cloudstic/cmd_auth_test.go b/cmd/cloudstic/cmd_auth_test.go index d5f81b1..f60b119 100644 --- a/cmd/cloudstic/cmd_auth_test.go +++ b/cmd/cloudstic/cmd_auth_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "path/filepath" "strings" @@ -21,14 +22,14 @@ func TestRunAuthNewAndListAndShow(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("auth new failed: %s", errOut.String()) } os.Args = []string{"cloudstic", "auth", "list", "-profiles-file", profilesPath} out.Reset() errOut.Reset() - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("auth list failed: %s", errOut.String()) } if !strings.Contains(out.String(), "Auth") || !strings.Contains(out.String(), "google-work") || !strings.Contains(out.String(), "PROVIDER") { @@ -38,7 +39,7 @@ func TestRunAuthNewAndListAndShow(t *testing.T) { os.Args = []string{"cloudstic", "auth", "show", "-profiles-file", profilesPath, "google-work"} out.Reset() errOut.Reset() - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("auth show failed: %s", errOut.String()) } if !strings.Contains(out.String(), "Auth google-work") || !strings.Contains(out.String(), "Provider Details") || !strings.Contains(out.String(), "/tmp/google-work.json") { @@ -51,7 +52,7 @@ func TestRunAuth_NoSubcommand(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 1 { + if code := r.runAuth(context.Background()); code != 1 { t.Fatalf("expected exit code 1, got %d", code) } if !strings.Contains(errOut.String(), "Available subcommands") { @@ -64,7 +65,7 @@ func TestRunAuth_UnknownSubcommand(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 1 { + if code := r.runAuth(context.Background()); code != 1 { t.Fatalf("expected exit code 1, got %d", code) } if !strings.Contains(errOut.String(), "Unknown auth subcommand") { @@ -87,7 +88,7 @@ func TestRunAuthNew_OneDriveProvider(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("auth new failed: %s", errOut.String()) } @@ -95,7 +96,7 @@ func TestRunAuthNew_OneDriveProvider(t *testing.T) { os.Args = []string{"cloudstic", "auth", "show", "-profiles-file", profilesPath, "od-personal"} out.Reset() errOut.Reset() - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("auth show failed: %s", errOut.String()) } got := out.String() @@ -114,7 +115,7 @@ func TestRunAuthNew_RequiresName(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut, noPrompt: true} - if code := r.runAuth(); code != 1 { + if code := r.runAuth(context.Background()); code != 1 { t.Fatalf("expected exit code 1, got %d", code) } if !strings.Contains(errOut.String(), "-name is required") { @@ -130,7 +131,7 @@ func TestRunAuthNew_RequiresProvider(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut, noPrompt: true} - if code := r.runAuth(); code != 1 { + if code := r.runAuth(context.Background()); code != 1 { t.Fatalf("expected exit code 1, got %d", code) } if !strings.Contains(errOut.String(), "-provider must be") { @@ -146,7 +147,7 @@ func TestRunAuthNew_InvalidName(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 1 { + if code := r.runAuth(context.Background()); code != 1 { t.Fatalf("expected exit code 1, got %d", code) } if !strings.Contains(errOut.String(), "invalid auth name") { @@ -169,7 +170,7 @@ func TestRunAuthShow_UnknownAuth(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("setup auth new failed: %s", errOut.String()) } @@ -177,7 +178,7 @@ func TestRunAuthShow_UnknownAuth(t *testing.T) { os.Args = []string{"cloudstic", "auth", "show", "-profiles-file", profilesPath, "missing"} out.Reset() errOut.Reset() - if code := r.runAuth(); code != 1 { + if code := r.runAuth(context.Background()); code != 1 { t.Fatalf("expected exit code 1, got %d", code) } if !strings.Contains(errOut.String(), "Unknown auth") { @@ -193,7 +194,7 @@ func TestRunAuthList_MissingFile(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("expected exit code 0, got %d; errOut: %s", code, errOut.String()) } } @@ -230,7 +231,7 @@ func TestRunAuthNew_OneDriveDerivesTokenFile(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("auth new failed: %s", errOut.String()) } @@ -252,7 +253,7 @@ func TestRunAuthNew_DerivesDefaultTokenFile(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runAuth(); code != 0 { + if code := r.runAuth(context.Background()); code != 0 { t.Fatalf("auth new failed: %s", errOut.String()) } raw, err := os.ReadFile(profilesPath) diff --git a/cmd/cloudstic/cmd_backup.go b/cmd/cloudstic/cmd_backup.go index f0d5cca..073a4cf 100644 --- a/cmd/cloudstic/cmd_backup.go +++ b/cmd/cloudstic/cmd_backup.go @@ -88,7 +88,7 @@ func parseBackupArgs() *backupArgs { return a } -func (r *runner) runBackup() int { +func (r *runner) runBackup(ctx context.Context) int { a := parseBackupArgs() if a.profile != "" && a.allProfiles { @@ -147,7 +147,7 @@ func (r *runner) runSingleBackup(a *backupArgs) int { return r.fail("Failed to init source: %v", err) } - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_breaklock.go b/cmd/cloudstic/cmd_breaklock.go index ef1bca5..297f032 100644 --- a/cmd/cloudstic/cmd_breaklock.go +++ b/cmd/cloudstic/cmd_breaklock.go @@ -19,9 +19,9 @@ func parseBreakLockArgs() *breakLockArgs { return a } -func (r *runner) runBreakLock() int { +func (r *runner) runBreakLock(ctx context.Context) int { a := parseBreakLockArgs() - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_breaklock_test.go b/cmd/cloudstic/cmd_breaklock_test.go index 87eada4..63cefc0 100644 --- a/cmd/cloudstic/cmd_breaklock_test.go +++ b/cmd/cloudstic/cmd_breaklock_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "strings" "testing" @@ -13,7 +14,7 @@ func TestRunBreakLock_NoLock(t *testing.T) { var errOut strings.Builder r := &runner{out: &strings.Builder{}, errOut: &errOut, client: &stubClient{breakLockResult: nil}} - r.runBreakLock() + r.runBreakLock(context.Background()) if !strings.Contains(errOut.String(), "not locked") { t.Errorf("expected 'not locked' message, got:\n%s", errOut.String()) @@ -35,7 +36,7 @@ func TestRunBreakLock_LocksRemoved(t *testing.T) { }, }} - r.runBreakLock() + r.runBreakLock(context.Background()) got := errOut.String() if !strings.Contains(got, "Locks removed") { diff --git a/cmd/cloudstic/cmd_cat.go b/cmd/cloudstic/cmd_cat.go index 9b1e0da..b6423cc 100644 --- a/cmd/cloudstic/cmd_cat.go +++ b/cmd/cloudstic/cmd_cat.go @@ -42,9 +42,9 @@ func parseCatArgs() *catArgs { return a } -func (r *runner) runCat() int { +func (r *runner) runCat(ctx context.Context) int { a := parseCatArgs() - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_cat_test.go b/cmd/cloudstic/cmd_cat_test.go index 89f93a0..9dcd0a9 100644 --- a/cmd/cloudstic/cmd_cat_test.go +++ b/cmd/cloudstic/cmd_cat_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "strings" "testing" @@ -17,7 +18,7 @@ func TestRunCat_SingleKey_JSON(t *testing.T) { }, }} - r.runCat() + r.runCat(context.Background()) got := out.String() if !strings.Contains(got, `"version"`) { @@ -35,7 +36,7 @@ func TestRunCat_MultipleKeys_HeadersShown(t *testing.T) { }, }} - r.runCat() + r.runCat(context.Background()) got := errOut.String() if !strings.Contains(got, "==> config <==") { @@ -56,7 +57,7 @@ func TestRunCat_QuietMode_NoHeaders(t *testing.T) { }, }} - r.runCat() + r.runCat(context.Background()) if strings.Contains(errOut.String(), "==>") { t.Errorf("quiet mode should not show headers, got:\n%s", errOut.String()) @@ -71,7 +72,7 @@ func TestRunCat_RawMode(t *testing.T) { catResults: []*cloudstic.CatResult{{Key: "config", Data: rawData}}, }} - r.runCat() + r.runCat(context.Background()) if out.String() != string(rawData) { t.Errorf("raw mode: expected %q, got %q", rawData, out.String()) @@ -85,7 +86,7 @@ func TestRunCat_InvalidJSON_PrintsRaw(t *testing.T) { catResults: []*cloudstic.CatResult{{Key: "config", Data: []byte("not-json")}}, }} - r.runCat() + r.runCat(context.Background()) if !strings.Contains(out.String(), "not-json") { t.Errorf("expected raw fallback output, got:\n%s", out.String()) diff --git a/cmd/cloudstic/cmd_check.go b/cmd/cloudstic/cmd_check.go index 32c8f87..67c76e1 100644 --- a/cmd/cloudstic/cmd_check.go +++ b/cmd/cloudstic/cmd_check.go @@ -28,9 +28,9 @@ func parseCheckArgs() *checkArgs { return a } -func (r *runner) runCheck() int { +func (r *runner) runCheck(ctx context.Context) int { a := parseCheckArgs() - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_check_test.go b/cmd/cloudstic/cmd_check_test.go index 5e68ae2..52861e8 100644 --- a/cmd/cloudstic/cmd_check_test.go +++ b/cmd/cloudstic/cmd_check_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "strings" "testing" @@ -20,7 +21,7 @@ func TestRunCheck_Healthy(t *testing.T) { }, }} - r.runCheck() + r.runCheck(context.Background()) got := errOut.String() if !strings.Contains(got, "No errors found") { diff --git a/cmd/cloudstic/cmd_diff.go b/cmd/cloudstic/cmd_diff.go index 6ed423e..0f9462c 100644 --- a/cmd/cloudstic/cmd_diff.go +++ b/cmd/cloudstic/cmd_diff.go @@ -30,9 +30,9 @@ func parseDiffArgs() *diffArgs { return a } -func (r *runner) runDiff() int { +func (r *runner) runDiff(ctx context.Context) int { a := parseDiffArgs() - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_diff_test.go b/cmd/cloudstic/cmd_diff_test.go index a3e1190..5857888 100644 --- a/cmd/cloudstic/cmd_diff_test.go +++ b/cmd/cloudstic/cmd_diff_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "strings" "testing" @@ -24,7 +25,7 @@ func TestRunDiff_Success(t *testing.T) { }, }} - r.runDiff() + r.runDiff(context.Background()) got := out.String() if !strings.Contains(got, "snapshot/aaa") { @@ -52,7 +53,7 @@ func TestRunDiff_NoChanges(t *testing.T) { }, }} - r.runDiff() + r.runDiff(context.Background()) lines := strings.Split(strings.TrimSpace(out.String()), "\n") if len(lines) != 1 { diff --git a/cmd/cloudstic/cmd_forget.go b/cmd/cloudstic/cmd_forget.go index 6c8922b..6629e60 100644 --- a/cmd/cloudstic/cmd_forget.go +++ b/cmd/cloudstic/cmd_forget.go @@ -83,9 +83,9 @@ func parseForgetArgs() *forgetArgs { return a } -func (r *runner) runForget() int { +func (r *runner) runForget(ctx context.Context) int { a := parseForgetArgs() - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_forget_test.go b/cmd/cloudstic/cmd_forget_test.go index 559ecf1..9204094 100644 --- a/cmd/cloudstic/cmd_forget_test.go +++ b/cmd/cloudstic/cmd_forget_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "strings" "testing" @@ -17,7 +18,7 @@ func TestRunForget_SingleSnapshot(t *testing.T) { forgetResult: &cloudstic.ForgetResult{Prune: nil}, }} - r.runForget() + r.runForget(context.Background()) if !strings.Contains(out.String(), "Snapshot removed.") { t.Errorf("expected 'Snapshot removed.', got:\n%s", out.String()) @@ -37,7 +38,7 @@ func TestRunForget_SingleSnapshot_WithPruneResult(t *testing.T) { }, }} - r.runForget() + r.runForget(context.Background()) got := out.String() if !strings.Contains(got, "Snapshot removed.") { @@ -63,7 +64,7 @@ func TestRunForget_Policy_NoRemove(t *testing.T) { }, }} - r.runForget() + r.runForget(context.Background()) got := out.String() if !strings.Contains(got, "No snapshots to remove") { @@ -86,7 +87,7 @@ func TestRunForget_Policy_WithRemoval(t *testing.T) { }, }} - r.runForget() + r.runForget(context.Background()) got := out.String() if !strings.Contains(got, "1 snapshots have been removed") { @@ -108,7 +109,7 @@ func TestRunForget_Policy_DryRun(t *testing.T) { }, }} - r.runForget() + r.runForget(context.Background()) got := out.String() if !strings.Contains(got, "would remove") { diff --git a/cmd/cloudstic/cmd_init.go b/cmd/cloudstic/cmd_init.go index eaf1266..b921397 100644 --- a/cmd/cloudstic/cmd_init.go +++ b/cmd/cloudstic/cmd_init.go @@ -33,7 +33,7 @@ func parseInitArgs() *initArgs { return a } -func (r *runner) runInit() int { +func (r *runner) runInit(ctx context.Context) int { a := parseInitArgs() raw, err := a.g.openStore() diff --git a/cmd/cloudstic/cmd_key.go b/cmd/cloudstic/cmd_key.go index 2cef156..0331fdb 100644 --- a/cmd/cloudstic/cmd_key.go +++ b/cmd/cloudstic/cmd_key.go @@ -13,7 +13,7 @@ import ( "github.com/moby/term" ) -func (r *runner) runKey() int { +func (r *runner) runKey(ctx context.Context) int { if len(os.Args) < 3 { _, _ = fmt.Fprintln(r.errOut, "Usage: cloudstic key ") _, _ = fmt.Fprintln(r.errOut) @@ -29,11 +29,11 @@ func (r *runner) runKey() int { switch sub { case "list": - return r.runKeyList() + return r.runKeyList(ctx) case "add-recovery": - return r.runAddRecoveryKey() + return r.runAddRecoveryKey(ctx) case "passwd": - return r.runKeyPasswd() + return r.runKeyPasswd(ctx) default: return r.fail("Unknown key subcommand: %s", sub) } @@ -50,7 +50,7 @@ func parseKeyListArgs() *keyListArgs { return a } -func (r *runner) runKeyList() int { +func (r *runner) runKeyList(ctx context.Context) int { a := parseKeyListArgs() raw, err := a.g.openStore() @@ -58,7 +58,7 @@ func (r *runner) runKeyList() int { return r.fail("Failed to init store: %v", err) } - slots, err := cloudstic.ListKeySlots(context.Background(), raw) + slots, err := cloudstic.ListKeySlots(ctx, raw) if err != nil { return r.fail("Failed to list key slots: %v", err) } @@ -101,11 +101,9 @@ func parseKeyPasswdArgs() *keyPasswdArgs { return a } -func (r *runner) runKeyPasswd() int { +func (r *runner) runKeyPasswd(ctx context.Context) int { a := parseKeyPasswdArgs() - ctx := context.Background() - raw, err := a.g.openStore() if err != nil { return r.fail("Failed to init store: %v", err) @@ -150,11 +148,9 @@ func parseAddRecoveryKeyArgs() *addRecoveryKeyArgs { return a } -func (r *runner) runAddRecoveryKey() int { +func (r *runner) runAddRecoveryKey(ctx context.Context) int { a := parseAddRecoveryKeyArgs() - ctx := context.Background() - raw, err := a.g.openStore() if err != nil { return r.fail("Failed to init store: %v", err) diff --git a/cmd/cloudstic/cmd_list.go b/cmd/cloudstic/cmd_list.go index 09b4482..4550316 100644 --- a/cmd/cloudstic/cmd_list.go +++ b/cmd/cloudstic/cmd_list.go @@ -23,9 +23,9 @@ func parseListArgs() *listArgs { return a } -func (r *runner) runList() int { +func (r *runner) runList(ctx context.Context) int { a := parseListArgs() - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_list_test.go b/cmd/cloudstic/cmd_list_test.go index d4da150..465e8c4 100644 --- a/cmd/cloudstic/cmd_list_test.go +++ b/cmd/cloudstic/cmd_list_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "strings" "testing" @@ -22,7 +23,7 @@ func TestRunList_Success(t *testing.T) { }, }} - r.runList() + r.runList(context.Background()) got := out.String() if !strings.Contains(got, "2 snapshots") { @@ -37,7 +38,7 @@ func TestRunList_Empty(t *testing.T) { listResult: &cloudstic.ListResult{Snapshots: nil}, }} - r.runList() + r.runList(context.Background()) if !strings.Contains(out.String(), "0 snapshots") { t.Errorf("expected '0 snapshots', got: %s", out.String()) @@ -68,7 +69,7 @@ func TestRunList_Group(t *testing.T) { }, }} - r.runList() + r.runList(context.Background()) got := out.String() if !strings.Contains(got, "2 snapshots") { diff --git a/cmd/cloudstic/cmd_ls.go b/cmd/cloudstic/cmd_ls.go index d914e44..cdbe75e 100644 --- a/cmd/cloudstic/cmd_ls.go +++ b/cmd/cloudstic/cmd_ls.go @@ -30,9 +30,9 @@ func parseLsArgs() *lsArgs { return a } -func (r *runner) runLsSnapshot() int { +func (r *runner) runLsSnapshot(ctx context.Context) int { a := parseLsArgs() - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_profile.go b/cmd/cloudstic/cmd_profile.go index ef3e7d7..b189448 100644 --- a/cmd/cloudstic/cmd_profile.go +++ b/cmd/cloudstic/cmd_profile.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "flag" "fmt" @@ -22,7 +23,7 @@ func defaultProfilesPath() (string, error) { return filepath.Join(configDir, defaultProfilesFilename), nil } -func (r *runner) runProfile() int { +func (r *runner) runProfile(ctx context.Context) int { if len(os.Args) < 3 { _, _ = fmt.Fprintln(r.errOut, "Usage: cloudstic profile [options]") _, _ = fmt.Fprintln(r.errOut, "") @@ -32,11 +33,11 @@ func (r *runner) runProfile() int { switch os.Args[2] { case "list": - return r.runProfileList() + return r.runProfileList(ctx) case "show": - return r.runProfileShow() + return r.runProfileShow(ctx) case "new": - return r.runProfileNew() + return r.runProfileNew(ctx) default: return r.fail("Unknown profile subcommand: %s", os.Args[2]) } @@ -66,7 +67,7 @@ func parseProfileShowArgs() (*profileShowArgs, error) { return a, nil } -func (r *runner) runProfileShow() int { +func (r *runner) runProfileShow(ctx context.Context) int { a, err := parseProfileShowArgs() if err != nil { return r.fail("%v", err) @@ -80,7 +81,7 @@ func (r *runner) runProfileShow() int { return r.fail("usage: cloudstic profile show [-profiles-file ] ") } names := sortedKeys(cfg.Profiles) - picked, pickErr := r.promptSelect("Select profile", names) + picked, pickErr := r.promptSelect(ctx, "Select profile", names) if pickErr != nil { return r.fail("Failed to select profile: %v", pickErr) } @@ -124,7 +125,7 @@ func parseProfileListArgs() *profileListArgs { return a } -func (r *runner) runProfileList() int { +func (r *runner) runProfileList(ctx context.Context) int { a := parseProfileListArgs() cfg, err := cloudstic.LoadProfilesFile(a.profilesFile) if err != nil { @@ -206,11 +207,11 @@ func parseProfileNewArgs() *profileNewArgs { return a } -func (r *runner) runProfileNew() int { +func (r *runner) runProfileNew(ctx context.Context) int { a := parseProfileNewArgs() if a.name == "" { if r.canPrompt() { - v, err := r.promptLine("Profile name", "") + v, err := r.promptLine(ctx, "Profile name", "") if err != nil { return r.fail("Failed to read profile name: %v", err) } @@ -237,7 +238,7 @@ func (r *runner) runProfileNew() int { if a.source == "" { if r.canPrompt() { - v, err := r.promptLine("Source URI", "") + v, err := r.promptLine(ctx, "Source URI", "") if err != nil { return r.fail("Failed to read source URI: %v", err) } @@ -252,7 +253,7 @@ func (r *runner) runProfileNew() int { } if a.store != "" && a.storeRef == "" { if r.canPrompt() { - v, err := r.promptLine("Store reference name", "default-store") + v, err := r.promptLine(ctx, "Store reference name", "default-store") if err != nil { return r.fail("Failed to read store reference: %v", err) } @@ -278,7 +279,7 @@ func (r *runner) runProfileNew() int { } else { // No store provided — prompt or fail. if r.canPrompt() { - ref, created, code := r.promptStoreSelection(cfg) + ref, created, code := r.promptStoreSelection(ctx, cfg) if code != 0 { return code } @@ -293,9 +294,9 @@ func (r *runner) runProfileNew() int { if createdStore && r.canPrompt() { s := cfg.Stores[a.storeRef] if !storeHasExplicitEncryption(s) { - r.promptEncryptionConfig(cfg, a.storeRef, a.profilesFile) + r.promptEncryptionConfig(ctx, cfg, a.storeRef, a.profilesFile) } - if err := r.checkOrInitStore(cfg, a.storeRef, a.profilesFile, checkOrInitOptions{ + if err := r.checkOrInitStore(ctx, cfg, a.storeRef, a.profilesFile, checkOrInitOptions{ allowMissingSecrets: true, warnOnMissingSecrets: true, offerInit: true, @@ -337,7 +338,7 @@ func (r *runner) runProfileNew() int { } else if requiredProvider != "" { // Cloud source without -auth-ref — prompt or fail. if r.canPrompt() { - ref, code := r.promptAuthSelection(cfg, requiredProvider, a.name) + ref, code := r.promptAuthSelection(ctx, cfg, requiredProvider, a.name) if code != 0 { return code } @@ -375,14 +376,14 @@ func (r *runner) runProfileNew() int { // promptStoreSelection prompts the user to pick an existing store or create a // new one. It returns the chosen store-ref name, whether a new store was // created, and exit code 0 on success. -func (r *runner) promptStoreSelection(cfg *cloudstic.ProfilesConfig) (string, bool, int) { +func (r *runner) promptStoreSelection(ctx context.Context, cfg *cloudstic.ProfilesConfig) (string, bool, int) { options := []string{"Create new store"} for name := range cfg.Stores { options = append(options, name) } sort.Strings(options[1:]) - picked, err := r.promptSelect("Select a store", options) + picked, err := r.promptSelect(ctx, "Select a store", options) if err != nil { return "", false, r.fail("Failed to select store: %v", err) } @@ -392,14 +393,14 @@ func (r *runner) promptStoreSelection(cfg *cloudstic.ProfilesConfig) (string, bo } // Create a new store inline. - refName, err := r.promptLine("Store reference name", "default-store") + refName, err := r.promptLine(ctx, "Store reference name", "default-store") if err != nil { return "", false, r.fail("Failed to read store reference name: %v", err) } if refName == "" { return "", false, r.fail("Store reference name is required") } - uri, err := r.promptLine("Store URI (e.g. s3:bucket/path, local:/path, sftp://host/path)", "") + uri, err := r.promptLine(ctx, "Store URI (e.g. s3:bucket/path, local:/path, sftp://host/path)", "") if err != nil { return "", false, r.fail("Failed to read store URI: %v", err) } @@ -416,7 +417,7 @@ func (r *runner) promptStoreSelection(cfg *cloudstic.ProfilesConfig) (string, bo // promptAuthSelection prompts the user to pick an existing auth entry (filtered // by provider) or create a new one. It returns the chosen auth-ref name and // exit code 0 on success. The new entry is added to cfg.Auth in place. -func (r *runner) promptAuthSelection(cfg *cloudstic.ProfilesConfig, provider, profileName string) (string, int) { +func (r *runner) promptAuthSelection(ctx context.Context, cfg *cloudstic.ProfilesConfig, provider, profileName string) (string, int) { options := []string{"Create new auth"} for name, auth := range cfg.Auth { if auth.Provider == provider { @@ -425,7 +426,7 @@ func (r *runner) promptAuthSelection(cfg *cloudstic.ProfilesConfig, provider, pr } sort.Strings(options[1:]) - picked, err := r.promptSelect(fmt.Sprintf("Select %s auth entry", provider), options) + picked, err := r.promptSelect(ctx, fmt.Sprintf("Select %s auth entry", provider), options) if err != nil { return "", r.fail("Failed to select auth entry: %v", err) } @@ -433,7 +434,7 @@ func (r *runner) promptAuthSelection(cfg *cloudstic.ProfilesConfig, provider, pr return picked, 0 } - refName, err := r.promptLine("Auth reference name", provider+"-"+profileName) + refName, err := r.promptLine(ctx, "Auth reference name", provider+"-"+profileName) if err != nil { return "", r.fail("Failed to read auth reference name: %v", err) } @@ -441,7 +442,7 @@ func (r *runner) promptAuthSelection(cfg *cloudstic.ProfilesConfig, provider, pr return "", r.fail("Auth reference name is required") } - tokenFile, err := r.promptLine("Token file path", defaultAuthTokenPath(provider, refName)) + tokenFile, err := r.promptLine(ctx, "Token file path", defaultAuthTokenPath(provider, refName)) if err != nil { return "", r.fail("Failed to read token file path: %v", err) } diff --git a/cmd/cloudstic/cmd_profile_test.go b/cmd/cloudstic/cmd_profile_test.go index d3f63de..8907866 100644 --- a/cmd/cloudstic/cmd_profile_test.go +++ b/cmd/cloudstic/cmd_profile_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "path/filepath" "strings" @@ -40,7 +41,7 @@ profiles: var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } @@ -80,7 +81,7 @@ profiles: var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } got := out.String() @@ -104,7 +105,7 @@ func TestRunProfileShow_UnknownProfile(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "Unknown profile") { @@ -118,7 +119,7 @@ func TestRunProfileList_MissingFile(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("expected zero exit code, got=%d err=%s", code, errOut.String()) } if out.String() != "" { @@ -135,7 +136,7 @@ func TestRunProfile_UnknownSubcommand(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatalf("expected non-zero exit code") } if !strings.Contains(errOut.String(), "Unknown profile subcommand") { @@ -159,7 +160,7 @@ func TestRunProfileNew_CreatesFile(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } @@ -193,7 +194,7 @@ func TestRunProfileNew_PrefillsExistingProfile(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("initial create: code=%d err=%s", code, errOut.String()) } @@ -206,7 +207,7 @@ func TestRunProfileNew_PrefillsExistingProfile(t *testing.T) { } out.Reset() errOut.Reset() - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("update: code=%d err=%s", code, errOut.String()) } @@ -242,7 +243,7 @@ func TestRunProfileNew_RequiresNameAndSource(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "-source is required") { @@ -263,7 +264,7 @@ func TestRunProfileNew_RequiresStore(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "-store-ref is required") { @@ -285,7 +286,7 @@ func TestRunProfileNew_RejectsUnknownStoreRef(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "Unknown store reference") { @@ -307,7 +308,7 @@ func TestRunProfileNew_CloudSourceRequiresAuthRef(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "-auth-ref is required for cloud sources") { @@ -330,7 +331,7 @@ func TestRunProfileNew_RejectsUnknownAuthRef(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "Unknown auth reference") { @@ -353,7 +354,7 @@ func TestRunProfileNew_AuthRefRequiresCloudSource(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "-auth-ref requires a cloud source") { @@ -435,7 +436,7 @@ profiles: var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } got := out.String() @@ -466,7 +467,7 @@ profiles: var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } got := out.String() @@ -498,7 +499,7 @@ profiles: var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } got := out.String() @@ -526,7 +527,7 @@ func TestRunProfileNew_WithExcludesAndTags(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } @@ -556,7 +557,7 @@ func TestRunProfileNew_InvalidName(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "invalid profile name") { @@ -578,7 +579,7 @@ func TestRunProfileNew_InvalidSource(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "Invalid source") { @@ -600,7 +601,7 @@ func TestRunProfileNew_InvalidStoreURI(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code == 0 { + if code := r.runProfile(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "Invalid store URI") { @@ -634,7 +635,7 @@ profiles: var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } got := out.String() @@ -676,7 +677,7 @@ profiles: var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runProfile(); code != 0 { + if code := r.runProfile(context.Background()); code != 0 { t.Fatalf("runProfile() code=%d err=%s", code, errOut.String()) } got := out.String() diff --git a/cmd/cloudstic/cmd_prune.go b/cmd/cloudstic/cmd_prune.go index 74306cb..062a91f 100644 --- a/cmd/cloudstic/cmd_prune.go +++ b/cmd/cloudstic/cmd_prune.go @@ -24,9 +24,9 @@ func parsePruneArgs() *pruneArgs { return a } -func (r *runner) runPrune() int { +func (r *runner) runPrune(ctx context.Context) int { a := parsePruneArgs() - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_prune_test.go b/cmd/cloudstic/cmd_prune_test.go index 2623b34..551dd11 100644 --- a/cmd/cloudstic/cmd_prune_test.go +++ b/cmd/cloudstic/cmd_prune_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "os" "strings" "testing" @@ -20,7 +21,7 @@ func TestRunPrune_Normal(t *testing.T) { }, }} - r.runPrune() + r.runPrune(context.Background()) got := out.String() if !strings.Contains(got, "Prune complete.") { @@ -48,7 +49,7 @@ func TestRunPrune_DryRun(t *testing.T) { }, }} - r.runPrune() + r.runPrune(context.Background()) got := out.String() if !strings.Contains(got, "Prune dry run complete.") { diff --git a/cmd/cloudstic/cmd_restore.go b/cmd/cloudstic/cmd_restore.go index f80e3d8..43e4f78 100644 --- a/cmd/cloudstic/cmd_restore.go +++ b/cmd/cloudstic/cmd_restore.go @@ -41,7 +41,7 @@ func parseRestoreArgs() *restoreArgs { return a } -func (r *runner) runRestore() int { +func (r *runner) runRestore(ctx context.Context) int { a := parseRestoreArgs() format, err := resolveRestoreFormat(a.format, a.output) if err != nil { @@ -49,7 +49,7 @@ func (r *runner) runRestore() int { } a.format = format - if err := r.openClient(a.g); err != nil { + if err := r.openClient(ctx, a.g); err != nil { return r.fail("Failed to init store: %v", err) } diff --git a/cmd/cloudstic/cmd_store.go b/cmd/cloudstic/cmd_store.go index 4dc695d..ee0fb95 100644 --- a/cmd/cloudstic/cmd_store.go +++ b/cmd/cloudstic/cmd_store.go @@ -15,7 +15,7 @@ import ( "github.com/cloudstic/cli/pkg/store" ) -func (r *runner) runStore() int { +func (r *runner) runStore(ctx context.Context) int { if len(os.Args) < 3 { _, _ = fmt.Fprintln(r.errOut, "Usage: cloudstic store [options]") _, _ = fmt.Fprintln(r.errOut, "") @@ -25,21 +25,21 @@ func (r *runner) runStore() int { switch os.Args[2] { case "list": - return r.runStoreList() + return r.runStoreList(ctx) case "show": - return r.runStoreShow() + return r.runStoreShow(ctx) case "new": - return r.runStoreNew() + return r.runStoreNew(ctx) case "verify": - return r.runStoreVerify() + return r.runStoreVerify(ctx) case "init": - return r.runStoreInit() + return r.runStoreInit(ctx) default: return r.fail("Unknown store subcommand: %s", os.Args[2]) } } -func (r *runner) runStoreList() int { +func (r *runner) runStoreList(ctx context.Context) int { fs := flag.NewFlagSet("store list", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") _ = fs.Parse(reorderArgs(fs, os.Args[3:])) @@ -56,7 +56,7 @@ func (r *runner) runStoreList() int { return 0 } -func (r *runner) runStoreShow() int { +func (r *runner) runStoreShow(ctx context.Context) int { fs := flag.NewFlagSet("store show", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") _ = fs.Parse(reorderArgs(fs, os.Args[3:])) @@ -78,7 +78,7 @@ func (r *runner) runStoreShow() int { return r.fail("usage: cloudstic store show [-profiles-file ] ") } names := sortedKeys(cfg.Stores) - picked, pickErr := r.promptSelect("Select store", names) + picked, pickErr := r.promptSelect(ctx, "Select store", names) if pickErr != nil { return r.fail("Failed to select store: %v", pickErr) } @@ -93,7 +93,7 @@ func (r *runner) runStoreShow() int { return 0 } -func (r *runner) runStoreNew() int { +func (r *runner) runStoreNew(ctx context.Context) int { fs := flag.NewFlagSet("store new", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") name := fs.String("name", "", "Store reference name") @@ -158,7 +158,7 @@ func (r *runner) runStoreNew() int { if *name == "" { if r.canPrompt() { - v, err := r.promptLine("Store reference name", "") + v, err := r.promptLine(ctx, "Store reference name", "") if err != nil { return r.fail("Failed to read store name: %v", err) } @@ -191,7 +191,7 @@ func (r *runner) runStoreNew() int { if *uri == "" || forcePromptURI { if r.canPrompt() { - v, err := r.promptLine("Store URI", *uri) + v, err := r.promptLine(ctx, "Store URI", *uri) if err != nil { return r.fail("Failed to read store URI: %v", err) } @@ -218,16 +218,16 @@ func (r *runner) runStoreNew() int { // If no encryption flags were provided, prompt for encryption config. s := cfg.Stores[*name] if askKeepEncryption { - keepCurrent, confirmErr := r.promptConfirm("Keep current encryption settings?", true) + keepCurrent, confirmErr := r.promptConfirm(ctx, "Keep current encryption settings?", true) if confirmErr != nil { return r.fail("Failed to read encryption confirmation: %v", confirmErr) } forcePromptEncryption = !keepCurrent } if forcePromptEncryption || !storeHasExplicitEncryption(s) { - r.promptEncryptionConfig(cfg, *name, *profilesFile) + r.promptEncryptionConfig(ctx, cfg, *name, *profilesFile) } - if err := r.checkOrInitStore(cfg, *name, *profilesFile, checkOrInitOptions{ + if err := r.checkOrInitStore(ctx, cfg, *name, *profilesFile, checkOrInitOptions{ allowMissingSecrets: true, warnOnMissingSecrets: !existedBefore, offerInit: true, @@ -244,7 +244,7 @@ func (r *runner) runStoreNew() int { // initialize it. Encryption config should already be saved in profiles.yaml // before calling this. Errors are printed but never cause a non-zero exit— // the store config has already been saved. -func (r *runner) runStoreVerify() int { +func (r *runner) runStoreVerify(ctx context.Context) int { fs := flag.NewFlagSet("store verify", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") _ = fs.Parse(reorderArgs(fs, os.Args[3:])) @@ -270,7 +270,7 @@ func (r *runner) runStoreVerify() int { return r.fail("usage: cloudstic store verify [-profiles-file ] ") } names := sortedKeys(cfg.Stores) - picked, pickErr := r.promptSelect("Select store", names) + picked, pickErr := r.promptSelect(ctx, "Select store", names) if pickErr != nil { return r.fail("Failed to select store: %v", pickErr) } @@ -280,7 +280,7 @@ func (r *runner) runStoreVerify() int { if _, ok := cfg.Stores[name]; !ok { return r.fail("Unknown store %q", name) } - if err := r.checkOrInitStore(cfg, name, *profilesFile, checkOrInitOptions{ + if err := r.checkOrInitStore(ctx, cfg, name, *profilesFile, checkOrInitOptions{ warnOnMissingSecrets: true, }); err != nil { return r.fail("%v", err) @@ -288,7 +288,7 @@ func (r *runner) runStoreVerify() int { return 0 } -func (r *runner) runStoreInit() int { +func (r *runner) runStoreInit(ctx context.Context) int { fs := flag.NewFlagSet("store init", flag.ExitOnError) profilesFile := fs.String("profiles-file", envDefault("CLOUDSTIC_PROFILES_FILE", defaultProfilesPathFallback()), "Path to profiles YAML file") yes := fs.Bool("yes", false, "Initialize without confirmation prompt") @@ -315,7 +315,7 @@ func (r *runner) runStoreInit() int { return r.fail("usage: cloudstic store init [-profiles-file ] [-yes] ") } names := sortedKeys(cfg.Stores) - picked, pickErr := r.promptSelect("Select store", names) + picked, pickErr := r.promptSelect(ctx, "Select store", names) if pickErr != nil { return r.fail("Failed to select store: %v", pickErr) } @@ -325,7 +325,7 @@ func (r *runner) runStoreInit() int { if _, ok := cfg.Stores[name]; !ok { return r.fail("Unknown store %q", name) } - if err := r.checkOrInitStore(cfg, name, *profilesFile, checkOrInitOptions{ + if err := r.checkOrInitStore(ctx, cfg, name, *profilesFile, checkOrInitOptions{ warnOnMissingSecrets: true, offerInit: true, assumeYes: *yes, @@ -342,7 +342,7 @@ type checkOrInitOptions struct { assumeYes bool } -func (r *runner) checkOrInitStore(cfg *cloudstic.ProfilesConfig, storeName, profilesFile string, opts checkOrInitOptions) error { +func (r *runner) checkOrInitStore(ctx context.Context, cfg *cloudstic.ProfilesConfig, storeName, profilesFile string, opts checkOrInitOptions) error { s := cfg.Stores[storeName] g, err := globalFlagsFromProfileStore(s) if err != nil { @@ -360,8 +360,6 @@ func (r *runner) checkOrInitStore(cfg *cloudstic.ProfilesConfig, storeName, prof return fmt.Errorf("could not connect to store: %w", err) } - ctx := context.Background() - // Check if already initialized by looking for the config marker. cfgData, err := raw.Get(ctx, "config") if err == nil && cfgData != nil { @@ -385,7 +383,7 @@ func (r *runner) checkOrInitStore(cfg *cloudstic.ProfilesConfig, storeName, prof return nil } if !opts.assumeYes { - yes, promptErr := r.promptConfirm("Initialize it now?", true) + yes, promptErr := r.promptConfirm(ctx, "Initialize it now?", true) if promptErr != nil || !yes { return nil } @@ -425,7 +423,7 @@ func (r *runner) checkOrInitStore(cfg *cloudstic.ProfilesConfig, storeName, prof // promptEncryptionConfig guides the user through encryption configuration // and saves the chosen settings to profiles.yaml. It does not build a keychain // or prompt for the actual password — that happens later during init. -func (r *runner) promptEncryptionConfig(cfg *cloudstic.ProfilesConfig, storeName, profilesFile string) { +func (r *runner) promptEncryptionConfig(ctx context.Context, cfg *cloudstic.ProfilesConfig, storeName, profilesFile string) { _, _ = fmt.Fprintln(r.out) _, _ = fmt.Fprintln(r.out, "No encryption is configured for this store.") @@ -435,13 +433,14 @@ func (r *runner) promptEncryptionConfig(cfg *cloudstic.ProfilesConfig, storeName "AWS KMS key (enterprise)", "No encryption (not recommended)", } - picked, err := r.promptSelect("Select encryption method", options) + picked, err := r.promptSelect(ctx, "Select encryption method", options) if err != nil { _, _ = fmt.Fprintf(r.errOut, "Failed to select encryption method: %v\n", err) return } s, err := configureStoreEncryptionSelection( + ctx, cfg.Stores[storeName], storeName, picked, @@ -465,15 +464,16 @@ func (r *runner) promptEncryptionConfig(cfg *cloudstic.ProfilesConfig, storeName } func configureStoreEncryptionSelection( + ctx context.Context, s cloudstic.ProfileStore, storeName, picked string, - promptSecretRef func(string, string, string, string) (string, error), - promptLine func(string, string) (string, error), + promptSecretRef func(context.Context, string, string, string, string) (string, error), + promptLine func(context.Context, string, string) (string, error), out io.Writer, ) (cloudstic.ProfileStore, error) { switch picked { case "Password (recommended for interactive use)": - secretRef, err := promptSecretRef(storeName, "repository password", "CLOUDSTIC_PASSWORD", "password") + secretRef, err := promptSecretRef(ctx, storeName, "repository password", "CLOUDSTIC_PASSWORD", "password") if err != nil { return s, fmt.Errorf("failed to configure password secret: %w", err) } @@ -481,7 +481,7 @@ func configureStoreEncryptionSelection( s.PasswordSecret = secretRef _, _ = fmt.Fprintf(out, "Encryption: password via %s\n", secretRef) case "Platform key (recommended for automation/CI)": - secretRef, err := promptSecretRef(storeName, "platform key (64-char hex)", "CLOUDSTIC_ENCRYPTION_KEY", "encryption-key") + secretRef, err := promptSecretRef(ctx, storeName, "platform key (64-char hex)", "CLOUDSTIC_ENCRYPTION_KEY", "encryption-key") if err != nil { return s, fmt.Errorf("failed to configure platform key secret: %w", err) } @@ -489,12 +489,12 @@ func configureStoreEncryptionSelection( s.EncryptionKeySecret = secretRef _, _ = fmt.Fprintf(out, "Encryption: platform key via %s\n", secretRef) case "AWS KMS key (enterprise)": - arn, err := promptLine("KMS key ARN", "") + arn, err := promptLine(ctx, "KMS key ARN", "") if err != nil || arn == "" { return s, fmt.Errorf("KMS key ARN is required") } s.KMSKeyARN = arn - region, _ := promptLine("KMS region", "us-east-1") + region, _ := promptLine(ctx, "KMS region", "us-east-1") if region != "" { s.KMSRegion = region } @@ -507,46 +507,48 @@ func configureStoreEncryptionSelection( return s, nil } -func (r *runner) promptSecretReference(storeName, secretLabel, defaultEnvName, defaultAccount string) (string, error) { +func (r *runner) promptSecretReference(ctx context.Context, storeName, secretLabel, defaultEnvName, defaultAccount string) (string, error) { return promptSecretReferenceWithFns( + ctx, storeName, secretLabel, defaultEnvName, defaultAccount, - r.promptSelect, - r.promptLine, - r.promptSecret, + func(_ context.Context, l string, o []string) (string, error) { return r.promptSelect(ctx, l, o) }, + func(ctx context.Context, l, d string) (string, error) { return r.promptLine(ctx, l, d) }, + func(_ context.Context, s string) (string, error) { return r.promptSecret(ctx, s) }, os.LookupEnv, profileSecretResolver, ) } func promptSecretReferenceWithFns( + ctx context.Context, storeName, secretLabel, defaultEnvName, defaultAccount string, - promptSelect func(string, []string) (string, error), - promptLine func(string, string) (string, error), - promptSecret func(string) (string, error), + promptSelect func(context.Context, string, []string) (string, error), + promptLine func(context.Context, string, string) (string, error), + promptSecret func(context.Context, string) (string, error), lookupEnv func(string) (string, bool), resolver *secretref.Resolver, ) (string, error) { writableBackends := resolver.WritableBackends() nativeRef := func(backend secretref.WritableBackend) (string, error) { ref := backend.DefaultRef(storeName, defaultAccount) - exists, err := resolver.Exists(context.Background(), ref) + exists, err := resolver.Exists(ctx, ref) if err != nil { return "", err } if exists { return ref, nil } - secretValue, err := promptSecret("Secret value") + secretValue, err := promptSecret(ctx, "Secret value") if err != nil { return "", err } if secretValue == "" { return "", fmt.Errorf("secret value cannot be empty") } - if err := resolver.Store(context.Background(), ref, secretValue); err != nil { + if err := resolver.Store(ctx, ref, secretValue); err != nil { return "", err } return ref, nil @@ -561,6 +563,7 @@ func promptSecretReferenceWithFns( backendByOption[option] = backend } picked, err := promptSelect( + ctx, fmt.Sprintf("Where should %s be stored?", secretLabel), options, ) @@ -572,7 +575,7 @@ func promptSecretReferenceWithFns( } } - envName, err := promptLine("Env var name", defaultEnvName) + envName, err := promptLine(ctx, "Env var name", defaultEnvName) if err != nil { return "", err } @@ -585,6 +588,7 @@ func promptSecretReferenceWithFns( backendByOption[option] = backend } picked, err := promptSelect( + ctx, fmt.Sprintf("Environment variable %q is not set in this shell", envName), options, ) @@ -611,7 +615,7 @@ func verifyStoreEncryptionCredentials(ctx context.Context, g *globalFlags, raw s if err != nil { return fmt.Errorf("build keychain: %w", err) } - _, err = cloudstic.NewClient(raw, + _, err = cloudstic.NewClient(ctx, raw, cloudstic.WithKeychain(kc), cloudstic.WithReporter(ui.NewNoOpReporter()), ) diff --git a/cmd/cloudstic/cmd_store_test.go b/cmd/cloudstic/cmd_store_test.go index ce24742..8f65c94 100644 --- a/cmd/cloudstic/cmd_store_test.go +++ b/cmd/cloudstic/cmd_store_test.go @@ -55,7 +55,7 @@ func TestRunStoreNewAndListAndShow(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store new failed: %s", errOut.String()) } if !strings.Contains(out.String(), `"prod-s3" saved`) { @@ -66,7 +66,7 @@ func TestRunStoreNewAndListAndShow(t *testing.T) { os.Args = []string{"cloudstic", "store", "list", "-profiles-file", profilesPath} out.Reset() errOut.Reset() - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store list failed: %s", errOut.String()) } if !strings.Contains(out.String(), "Stores") || !strings.Contains(out.String(), "prod-s3") || !strings.Contains(out.String(), "AUTH") { @@ -77,7 +77,7 @@ func TestRunStoreNewAndListAndShow(t *testing.T) { os.Args = []string{"cloudstic", "store", "show", "-profiles-file", profilesPath, "prod-s3"} out.Reset() errOut.Reset() - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store show failed: %s", errOut.String()) } got := out.String() @@ -104,7 +104,7 @@ func TestRunStoreNew_RequiresNameAndURI(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code == 0 { + if code := r.runStore(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "-uri is required") { @@ -140,7 +140,7 @@ func TestRunStoreNew_ExistingStorePrefillsUnsetValues(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store new failed: %s", errOut.String()) } @@ -198,7 +198,7 @@ func TestRunStoreShow_UnknownStore(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code == 0 { + if code := r.runStore(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "Unknown store") { @@ -229,7 +229,7 @@ profiles: var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store show failed: %s", errOut.String()) } got := out.String() @@ -243,7 +243,7 @@ func TestRunStoreList_MissingFile(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("expected zero exit code, got err=%s", errOut.String()) } } @@ -264,7 +264,7 @@ func TestRunStoreNew_WithEncryption(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store new failed: %s", errOut.String()) } @@ -272,7 +272,7 @@ func TestRunStoreNew_WithEncryption(t *testing.T) { os.Args = []string{"cloudstic", "store", "show", "-profiles-file", profilesPath, "encrypted-s3"} out.Reset() errOut.Reset() - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store show failed: %s", errOut.String()) } got := out.String() @@ -334,7 +334,7 @@ func TestCheckOrInitStore_AlreadyInitialized(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if err := r.checkOrInitStore(cfg, "test", profilesPath, checkOrInitOptions{warnOnMissingSecrets: true, offerInit: true}); err != nil { + if err := r.checkOrInitStore(context.Background(), cfg, "test", profilesPath, checkOrInitOptions{warnOnMissingSecrets: true, offerInit: true}); err != nil { t.Fatalf("checkOrInitStore: %v", err) } @@ -372,7 +372,7 @@ func TestCheckOrInitStore_InitializedEncrypted_ValidCredentials(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if err := r.checkOrInitStore(cfg, "test", "profiles.yaml", checkOrInitOptions{warnOnMissingSecrets: true, offerInit: true}); err != nil { + if err := r.checkOrInitStore(context.Background(), cfg, "test", "profiles.yaml", checkOrInitOptions{warnOnMissingSecrets: true, offerInit: true}); err != nil { t.Fatalf("checkOrInitStore: %v", err) } if !strings.Contains(out.String(), "Repository is encrypted; verifying configured credentials") { @@ -410,7 +410,7 @@ func TestCheckOrInitStore_InitializedEncrypted_InvalidCredentials(t *testing.T) }} r := &runner{out: &strings.Builder{}, errOut: &strings.Builder{}} - err = r.checkOrInitStore(cfg, "test", "profiles.yaml", checkOrInitOptions{warnOnMissingSecrets: true, offerInit: true}) + err = r.checkOrInitStore(context.Background(), cfg, "test", "profiles.yaml", checkOrInitOptions{warnOnMissingSecrets: true, offerInit: true}) if err == nil { t.Fatal("expected error") } @@ -499,7 +499,7 @@ func TestRunStoreNew_InvalidURI(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code == 0 { + if code := r.runStore(context.Background()); code == 0 { t.Fatal("expected non-zero exit code for invalid URI") } if !strings.Contains(errOut.String(), "invalid store URI") { @@ -515,7 +515,7 @@ func TestRunStoreNew_InvalidName(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code == 0 { + if code := r.runStore(context.Background()); code == 0 { t.Fatal("expected non-zero exit code for invalid name") } if !strings.Contains(errOut.String(), "invalid store name") { @@ -528,7 +528,7 @@ func TestRunStore_UnknownSubcommand(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code == 0 { + if code := r.runStore(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "Unknown store subcommand") { @@ -558,7 +558,7 @@ stores: var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store show failed: %s", errOut.String()) } got := out.String() @@ -600,7 +600,7 @@ stores: var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store show failed: %s", errOut.String()) } got := out.String() @@ -635,7 +635,7 @@ stores: var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store show failed: %s", errOut.String()) } got := out.String() @@ -668,7 +668,7 @@ func TestRunStoreNew_WithSFTPOptions(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store new failed: %s", errOut.String()) } @@ -676,7 +676,7 @@ func TestRunStoreNew_WithSFTPOptions(t *testing.T) { os.Args = []string{"cloudstic", "store", "show", "-profiles-file", profilesPath, "sftp-new"} out.Reset() errOut.Reset() - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store show failed: %s", errOut.String()) } got := out.String() @@ -711,7 +711,7 @@ func TestRunStoreNew_WithAllS3Options(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store new failed: %s", errOut.String()) } @@ -754,7 +754,7 @@ func TestRunStoreNew_WithSecretRefFlags(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store new failed: %s", errOut.String()) } @@ -797,14 +797,14 @@ func TestPromptSecretReferenceWithFns_DarwinKeychain(t *testing.T) { }, }) - gotRef, err := promptSecretReferenceWithFns( + gotRef, err := promptSecretReferenceWithFns(context.Background(), "prod-store", "repository password", "CLOUDSTIC_PASSWORD", "password", - func(_ string, _ []string) (string, error) { return "macOS Keychain (keychain://)", nil }, - func(label, def string) (string, error) { return def, nil }, - func(_ string) (string, error) { return "super-secret", nil }, + func(_ context.Context, _ string, _ []string) (string, error) { return "macOS Keychain (keychain://)", nil }, + func(_ context.Context, label, def string) (string, error) { return def, nil }, + func(_ context.Context, _ string) (string, error) { return "super-secret", nil }, func(string) (string, bool) { return "", false }, resolver, ) @@ -818,19 +818,19 @@ func TestPromptSecretReferenceWithFns_DarwinKeychain(t *testing.T) { func TestPromptSecretReferenceWithFns_EnvFallback(t *testing.T) { resolver := secretref.NewResolver(nil) - gotRef, err := promptSecretReferenceWithFns( + gotRef, err := promptSecretReferenceWithFns(context.Background(), "prod-store", "repository password", "CLOUDSTIC_PASSWORD", "password", - func(_ string, _ []string) (string, error) { return "Environment variable (env://)", nil }, - func(label, def string) (string, error) { + func(_ context.Context, _ string, _ []string) (string, error) { return "Environment variable (env://)", nil }, + func(_ context.Context, label, def string) (string, error) { if label != "Env var name" { t.Fatalf("unexpected label: %s", label) } return def, nil }, - func(_ string) (string, error) { + func(_ context.Context, _ string) (string, error) { t.Fatal("promptSecret should not be called") return "", nil }, @@ -855,14 +855,14 @@ func TestPromptSecretReferenceWithFns_KeychainWriteError(t *testing.T) { store: func(context.Context, secretref.Ref, string) error { return errors.New("write failed") }, }, }) - _, err := promptSecretReferenceWithFns( + _, err := promptSecretReferenceWithFns(context.Background(), "prod-store", "repository password", "CLOUDSTIC_PASSWORD", "password", - func(_ string, _ []string) (string, error) { return "macOS Keychain (keychain://)", nil }, - func(_ string, def string) (string, error) { return def, nil }, - func(_ string) (string, error) { return "secret", nil }, + func(_ context.Context, _ string, _ []string) (string, error) { return "macOS Keychain (keychain://)", nil }, + func(_ context.Context, _ string, def string) (string, error) { return def, nil }, + func(_ context.Context, _ string) (string, error) { return "secret", nil }, func(string) (string, bool) { return "", false }, resolver, ) @@ -878,14 +878,14 @@ func TestPromptSecretReferenceWithFns_EmptySecret(t *testing.T) { resolver := secretref.NewResolver(map[string]secretref.Backend{ "keychain": writableBackendStub{scheme: "keychain", displayName: "macOS Keychain", defaultRef: "keychain://cloudstic/store/prod-store/password"}, }) - _, err := promptSecretReferenceWithFns( + _, err := promptSecretReferenceWithFns(context.Background(), "prod-store", "repository password", "CLOUDSTIC_PASSWORD", "password", - func(_ string, _ []string) (string, error) { return "macOS Keychain (keychain://)", nil }, - func(_ string, def string) (string, error) { return def, nil }, - func(_ string) (string, error) { return "", nil }, + func(_ context.Context, _ string, _ []string) (string, error) { return "macOS Keychain (keychain://)", nil }, + func(_ context.Context, _ string, def string) (string, error) { return def, nil }, + func(_ context.Context, _ string) (string, error) { return "", nil }, func(string) (string, bool) { return "", false }, resolver, ) @@ -915,14 +915,14 @@ func TestPromptSecretReferenceWithFns_DarwinKeychainAdoptsExisting(t *testing.T) }, }, }) - gotRef, err := promptSecretReferenceWithFns( + gotRef, err := promptSecretReferenceWithFns(context.Background(), "prod-store", "repository password", "CLOUDSTIC_PASSWORD", "password", - func(_ string, _ []string) (string, error) { return "macOS Keychain (keychain://)", nil }, - func(_ string, def string) (string, error) { return def, nil }, - func(_ string) (string, error) { + func(_ context.Context, _ string, _ []string) (string, error) { return "macOS Keychain (keychain://)", nil }, + func(_ context.Context, _ string, def string) (string, error) { return def, nil }, + func(_ context.Context, _ string) (string, error) { t.Fatal("promptSecret should not be called when key exists") return "", nil }, @@ -948,25 +948,25 @@ func TestPromptSecretReferenceWithFns_DarwinEnvUnsetSwitchesToKeychain(t *testin store: func(context.Context, secretref.Ref, string) error { return nil }, }, }) - gotRef, err := promptSecretReferenceWithFns( + gotRef, err := promptSecretReferenceWithFns(context.Background(), "prod-store", "repository password", "CLOUDSTIC_PASSWORD", "password", - func(_ string, _ []string) (string, error) { + func(_ context.Context, _ string, _ []string) (string, error) { selectCall++ if selectCall == 1 { return "Environment variable (env://)", nil } return "Store in macOS Keychain instead (keychain://)", nil }, - func(label, def string) (string, error) { + func(_ context.Context, label, def string) (string, error) { if label != "Env var name" { t.Fatalf("unexpected prompt line label: %s", label) } return "UNSET_PASSWORD", nil }, - func(_ string) (string, error) { return "secret-value", nil }, + func(_ context.Context, _ string) (string, error) { return "secret-value", nil }, func(string) (string, bool) { return "", false }, resolver, ) @@ -989,7 +989,7 @@ func TestCheckOrInitStore_MissingSecretAllowed(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if err := r.checkOrInitStore(cfg, "test", "profiles.yaml", checkOrInitOptions{allowMissingSecrets: true, warnOnMissingSecrets: true, offerInit: true}); err != nil { + if err := r.checkOrInitStore(context.Background(), cfg, "test", "profiles.yaml", checkOrInitOptions{allowMissingSecrets: true, warnOnMissingSecrets: true, offerInit: true}); err != nil { t.Fatalf("checkOrInitStore: %v", err) } if !strings.Contains(errOut.String(), "cloudstic store verify test") { @@ -1008,7 +1008,7 @@ func TestCheckOrInitStore_MissingSecretAllowedSilent(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if err := r.checkOrInitStore(cfg, "test", "profiles.yaml", checkOrInitOptions{allowMissingSecrets: true, offerInit: true}); err != nil { + if err := r.checkOrInitStore(context.Background(), cfg, "test", "profiles.yaml", checkOrInitOptions{allowMissingSecrets: true, offerInit: true}); err != nil { t.Fatalf("checkOrInitStore: %v", err) } if errOut.String() != "" { @@ -1033,7 +1033,7 @@ func TestRunStoreVerify_MissingSecretFails(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code == 0 { + if code := r.runStore(context.Background()); code == 0 { t.Fatal("expected non-zero exit code") } if !strings.Contains(errOut.String(), "could not resolve store credentials") { @@ -1092,12 +1092,12 @@ func TestExistingStoreInteractivePlan(t *testing.T) { func TestConfigureStoreEncryptionSelection_Password(t *testing.T) { var out strings.Builder - s, err := configureStoreEncryptionSelection( + s, err := configureStoreEncryptionSelection(context.Background(), cloudstic.ProfileStore{}, "prod", "Password (recommended for interactive use)", - func(string, string, string, string) (string, error) { return "env://MY_BACKUP_PASSWORD", nil }, - func(string, string) (string, error) { return "", nil }, + func(context.Context, string, string, string, string) (string, error) { return "env://MY_BACKUP_PASSWORD", nil }, + func(context.Context, string, string) (string, error) { return "", nil }, &out, ) if err != nil { @@ -1113,12 +1113,12 @@ func TestConfigureStoreEncryptionSelection_Password(t *testing.T) { func TestConfigureStoreEncryptionSelection_KMS(t *testing.T) { var out strings.Builder - s, err := configureStoreEncryptionSelection( + s, err := configureStoreEncryptionSelection(context.Background(), cloudstic.ProfileStore{}, "prod", "AWS KMS key (enterprise)", - func(string, string, string, string) (string, error) { return "", nil }, - func(label, def string) (string, error) { + func(context.Context, string, string, string, string) (string, error) { return "", nil }, + func(ctx context.Context, label, def string) (string, error) { switch label { case "KMS key ARN": return "arn:aws:kms:us-east-1:123:key/abc", nil @@ -1140,12 +1140,12 @@ func TestConfigureStoreEncryptionSelection_KMS(t *testing.T) { func TestConfigureStoreEncryptionSelection_NoEncryption(t *testing.T) { var out strings.Builder - _, err := configureStoreEncryptionSelection( + _, err := configureStoreEncryptionSelection(context.Background(), cloudstic.ProfileStore{}, "prod", "No encryption (not recommended)", - func(string, string, string, string) (string, error) { return "", nil }, - func(string, string) (string, error) { return "", nil }, + func(context.Context, string, string, string, string) (string, error) { return "", nil }, + func(context.Context, string, string) (string, error) { return "", nil }, &out, ) if err != nil { @@ -1157,12 +1157,12 @@ func TestConfigureStoreEncryptionSelection_NoEncryption(t *testing.T) { } func TestConfigureStoreEncryptionSelection_KMSError(t *testing.T) { - _, err := configureStoreEncryptionSelection( + _, err := configureStoreEncryptionSelection(context.Background(), cloudstic.ProfileStore{}, "prod", "AWS KMS key (enterprise)", - func(string, string, string, string) (string, error) { return "", nil }, - func(label, def string) (string, error) { + func(context.Context, string, string, string, string) (string, error) { return "", nil }, + func(ctx context.Context, label, def string) (string, error) { if label == "KMS key ARN" { return "", nil } @@ -1189,7 +1189,7 @@ func TestPromptSecretReference_EnvInteractive(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - got, err := r.promptSecretReference("prod", "repository password", "CLOUDSTIC_PASSWORD", "password") + got, err := r.promptSecretReference(context.Background(), "prod", "repository password", "CLOUDSTIC_PASSWORD", "password") if err != nil { t.Fatalf("promptSecretReference: %v", err) } @@ -1218,7 +1218,7 @@ func TestPromptEncryptionConfig_PasswordViaEnvRef(t *testing.T) { var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - r.promptEncryptionConfig(cfg, "prod", profilesPath) + r.promptEncryptionConfig(context.Background(), cfg, "prod", profilesPath) s := cfg.Stores["prod"] if s.PasswordSecret != "env://MY_BACKUP_PASSWORD" { @@ -1287,7 +1287,7 @@ stores: var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store list failed: %s", errOut.String()) } got := out.String() @@ -1314,7 +1314,7 @@ func TestRunStoreNew_LocalStore(t *testing.T) { var out strings.Builder var errOut strings.Builder r := &runner{out: &out, errOut: &errOut} - if code := r.runStore(); code != 0 { + if code := r.runStore(context.Background()); code != 0 { t.Fatalf("store new failed: %s", errOut.String()) } if !strings.Contains(out.String(), `"local-bk" saved`) { diff --git a/cmd/cloudstic/interactive.go b/cmd/cloudstic/interactive.go index fd0552f..431e45d 100644 --- a/cmd/cloudstic/interactive.go +++ b/cmd/cloudstic/interactive.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" "strconv" @@ -18,7 +19,7 @@ func (r *runner) canPrompt() bool { return !r.noPrompt && term.IsTerminal(stdin.Fd()) && term.IsTerminal(os.Stdout.Fd()) } -func (r *runner) promptLine(label, defaultValue string) (string, error) { +func (r *runner) promptLine(ctx context.Context, label, defaultValue string) (string, error) { if defaultValue != "" { _, _ = fmt.Fprintf(r.errOut, "%s [%s]: ", label, defaultValue) } else { @@ -35,14 +36,14 @@ func (r *runner) promptLine(label, defaultValue string) (string, error) { return line, nil } -func (r *runner) promptConfirm(label string, defaultYes bool) (bool, error) { +func (r *runner) promptConfirm(ctx context.Context, label string, defaultYes bool) (bool, error) { hint := "y/N" dflt := "n" if defaultYes { hint = "Y/n" dflt = "y" } - answer, err := r.promptLine(fmt.Sprintf("%s [%s]", label, hint), dflt) + answer, err := r.promptLine(ctx, fmt.Sprintf("%s [%s]", label, hint), dflt) if err != nil { return false, err } @@ -50,7 +51,7 @@ func (r *runner) promptConfirm(label string, defaultYes bool) (bool, error) { return answer == "y" || answer == "yes", nil } -func (r *runner) promptSelect(label string, options []string) (string, error) { +func (r *runner) promptSelect(ctx context.Context, label string, options []string) (string, error) { if len(options) == 0 { return "", fmt.Errorf("no options available") } @@ -59,7 +60,7 @@ func (r *runner) promptSelect(label string, options []string) (string, error) { _, _ = fmt.Fprintf(r.errOut, " %d) %s\n", i+1, opt) } for { - choice, err := r.promptLine("Select option number", "1") + choice, err := r.promptLine(ctx, "Select option number", "1") if err != nil { return "", err } @@ -72,7 +73,7 @@ func (r *runner) promptSelect(label string, options []string) (string, error) { } } -func (r *runner) promptSecret(label string) (string, error) { +func (r *runner) promptSecret(ctx context.Context, label string) (string, error) { _, _ = fmt.Fprintf(r.errOut, "%s: ", label) stdin := r.stdin if stdin == nil { diff --git a/cmd/cloudstic/main.go b/cmd/cloudstic/main.go index 1508552..68b5297 100644 --- a/cmd/cloudstic/main.go +++ b/cmd/cloudstic/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" ) @@ -35,40 +36,41 @@ func main() { func runCmd(cmd string) int { r := newRunner() + ctx := context.Background() switch cmd { case "version", "--version", "-v": fmt.Printf("cloudstic %s (commit %s, built %s)\n", version, commit, date) return 0 case "init": - return r.runInit() + return r.runInit(ctx) case "backup": - return r.runBackup() + return r.runBackup(ctx) case "restore": - return r.runRestore() + return r.runRestore(ctx) case "list": - return r.runList() + return r.runList(ctx) case "ls": - return r.runLsSnapshot() + return r.runLsSnapshot(ctx) case "prune": - return r.runPrune() + return r.runPrune(ctx) case "forget": - return r.runForget() + return r.runForget(ctx) case "diff": - return r.runDiff() + return r.runDiff(ctx) case "break-lock": - return r.runBreakLock() + return r.runBreakLock(ctx) case "key": - return r.runKey() + return r.runKey(ctx) case "check": - return r.runCheck() + return r.runCheck(ctx) case "cat": - return r.runCat() + return r.runCat(ctx) case "profile": - return r.runProfile() + return r.runProfile(ctx) case "auth": - return r.runAuth() + return r.runAuth(ctx) case "store": - return r.runStore() + return r.runStore(ctx) case "completion": runCompletion() return 0 diff --git a/cmd/cloudstic/runner.go b/cmd/cloudstic/runner.go index d014716..df1a12f 100644 --- a/cmd/cloudstic/runner.go +++ b/cmd/cloudstic/runner.go @@ -2,6 +2,7 @@ package main import ( "bufio" + "context" "fmt" "io" "os" @@ -55,11 +56,11 @@ func (r *runner) fail(format string, args ...any) int { // openClient opens the cloudstic client from the given global flags. // No-op if r.client is already set (e.g. injected for tests). -func (r *runner) openClient(g *globalFlags) error { +func (r *runner) openClient(ctx context.Context, g *globalFlags) error { if r.client != nil { return nil } - client, err := g.openClient() + client, err := g.openClient(ctx) if err != nil { return err } diff --git a/cmd/cloudstic/store.go b/cmd/cloudstic/store.go index 5eea123..91d7db5 100644 --- a/cmd/cloudstic/store.go +++ b/cmd/cloudstic/store.go @@ -44,7 +44,7 @@ func (g *globalFlags) applyDebug(s store.ObjectStore) store.ObjectStore { return store.NewDebugStore(s, g.debugLog) } -func (g *globalFlags) openClient() (*cloudstic.Client, error) { +func (g *globalFlags) openClient(ctx context.Context) (*cloudstic.Client, error) { if err := g.applyProfileStoreOverrides(); err != nil { return nil, err } @@ -67,12 +67,12 @@ func (g *globalFlags) openClient() (*cloudstic.Client, error) { reporter = cr } - kc, err := g.buildKeychain(context.Background()) + kc, err := g.buildKeychain(ctx) if err != nil { return nil, err } - return cloudstic.NewClient(raw, + return cloudstic.NewClient(ctx, raw, cloudstic.WithKeychain(kc), cloudstic.WithReporter(reporter), cloudstic.WithPackfile(packfileEnabled), diff --git a/internal/engine/backup.go b/internal/engine/backup.go index 953be94..6fc5fa0 100644 --- a/internal/engine/backup.go +++ b/internal/engine/backup.go @@ -216,7 +216,7 @@ func (bm *BackupManager) Run(ctx context.Context) (*RunResult, error) { if bm.cfg.dryRun { if usedFullScan { - if err := bm.countRemoved(oldRoot, newRoot); err != nil { + if err := bm.countRemoved(ctx, oldRoot, newRoot); err != nil { return nil, fmt.Errorf("counting removed entries: %w", err) } } @@ -241,7 +241,7 @@ func (bm *BackupManager) Run(ctx context.Context) (*RunResult, error) { } if usedFullScan { - if err := bm.countRemoved(oldRoot, newRoot); err != nil { + if err := bm.countRemoved(ctx, oldRoot, newRoot); err != nil { return nil, fmt.Errorf("counting removed entries: %w", err) } } @@ -375,7 +375,7 @@ func (bm *BackupManager) trackFileMeta(ref string, fm core.FileMeta) { bm.newMetas[ref] = fm } -func (bm *BackupManager) loadMeta(ref string) (*core.FileMeta, error) { +func (bm *BackupManager) loadMeta(ctx context.Context, ref string) (*core.FileMeta, error) { bm.metaCacheMu.RLock() fm, ok := bm.metaCache[ref] bm.metaCacheMu.RUnlock() @@ -383,7 +383,7 @@ func (bm *BackupManager) loadMeta(ref string) (*core.FileMeta, error) { return &fm, nil } - data, err := bm.store.Get(context.Background(), ref) + data, err := bm.store.Get(ctx, ref) if err != nil { return nil, err } diff --git a/internal/engine/backup_scan.go b/internal/engine/backup_scan.go index 6a9beb2..a07f580 100644 --- a/internal/engine/backup_scan.go +++ b/internal/engine/backup_scan.go @@ -60,10 +60,10 @@ func (bm *BackupManager) processEntry(ctx context.Context, meta *core.FileMeta, // Resolve Paths when the source hasn't populated it (incremental/changes // sources only emit changed entries and can't build a full path map). if len(meta.Paths) == 0 { - meta.Paths = []string{bm.buildPathFromTree(s.root, meta)} + meta.Paths = []string{bm.buildPathFromTree(ctx, s.root, meta)} } - changed, oldRef, err := bm.detectChange(oldRoot, meta) + changed, oldRef, err := bm.detectChange(ctx, oldRoot, meta) if err != nil { return err } @@ -121,7 +121,7 @@ func (bm *BackupManager) scanIncremental(ctx context.Context, oldRoot string, in bm.recordRemoved(fc.Meta.Type) deleteParentID := primaryParentID(&fc.Meta) if deleteParentID == "" { - deleteParentID, err = bm.lookupDeleteParentID(s.root, fc.Meta.FileID) + deleteParentID, err = bm.lookupDeleteParentID(ctx, s.root, fc.Meta.FileID) if err != nil { return err } @@ -145,7 +145,7 @@ func (bm *BackupManager) scanIncremental(ctx context.Context, oldRoot string, in return s.root, s.pending, s.totalBytes, newToken, nil } -func (bm *BackupManager) lookupDeleteParentID(root, fileID string) (string, error) { +func (bm *BackupManager) lookupDeleteParentID(ctx context.Context, root, fileID string) (string, error) { if root == "" { return "", nil } @@ -158,7 +158,7 @@ func (bm *BackupManager) lookupDeleteParentID(root, fileID string) (string, erro return "", nil } - oldMeta, err := bm.loadMeta(ref) + oldMeta, err := bm.loadMeta(ctx, ref) if err != nil { return "", fmt.Errorf("load old file metadata for delete %s: %w", fileID, err) } @@ -171,7 +171,7 @@ func (bm *BackupManager) lookupDeleteParentID(root, fileID string) (string, erro // For sources that do not provide a content hash (e.g. Google Drive), a // fast-path compares observable metadata and carries the hash forward to avoid // false-positive diffs. -func (bm *BackupManager) detectChange(oldRoot string, meta *core.FileMeta) (changed bool, oldRef string, err error) { +func (bm *BackupManager) detectChange(ctx context.Context, oldRoot string, meta *core.FileMeta) (changed bool, oldRef string, err error) { oldRef, err = bm.tree.Lookup(oldRoot, primaryParentID(meta), meta.FileID) if err != nil { return false, "", fmt.Errorf("hamt lookup: %w", err) @@ -180,7 +180,7 @@ func (bm *BackupManager) detectChange(oldRoot string, meta *core.FileMeta) (chan return true, "", nil } - oldMeta, err := bm.loadMeta(oldRef) + oldMeta, err := bm.loadMeta(ctx, oldRef) if err != nil { return false, "", err } @@ -352,12 +352,12 @@ func (bm *BackupManager) recordRemoved(ft core.FileType) { // buildPathFromTree reconstructs the full path for a FileMeta entry by walking // the parent chain in the HAMT tree. This is used for incremental/changes // sources that can't build a path map (the parent may not be in the change set). -func (bm *BackupManager) buildPathFromTree(root string, meta *core.FileMeta) string { +func (bm *BackupManager) buildPathFromTree(ctx context.Context, root string, meta *core.FileMeta) string { const maxDepth = 50 parts := []string{meta.Name} curParents := meta.Parents for i := 0; i < maxDepth && len(curParents) > 0; i++ { - parent := bm.lookupMetaByFileID(root, curParents[0]) + parent := bm.lookupMetaByFileID(ctx, root, curParents[0]) if parent == nil { break } @@ -378,7 +378,7 @@ func (bm *BackupManager) buildPathFromTree(root string, meta *core.FileMeta) str // It checks newMetas (just inserted this scan) first, then falls back to the store. // Uses parentIndex to resolve the AffinityKey; falls back to a full-tree walk // for entries not yet seen in this scan (e.g. incremental backups). -func (bm *BackupManager) lookupMetaByFileID(root, fileID string) *core.FileMeta { +func (bm *BackupManager) lookupMetaByFileID(ctx context.Context, root, fileID string) *core.FileMeta { parentID := bm.parentIndex[fileID] ref, err := bm.tree.Lookup(root, parentID, fileID) if err != nil || ref == "" { @@ -392,7 +392,7 @@ func (bm *BackupManager) lookupMetaByFileID(root, fileID string) *core.FileMeta if fm, ok := bm.newMetas[ref]; ok { return &fm } - fm, err := bm.loadMeta(ref) + fm, err := bm.loadMeta(ctx, ref) if err != nil { return nil } @@ -401,13 +401,13 @@ func (bm *BackupManager) lookupMetaByFileID(root, fileID string) *core.FileMeta // countRemoved uses a structural HAMT diff to count entries present in oldRoot // but absent from newRoot (full-scan path where deletions are implicit). -func (bm *BackupManager) countRemoved(oldRoot, newRoot string) error { +func (bm *BackupManager) countRemoved(ctx context.Context, oldRoot, newRoot string) error { if oldRoot == "" { return nil } return bm.tree.Diff(oldRoot, newRoot, func(d hamt.DiffEntry) error { if d.OldValue != "" && d.NewValue == "" { - meta, err := bm.loadMeta(d.OldValue) + meta, err := bm.loadMeta(ctx, d.OldValue) if err != nil { return err } diff --git a/internal/engine/backup_upload.go b/internal/engine/backup_upload.go index 9e1f73e..317a799 100644 --- a/internal/engine/backup_upload.go +++ b/internal/engine/backup_upload.go @@ -168,14 +168,14 @@ func (bm *BackupManager) uploadContent(ctx context.Context, meta core.FileMeta, return bm.uploadInline(ctx, rc, meta, phase) } - chunkRefs, size, hash, err := bm.chunker.ProcessStream(rc, func(n int64) { + chunkRefs, size, hash, err := bm.chunker.ProcessStream(ctx, rc, func(n int64) { phase.Increment(n) }) if err != nil { return "", 0, "", nil, fmt.Errorf("chunking %s: %w", meta.Name, err) } - contentRef, err = bm.chunker.CreateContentObject(chunkRefs, size, hash) + contentRef, err = bm.chunker.CreateContentObject(ctx, chunkRefs, size, hash) if err != nil { return "", 0, "", nil, fmt.Errorf("create content for %s: %w", meta.Name, err) } diff --git a/internal/engine/chunker.go b/internal/engine/chunker.go index ccfc190..280cf81 100644 --- a/internal/engine/chunker.go +++ b/internal/engine/chunker.go @@ -43,7 +43,7 @@ func NewChunker(s store.ObjectStore, hmacKey []byte) *Chunker { // and the SHA-256 content hash over the raw stream. // // onProgress is called after each chunk with the number of raw bytes consumed. -func (c *Chunker) ProcessStream(r io.Reader, onProgress func(int64)) (refs []string, size int64, hash string, err error) { +func (c *Chunker) ProcessStream(ctx context.Context, r io.Reader, onProgress func(int64)) (refs []string, size int64, hash string, err error) { cdcMu.Lock() cdc, err := fastcdc.NewChunker(r, fastcdc.Options{ MinSize: cdcMinSize, @@ -55,7 +55,6 @@ func (c *Chunker) ProcessStream(r io.Reader, onProgress func(int64)) (refs []str return nil, 0, "", err } - ctx := context.Background() hasher := sha256.New() type chunkJob struct { @@ -145,7 +144,7 @@ func (c *Chunker) ProcessStream(r io.Reader, onProgress func(int64)) (refs []str // CreateContentObject persists a Content object. The object is keyed by an HMAC // of the contentHash (if encryption is enabled) to prevent hash leakage, or the // plain contentHash otherwise. Returns the secure contentRef (the hex hash used). -func (c *Chunker) CreateContentObject(chunkRefs []string, size int64, contentHash string) (string, error) { +func (c *Chunker) CreateContentObject(ctx context.Context, chunkRefs []string, size int64, contentHash string) (string, error) { content := core.Content{ Type: core.ObjectTypeContent, Size: size, @@ -165,7 +164,7 @@ func (c *Chunker) CreateContentObject(chunkRefs []string, size int64, contentHas } ref := "content/" + contentRef - if err := c.store.Put(context.Background(), ref, data); err != nil { + if err := c.store.Put(ctx, ref, data); err != nil { return "", err } return contentRef, nil diff --git a/internal/engine/chunker_test.go b/internal/engine/chunker_test.go index 70a2a1c..36b36ed 100644 --- a/internal/engine/chunker_test.go +++ b/internal/engine/chunker_test.go @@ -14,7 +14,7 @@ func TestChunker_ProcessStream(t *testing.T) { data := "1234567890123456789012345" reader := strings.NewReader(data) - refs, size, _, err := chunker.ProcessStream(reader, nil) + refs, size, _, err := chunker.ProcessStream(ctx, reader, nil) if err != nil { t.Fatalf("ProcessStream failed: %v", err) } @@ -35,13 +35,14 @@ func TestChunker_ProcessStream(t *testing.T) { } func TestChunker_Deduplication(t *testing.T) { + ctx := context.Background() store := NewMockStore() chunker := NewChunker(store, nil) data := "1234567890" - refs1, _, _, _ := chunker.ProcessStream(strings.NewReader(data), nil) - refs2, _, _, _ := chunker.ProcessStream(strings.NewReader(data), nil) + refs1, _, _, _ := chunker.ProcessStream(ctx, strings.NewReader(data), nil) + refs2, _, _, _ := chunker.ProcessStream(ctx, strings.NewReader(data), nil) if len(refs1) == 0 || len(refs2) == 0 { t.Fatal("No chunks produced") @@ -61,7 +62,7 @@ func TestChunker_CreateContentObject(t *testing.T) { size := int64(100) contentHash := "test-hash" - ref, err := chunker.CreateContentObject(chunks, size, contentHash) + ref, err := chunker.CreateContentObject(ctx, chunks, size, contentHash) if err != nil { t.Fatalf("CreateContentObject failed: %v", err) } @@ -86,7 +87,7 @@ func TestChunker_HMAC_CreateContentObject(t *testing.T) { chunks := []string{"chunk/1", "chunk/2"} contentHash := "plain-content-hash" - contentRef, err := chunker.CreateContentObject(chunks, 100, contentHash) + contentRef, err := chunker.CreateContentObject(ctx, chunks, 100, contentHash) if err != nil { t.Fatalf("CreateContentObject failed: %v", err) } @@ -114,6 +115,7 @@ func TestChunker_HMAC_CreateContentObject(t *testing.T) { // TestChunker_HMAC_ChunkRefsUseHMAC verifies HMAC key produces different chunk refs but identical content hash. func TestChunker_HMAC_ChunkRefsUseHMAC(t *testing.T) { + ctx := context.Background() hmacKey := []byte("test-hmac-key-32-bytes-long!!!!!") data := "deterministic test payload" @@ -121,7 +123,7 @@ func TestChunker_HMAC_ChunkRefsUseHMAC(t *testing.T) { // Without HMAC key plainStore := NewMockStore() plainChunker := NewChunker(plainStore, nil) - plainRefs, _, plainHash, err := plainChunker.ProcessStream(strings.NewReader(data), nil) + plainRefs, _, plainHash, err := plainChunker.ProcessStream(ctx, strings.NewReader(data), nil) if err != nil { t.Fatalf("plain ProcessStream failed: %v", err) } @@ -129,7 +131,7 @@ func TestChunker_HMAC_ChunkRefsUseHMAC(t *testing.T) { // With HMAC key hmacStore := NewMockStore() hmacChunker := NewChunker(hmacStore, hmacKey) - hmacRefs, _, hmacHash, err := hmacChunker.ProcessStream(strings.NewReader(data), nil) + hmacRefs, _, hmacHash, err := hmacChunker.ProcessStream(ctx, strings.NewReader(data), nil) if err != nil { t.Fatalf("hmac ProcessStream failed: %v", err) } @@ -150,15 +152,16 @@ func TestChunker_HMAC_ChunkRefsUseHMAC(t *testing.T) { // TestChunker_HMAC_ContentHashStable verifies content hash is deterministic and independent of HMAC key. func TestChunker_HMAC_ContentHashStable(t *testing.T) { + ctx := context.Background() hmacKey := []byte("test-hmac-key-32-bytes-long!!!!!") data := "stable content hash test" // Run ProcessStream twice with the same HMAC key s1 := NewMockStore() - _, _, hash1, _ := NewChunker(s1, hmacKey).ProcessStream(strings.NewReader(data), nil) + _, _, hash1, _ := NewChunker(s1, hmacKey).ProcessStream(ctx, strings.NewReader(data), nil) s2 := NewMockStore() - _, _, hash2, _ := NewChunker(s2, hmacKey).ProcessStream(strings.NewReader(data), nil) + _, _, hash2, _ := NewChunker(s2, hmacKey).ProcessStream(ctx, strings.NewReader(data), nil) if hash1 != hash2 { t.Errorf("content hash not stable across runs: %s vs %s", hash1, hash2) @@ -166,7 +169,7 @@ func TestChunker_HMAC_ContentHashStable(t *testing.T) { // Run without HMAC key — content hash must still match s3 := NewMockStore() - _, _, hash3, _ := NewChunker(s3, nil).ProcessStream(strings.NewReader(data), nil) + _, _, hash3, _ := NewChunker(s3, nil).ProcessStream(ctx, strings.NewReader(data), nil) if hash1 != hash3 { t.Errorf("content hash must be identical with/without HMAC key: %s vs %s", hash1, hash3) @@ -175,14 +178,15 @@ func TestChunker_HMAC_ContentHashStable(t *testing.T) { // TestChunker_HMAC_Deduplication verifies that dedup works correctly with an HMAC key. func TestChunker_HMAC_Deduplication(t *testing.T) { + ctx := context.Background() hmacKey := []byte("test-hmac-key-32-bytes-long!!!!!") store := NewMockStore() chunker := NewChunker(store, hmacKey) data := "dedup with hmac test" - refs1, _, _, _ := chunker.ProcessStream(strings.NewReader(data), nil) - refs2, _, _, _ := chunker.ProcessStream(strings.NewReader(data), nil) + refs1, _, _, _ := chunker.ProcessStream(ctx, strings.NewReader(data), nil) + refs2, _, _, _ := chunker.ProcessStream(ctx, strings.NewReader(data), nil) if len(refs1) == 0 || len(refs2) == 0 { t.Fatal("no chunks produced") diff --git a/internal/engine/init.go b/internal/engine/init.go index 9bd7abf..37d43d6 100644 --- a/internal/engine/init.go +++ b/internal/engine/init.go @@ -110,7 +110,7 @@ func (m *InitManager) Run(ctx context.Context, opts ...InitOption) (*InitResult, // setupEncryption creates new key slots or adopts existing ones. Returns true // if existing slots were adopted. func (m *InitManager) setupEncryption(ctx context.Context, cfg initConfig) (adopted bool, err error) { - slots, err := keychain.LoadKeySlots(m.store) + slots, err := keychain.LoadKeySlots(ctx, m.store) if err != nil { return false, fmt.Errorf("load key slots: %w", err) } @@ -140,7 +140,7 @@ func (m *InitManager) setupEncryption(ctx context.Context, cfg initConfig) (adop return false, fmt.Errorf("wrap master key: %w", err) } for _, slot := range newSlots { - if err := keychain.WriteKeySlot(m.store, slot); err != nil { + if err := keychain.WriteKeySlot(ctx, m.store, slot); err != nil { return false, fmt.Errorf("write key slot: %w", err) } } @@ -150,7 +150,7 @@ func (m *InitManager) setupEncryption(ctx context.Context, cfg initConfig) (adop // addRecoverySlot extracts the master key and creates a recovery slot. func (m *InitManager) addRecoverySlot(ctx context.Context, cfg initConfig) (string, error) { - slots, err := keychain.LoadKeySlots(m.store) + slots, err := keychain.LoadKeySlots(ctx, m.store) if err != nil { return "", fmt.Errorf("reload key slots: %w", err) } @@ -158,7 +158,7 @@ func (m *InitManager) addRecoverySlot(ctx context.Context, cfg initConfig) (stri if err != nil { return "", fmt.Errorf("extract master key for recovery slot: %w", err) } - mnemonic, err := keychain.AddRecoverySlot(m.store, masterKey) + mnemonic, err := keychain.AddRecoverySlot(ctx, m.store, masterKey) if err != nil { return "", fmt.Errorf("create recovery key: %w", err) } diff --git a/internal/engine/init_test.go b/internal/engine/init_test.go index 7b6bc73..4310514 100644 --- a/internal/engine/init_test.go +++ b/internal/engine/init_test.go @@ -80,7 +80,7 @@ func TestInitManager_EncryptedWithPassword(t *testing.T) { } // Verify key slots were created. - slots, err := keychain.LoadKeySlots(s) + slots, err := keychain.LoadKeySlots(context.Background(), s) if err != nil { t.Fatalf("failed to load key slots: %v", err) } @@ -120,7 +120,7 @@ func TestInitManager_EncryptedWithPlatformKey(t *testing.T) { t.Error("expected encrypted repo") } - slots, err := keychain.LoadKeySlots(s) + slots, err := keychain.LoadKeySlots(context.Background(), s) if err != nil { t.Fatalf("failed to load key slots: %v", err) } @@ -155,7 +155,7 @@ func TestInitManager_AdoptsExistingSlots(t *testing.T) { } masterKey, _ := crypto.GenerateKey() slot, _ := keychain.CreatePlatformSlot(masterKey, platformKey) - _ = keychain.WriteKeySlot(s, slot) + _ = keychain.WriteKeySlot(context.Background(), s, slot) mgr := NewInitManager(s) result, err := mgr.Run(context.Background(), WithInitCredentials(keychain.Chain{keychain.WithPlatformKey(platformKey)})) @@ -177,7 +177,7 @@ func TestInitManager_AdoptExistingSlots_WrongCredential(t *testing.T) { } masterKey, _ := crypto.GenerateKey() slot, _ := keychain.CreatePlatformSlot(masterKey, originalKey) - _ = keychain.WriteKeySlot(s, slot) + _ = keychain.WriteKeySlot(context.Background(), s, slot) // Try to init with a different platform key. wrongKey := make([]byte, 32) @@ -220,7 +220,7 @@ func TestInitManager_WithRecoveryKey(t *testing.T) { } // Verify recovery slot was created. - slots, err := keychain.LoadKeySlots(s) + slots, err := keychain.LoadKeySlots(context.Background(), s) if err != nil { t.Fatalf("failed to load key slots: %v", err) } @@ -254,7 +254,7 @@ func TestInitManager_EncryptedWithKMS(t *testing.T) { } // Verify kms-platform slot was created. - slots, err := keychain.LoadKeySlots(s) + slots, err := keychain.LoadKeySlots(context.Background(), s) if err != nil { t.Fatalf("failed to load key slots: %v", err) } @@ -306,7 +306,7 @@ func TestInitManager_KMSWithPasswordSlots(t *testing.T) { t.Error("expected encrypted repo") } - slots, err := keychain.LoadKeySlots(s) + slots, err := keychain.LoadKeySlots(context.Background(), s) if err != nil { t.Fatalf("failed to load key slots: %v", err) } @@ -352,7 +352,7 @@ func TestInitManager_KMSWithRecovery(t *testing.T) { t.Error("expected recovery key mnemonic") } - slots, err := keychain.LoadKeySlots(s) + slots, err := keychain.LoadKeySlots(context.Background(), s) if err != nil { t.Fatalf("failed to load key slots: %v", err) } @@ -384,7 +384,7 @@ func TestInitManager_NoEncryptionOverridesCreds(t *testing.T) { } // No key slots should exist. - slots, err := keychain.LoadKeySlots(s) + slots, err := keychain.LoadKeySlots(context.Background(), s) if err != nil { t.Fatalf("failed to load key slots: %v", err) } @@ -401,7 +401,7 @@ func TestInitManager_AdoptAddsNewSlots(t *testing.T) { password := "p1" masterKey, _ := crypto.GenerateKey() slot, _ := keychain.CreatePasswordSlot(masterKey, password) - _ = keychain.WriteKeySlot(s, slot) + _ = keychain.WriteKeySlot(context.Background(), s, slot) // 2. Adopt with password AND a new platform key platformKey := make([]byte, 32) @@ -424,7 +424,7 @@ func TestInitManager_AdoptAddsNewSlots(t *testing.T) { } // 3. Verify both slots exist and work - slots, err := keychain.LoadKeySlots(s) + slots, err := keychain.LoadKeySlots(context.Background(), s) if err != nil { t.Fatalf("failed to load key slots: %v", err) } diff --git a/pkg/keychain/keychain.go b/pkg/keychain/keychain.go index f27ea63..f4db376 100644 --- a/pkg/keychain/keychain.go +++ b/pkg/keychain/keychain.go @@ -18,7 +18,7 @@ func DeriveEncryptionKey(masterKey []byte) ([]byte, error) { // AddRecoverySlot generates a recovery key, wraps the given master key with // it, stores the recovery slot, and returns the BIP39 24-word mnemonic. -func AddRecoverySlot(s store.ObjectStore, masterKey []byte) (mnemonic string, err error) { +func AddRecoverySlot(ctx context.Context, s store.ObjectStore, masterKey []byte) (mnemonic string, err error) { mnemonic, recoveryKey, err := crypto.GenerateRecoveryMnemonic() if err != nil { return "", err @@ -27,7 +27,7 @@ func AddRecoverySlot(s store.ObjectStore, masterKey []byte) (mnemonic string, er if err != nil { return "", err } - if err := WriteKeySlot(s, slot); err != nil { + if err := WriteKeySlot(ctx, s, slot); err != nil { return "", err } return mnemonic, nil @@ -37,7 +37,7 @@ func AddRecoverySlot(s store.ObjectStore, masterKey []byte) (mnemonic string, er // repository. masterKey is the unwrapped master key; newPassword is the new // password to wrap it with. The old password slot (keys/password-default) is // overwritten. -func ChangePasswordSlot(s store.ObjectStore, masterKey []byte, newPassword string) error { +func ChangePasswordSlot(ctx context.Context, s store.ObjectStore, masterKey []byte, newPassword string) error { if newPassword == "" { return fmt.Errorf("new password cannot be empty") } @@ -45,7 +45,7 @@ func ChangePasswordSlot(s store.ObjectStore, masterKey []byte, newPassword strin if err != nil { return err } - return WriteKeySlot(s, slot) + return WriteKeySlot(ctx, s, slot) } // Credential attempts to resolve or wrap the master key for the repository. diff --git a/pkg/keychain/keyslot.go b/pkg/keychain/keyslot.go index 2b82f35..67aa5fa 100644 --- a/pkg/keychain/keyslot.go +++ b/pkg/keychain/keyslot.go @@ -27,8 +27,7 @@ type KDFParams struct { } // LoadKeySlots reads all key slot objects from the store. -func LoadKeySlots(s store.ObjectStore) ([]KeySlot, error) { - ctx := context.Background() +func LoadKeySlots(ctx context.Context, s store.ObjectStore) ([]KeySlot, error) { keys, err := s.List(ctx, store.KeySlotPrefix) if err != nil { return nil, fmt.Errorf("list key slots: %w", err) @@ -52,17 +51,17 @@ func slotObjectKey(slotType, label string) string { return store.KeySlotPrefix + slotType + "-" + label } -func WriteKeySlot(s store.ObjectStore, slot KeySlot) error { +func WriteKeySlot(ctx context.Context, s store.ObjectStore, slot KeySlot) error { data, err := json.Marshal(slot) if err != nil { return fmt.Errorf("marshal key slot: %w", err) } - return s.Put(context.Background(), slotObjectKey(slot.SlotType, slot.Label), data) + return s.Put(ctx, slotObjectKey(slot.SlotType, slot.Label), data) } // HasKeySlots reports whether the store contains any encryption key slots. -func HasKeySlots(s store.ObjectStore) bool { - keys, err := s.List(context.Background(), store.KeySlotPrefix) +func HasKeySlots(ctx context.Context, s store.ObjectStore) bool { + keys, err := s.List(ctx, store.KeySlotPrefix) return err == nil && len(keys) > 0 } diff --git a/pkg/keychain/keyslot_test.go b/pkg/keychain/keyslot_test.go index 2b688eb..09e8048 100644 --- a/pkg/keychain/keyslot_test.go +++ b/pkg/keychain/keyslot_test.go @@ -90,7 +90,7 @@ func initEncryptionKey(s *mockStore, platformKey []byte, password string) ([]byt return nil, err } for _, slot := range slots { - if err := WriteKeySlot(s, slot); err != nil { + if err := WriteKeySlot(context.Background(), s, slot); err != nil { return nil, err } } @@ -109,7 +109,7 @@ func TestInitAndOpenPlatformKey(t *testing.T) { t.Fatalf("encryption key length = %d, want %d", len(encKey), crypto.KeySize) } - slots, err := LoadKeySlots(inner) + slots, err := LoadKeySlots(context.Background(), inner) if err != nil { t.Fatalf("LoadKeySlots: %v", err) } @@ -143,7 +143,7 @@ func TestInitAndOpenPassword(t *testing.T) { t.Fatalf("InitEncryptionKey: %v", err) } - slots, err := LoadKeySlots(inner) + slots, err := LoadKeySlots(context.Background(), inner) if err != nil { t.Fatalf("LoadKeySlots: %v", err) } @@ -178,7 +178,7 @@ func TestOpenWithWrongPassword(t *testing.T) { t.Fatal(err) } - slots, _ := LoadKeySlots(inner) + slots, _ := LoadKeySlots(context.Background(), inner) if _, err := (Chain{WithPassword("wrong-password")}).Resolve(context.Background(), slots); err == nil { t.Fatal("expected error with wrong password") } @@ -194,7 +194,7 @@ func TestOpenWithWrongPlatformKey(t *testing.T) { t.Fatal(err) } - slots, _ := LoadKeySlots(inner) + slots, _ := LoadKeySlots(context.Background(), inner) if _, err := (Chain{WithPlatformKey(key2)}).Resolve(context.Background(), slots); err == nil { t.Fatal("expected error with wrong platform key") } @@ -210,7 +210,7 @@ func TestDualSlots(t *testing.T) { t.Fatal(err) } - slots, _ := LoadKeySlots(inner) + slots, _ := LoadKeySlots(context.Background(), inner) if len(slots) != 2 { t.Fatalf("expected 2 slots, got %d", len(slots)) } @@ -264,13 +264,13 @@ func TestAddAndOpenRecoveryKey(t *testing.T) { t.Fatal(err) } - slots, _ := LoadKeySlots(inner) + slots, _ := LoadKeySlots(context.Background(), inner) masterKey, err := (Chain{WithPlatformKey(platformKey)}).Resolve(context.Background(), slots) if err != nil { t.Fatalf("Resolve with platform key: %v", err) } - mnemonic, err := AddRecoverySlot(inner, masterKey) + mnemonic, err := AddRecoverySlot(context.Background(), inner, masterKey) if err != nil { t.Fatalf("AddRecoverySlot: %v", err) } @@ -278,7 +278,7 @@ func TestAddAndOpenRecoveryKey(t *testing.T) { t.Fatal("mnemonic should not be empty") } - slots, _ = LoadKeySlots(inner) + slots, _ = LoadKeySlots(context.Background(), inner) var hasRecovery bool for _, s := range slots { if s.SlotType == "recovery" { @@ -309,11 +309,11 @@ func TestOpenWithWrongRecoveryKey(t *testing.T) { t.Fatal(err) } - slots, _ := LoadKeySlots(inner) + slots, _ := LoadKeySlots(context.Background(), inner) mk, _ := (Chain{WithPlatformKey(platformKey)}).Resolve(context.Background(), slots) - _, _ = AddRecoverySlot(inner, mk) + _, _ = AddRecoverySlot(context.Background(), inner, mk) - slots, _ = LoadKeySlots(inner) + slots, _ = LoadKeySlots(context.Background(), inner) // Create another valid mnemonic wrongMnemonic, _, _ := crypto.GenerateRecoveryMnemonic() @@ -333,18 +333,18 @@ func TestChangePasswordSlot(t *testing.T) { t.Fatal(err) } - slots, _ := LoadKeySlots(inner) + slots, _ := LoadKeySlots(context.Background(), inner) mk, err := (Chain{WithPassword(oldPassword)}).Resolve(context.Background(), slots) if err != nil { t.Fatalf("Resolve with old password: %v", err) } - if err := ChangePasswordSlot(inner, mk, newPassword); err != nil { + if err := ChangePasswordSlot(context.Background(), inner, mk, newPassword); err != nil { t.Fatalf("ChangePasswordSlot: %v", err) } // Old password should no longer work. - slots, _ = LoadKeySlots(inner) + slots, _ = LoadKeySlots(context.Background(), inner) if _, err := (Chain{WithPassword(oldPassword)}).Resolve(context.Background(), slots); err == nil { t.Fatal("old password should no longer open the repo") } @@ -364,19 +364,19 @@ func TestChangePasswordSlot(t *testing.T) { func TestChangePasswordSlot_EmptyPassword(t *testing.T) { inner := newMemStore() mk, _ := crypto.GenerateKey() - if err := ChangePasswordSlot(inner, mk, ""); err == nil { + if err := ChangePasswordSlot(context.Background(), inner, mk, ""); err == nil { t.Fatal("expected error for empty password") } } func TestHasKeySlots(t *testing.T) { inner := newMemStore() - if HasKeySlots(inner) { + if HasKeySlots(context.Background(), inner) { t.Fatal("empty store should not have key slots") } key, _ := crypto.GenerateKey() _, _ = initEncryptionKey(inner, key, "") - if !HasKeySlots(inner) { + if !HasKeySlots(context.Background(), inner) { t.Fatal("store should have key slots after init") } }