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.
|
// 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 {
|
if len(in.Extra) > 0 {
|
||||||
names := make([]string, 0, len(in.Extra))
|
names := make([]string, 0, len(in.Extra))
|
||||||
for name := range 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 {
|
for _, name := range names {
|
||||||
entry, err := zipWriter.Create(name)
|
entry, err := zipWriter.Create(name)
|
||||||
if err != nil {
|
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 {
|
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"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -95,3 +96,76 @@ func TestBuildArchiveRoundTrip(t *testing.T) {
|
|||||||
t.Fatal("media file missing or corrupted in archive")
|
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