fix(backup): Extra entry failures warn and continue (cms parity) — never abort the archive
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e12160f05c
commit
7089964f6b
@ -163,6 +163,10 @@ func BuildArchive(ctx context.Context, w io.Writer, in BuildInput) (*Manifest, [
|
||||
}
|
||||
|
||||
// Extra fixed entries, in a stable (sorted) order for reproducibility.
|
||||
// Failure contract (cms parity — the cssManifestsFile block in
|
||||
// buildBackupArchive): a failed Create skips the entry silently, a failed
|
||||
// Write appends a warning. Extra entries are regenerable extras; a problem
|
||||
// with one must never abort the archive.
|
||||
if len(in.Extra) > 0 {
|
||||
names := make([]string, 0, len(in.Extra))
|
||||
for name := range in.Extra {
|
||||
@ -172,10 +176,10 @@ func BuildArchive(ctx context.Context, w io.Writer, in BuildInput) (*Manifest, [
|
||||
for _, name := range names {
|
||||
entry, err := zipWriter.Create(name)
|
||||
if err != nil {
|
||||
return nil, warnings, fmt.Errorf("failed to create %s in zip: %w", name, err)
|
||||
continue
|
||||
}
|
||||
if _, err := entry.Write(in.Extra[name]); err != nil {
|
||||
return nil, warnings, fmt.Errorf("failed to write %s: %w", name, err)
|
||||
warnings = append(warnings, fmt.Sprintf("Failed to write %s: %v", name, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -95,3 +96,76 @@ func TestBuildArchiveRoundTrip(t *testing.T) {
|
||||
t.Fatal("media file missing or corrupted in archive")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildArchiveExtraEntries locks the happy path of the Extra contract: extra
|
||||
// entries land in the archive with their exact contents. The failure half of the
|
||||
// contract (Create failure → skip silently, Write failure → warning, never a
|
||||
// fatal error — cms cssManifestsFile parity) is documented at the Extra block in
|
||||
// BuildArchive; zip.Writer failures cannot be injected deterministically without
|
||||
// error-injection infrastructure, so the failure path is covered for section
|
||||
// files below where the failure is controllable.
|
||||
func TestBuildArchiveExtraEntries(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
_, warnings, err := BuildArchive(context.Background(), &buf, BuildInput{
|
||||
Source: Source{SiteTitle: "Test Site"},
|
||||
Dump: []byte("dump"),
|
||||
Password: "correct horse",
|
||||
Extra: map[string][]byte{
|
||||
"state/css-manifests.json": []byte(`{"plugins":{}}`),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("BuildArchive: %v", err)
|
||||
}
|
||||
if len(warnings) != 0 {
|
||||
t.Fatalf("unexpected warnings: %v", warnings)
|
||||
}
|
||||
if got := string(readZipEntry(t, buf.Bytes(), "state/css-manifests.json")); got != `{"plugins":{}}` {
|
||||
t.Fatalf("extra entry content = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildArchiveUnreadableFileWarnsAndContinues proves the warning-not-error
|
||||
// contract: a file that cannot be opened inside an existing section dir appends
|
||||
// a warning and the archive still completes with every readable file present.
|
||||
func TestBuildArchiveUnreadableFileWarnsAndContinues(t *testing.T) {
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("running as root: chmod 0 files remain readable")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
mediaDir := filepath.Join(dir, "media")
|
||||
if err := os.MkdirAll(mediaDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(mediaDir, "good.txt"), []byte("ok"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
badPath := filepath.Join(mediaDir, "bad.txt")
|
||||
if err := os.WriteFile(badPath, []byte("nope"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(badPath, 0o000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
manifest, warnings, err := BuildArchive(context.Background(), &buf, BuildInput{
|
||||
Source: Source{SiteTitle: "Test Site"},
|
||||
Dump: []byte("dump"),
|
||||
Password: "correct horse",
|
||||
Sections: []Section{{Prefix: "media/", Dir: mediaDir}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unreadable file must not abort the archive, got error: %v", err)
|
||||
}
|
||||
if len(warnings) != 1 || !strings.Contains(warnings[0], "bad.txt") {
|
||||
t.Fatalf("want exactly one warning mentioning bad.txt, got: %v", warnings)
|
||||
}
|
||||
// Both files were collected (open fails later, at write time) — cms parity.
|
||||
if manifest.MediaCount != 2 {
|
||||
t.Fatalf("media count = %d, want 2", manifest.MediaCount)
|
||||
}
|
||||
if got := string(readZipEntry(t, buf.Bytes(), "media/good.txt")); got != "ok" {
|
||||
t.Fatalf("good.txt content = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user