From 81cedf4e9eed75c7fc89bc9b44cdff9e13fa66bd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 01:46:50 +0100 Subject: [PATCH] fix(backup): ignore interrupted shard temps --- internal/backup/backup.go | 22 ++++++++++++++++++++++ internal/backup/backup_test.go | 19 +++++++++++++++++++ internal/backup/git.go | 3 +++ 3 files changed, 44 insertions(+) diff --git a/internal/backup/backup.go b/internal/backup/backup.go index d604c4c..38f5d60 100644 --- a/internal/backup/backup.go +++ b/internal/backup/backup.go @@ -760,3 +760,25 @@ func removeStaleShards(repo string, shards []ShardEntry) error { } return nil } + +func removeTempShardFiles(repo string) error { + for _, rootName := range []string{"data", "checkpoints"} { + root := filepath.Join(repo, rootName) + if _, err := os.Stat(root); os.IsNotExist(err) { + continue + } + if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil || d == nil || d.IsDir() { + return err + } + name := d.Name() + if strings.HasPrefix(name, ".shard-") && strings.HasSuffix(name, ".age") { + return os.Remove(path) //nolint:gosec // repo-owned temp shard paths come from WalkDir below configured backup roots. + } + return nil + }); err != nil { + return err + } + } + return nil +} diff --git a/internal/backup/backup_test.go b/internal/backup/backup_test.go index 5005a98..6d7811f 100644 --- a/internal/backup/backup_test.go +++ b/internal/backup/backup_test.go @@ -141,6 +141,25 @@ func TestPushCheckpointWritesIncompleteManifestOutsideMainSnapshot(t *testing.T) } } +func TestCommitAndPushRemovesInterruptedShardTemps(t *testing.T) { + ctx, repo, config, _ := initTestBackup(t) + temp := filepath.Join(repo, "checkpoints", "gmail", "acct", "run-one", "messages", ".shard-interrupted.age") + if err := os.MkdirAll(filepath.Dir(temp), 0o700); err != nil { + t.Fatalf("MkdirAll: %v", err) + } + if err := os.WriteFile(temp, []byte("partial ciphertext"), 0o600); err != nil { + t.Fatalf("WriteFile: %v", err) + } + + pushSingleShard(t, ctx, config, mustGmailMessageShard(t, "data/gmail/acct/messages/2026/04/part-0001.jsonl.gz.age", []map[string]string{{"id": "m1", "raw": "final"}})) + if _, err := os.Stat(temp); !os.IsNotExist(err) { + t.Fatalf("temp shard should be removed before commit: %v", err) + } + if err := git(ctx, repo, "ls-files", "--error-unmatch", "checkpoints/gmail/acct/run-one/messages/.shard-interrupted.age"); err == nil { + t.Fatal("temp shard was committed") + } +} + func TestCatAndDecryptSnapshotVerifyPlaintext(t *testing.T) { ctx, repo, config, _ := initTestBackup(t) shardPath := "data/gmail/acct/messages/2026/04/part-0001.jsonl.gz.age" diff --git a/internal/backup/git.go b/internal/backup/git.go index 525f3a5..f179092 100644 --- a/internal/backup/git.go +++ b/internal/backup/git.go @@ -54,6 +54,9 @@ func ensureRepo(ctx context.Context, cfg Config) error { } func commitAndPush(ctx context.Context, cfg Config, message string, push bool) (bool, error) { + if err := removeTempShardFiles(cfg.Repo); err != nil { + return false, err + } if err := git(ctx, cfg.Repo, "add", "."); err != nil { return false, err }