Open extracts and verifies every member against the manifest before the reader is usable, so a corrupt archive is refused before a restore writes its first document rather than halfway through.
217 lines
5.0 KiB
Go
217 lines
5.0 KiB
Go
package backup
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// writeSampleArchive builds a two-collection archive on disk and returns its path.
|
|
func writeSampleArchive(t *testing.T) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "sample.tar.gz")
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
w := NewWriter(f)
|
|
servers, err := w.WriteCollection("servers", [][]byte{[]byte("one"), []byte("two")})
|
|
if err != nil {
|
|
t.Fatalf("WriteCollection: %v", err)
|
|
}
|
|
if err := w.WriteIndexes("servers", []byte(`[{"name":"idx"}]`)); err != nil {
|
|
t.Fatalf("WriteIndexes: %v", err)
|
|
}
|
|
keys, err := w.WriteCollection("keys", [][]byte{[]byte("k")})
|
|
if err != nil {
|
|
t.Fatalf("WriteCollection: %v", err)
|
|
}
|
|
if err := w.Close(Manifest{
|
|
FormatVersion: FormatVersion,
|
|
CreatedAt: time.Now().UTC(),
|
|
MongoDB: "vantage",
|
|
Collections: []CollectionEntry{servers, keys},
|
|
}); err != nil {
|
|
t.Fatalf("Close: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestWriterRecordsCountsAndChecksums(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
w := NewWriter(&buf)
|
|
e, err := w.WriteCollection("servers", [][]byte{[]byte("one"), []byte("two")})
|
|
if err != nil {
|
|
t.Fatalf("WriteCollection: %v", err)
|
|
}
|
|
if e.Name != "servers" {
|
|
t.Fatalf("name %q", e.Name)
|
|
}
|
|
if e.Documents != 2 {
|
|
t.Fatalf("documents %d, want 2", e.Documents)
|
|
}
|
|
if e.Bytes != 6 {
|
|
t.Fatalf("bytes %d, want 6", e.Bytes)
|
|
}
|
|
if len(e.SHA256) != 64 {
|
|
t.Fatalf("sha256 %q is not 64 hex chars", e.SHA256)
|
|
}
|
|
}
|
|
|
|
func TestRoundTrip(t *testing.T) {
|
|
r, err := Open(writeSampleArchive(t))
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
defer r.Close()
|
|
|
|
if r.Manifest().MongoDB != "vantage" {
|
|
t.Fatalf("manifest not read back: %+v", r.Manifest())
|
|
}
|
|
|
|
rc, err := r.OpenCollection("servers")
|
|
if err != nil {
|
|
t.Fatalf("OpenCollection: %v", err)
|
|
}
|
|
defer rc.Close()
|
|
got, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if string(got) != "onetwo" {
|
|
t.Fatalf("got %q, want %q", got, "onetwo")
|
|
}
|
|
|
|
idx, err := r.IndexesJSON("servers")
|
|
if err != nil {
|
|
t.Fatalf("IndexesJSON: %v", err)
|
|
}
|
|
if string(idx) != `[{"name":"idx"}]` {
|
|
t.Fatalf("indexes round-tripped as %q", idx)
|
|
}
|
|
}
|
|
|
|
func TestIndexesJSONAbsentIsEmptyNotError(t *testing.T) {
|
|
r, err := Open(writeSampleArchive(t))
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
defer r.Close()
|
|
idx, err := r.IndexesJSON("keys")
|
|
if err != nil {
|
|
t.Fatalf("a collection with no index member must not error: %v", err)
|
|
}
|
|
if len(idx) != 0 {
|
|
t.Fatalf("want empty, got %q", idx)
|
|
}
|
|
}
|
|
|
|
func TestOpenRejectsCorruptedMember(t *testing.T) {
|
|
path := writeSampleArchive(t)
|
|
|
|
// Rewrite the archive with one byte of a collection member flipped, leaving
|
|
// the manifest's checksum describing the original.
|
|
corrupt := filepath.Join(t.TempDir(), "corrupt.tar.gz")
|
|
rewriteFlippingCollectionByte(t, path, corrupt, "servers")
|
|
|
|
if _, err := Open(corrupt); !errors.Is(err, ErrChecksum) {
|
|
t.Fatalf("got %v, want ErrChecksum", err)
|
|
}
|
|
}
|
|
|
|
func TestOpenRejectsUnknownFormatVersion(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "future.tar.gz")
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
w := NewWriter(f)
|
|
if err := w.Close(Manifest{FormatVersion: 99}); err != nil {
|
|
t.Fatalf("Close: %v", err)
|
|
}
|
|
f.Close()
|
|
|
|
if _, err := Open(path); !errors.Is(err, ErrUnknownFormat) {
|
|
t.Fatalf("got %v, want ErrUnknownFormat", err)
|
|
}
|
|
}
|
|
|
|
func TestCloseRemovesTempDir(t *testing.T) {
|
|
r, err := Open(writeSampleArchive(t))
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
dir := r.dir
|
|
if _, err := os.Stat(dir); err != nil {
|
|
t.Fatalf("temp dir missing while open: %v", err)
|
|
}
|
|
if err := r.Close(); err != nil {
|
|
t.Fatalf("Close: %v", err)
|
|
}
|
|
if _, err := os.Stat(dir); !os.IsNotExist(err) {
|
|
t.Fatalf("temp dir %s survived Close", dir)
|
|
}
|
|
}
|
|
|
|
// rewriteFlippingCollectionByte copies an archive, flipping one byte inside the
|
|
// named collection's .bson member so its content no longer matches the checksum
|
|
// the manifest recorded.
|
|
func rewriteFlippingCollectionByte(t *testing.T, src, dst, collection string) {
|
|
t.Helper()
|
|
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
t.Fatalf("open src: %v", err)
|
|
}
|
|
defer in.Close()
|
|
gz, err := gzip.NewReader(in)
|
|
if err != nil {
|
|
t.Fatalf("gzip: %v", err)
|
|
}
|
|
defer gz.Close()
|
|
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
t.Fatalf("create dst: %v", err)
|
|
}
|
|
defer out.Close()
|
|
gw := gzip.NewWriter(out)
|
|
defer gw.Close()
|
|
tw := tar.NewWriter(gw)
|
|
defer tw.Close()
|
|
|
|
tr := tar.NewReader(gz)
|
|
target := "collections/" + collection + ".bson"
|
|
for {
|
|
h, err := tr.Next()
|
|
if err == io.EOF {
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("tar next: %v", err)
|
|
}
|
|
body, err := io.ReadAll(tr)
|
|
if err != nil {
|
|
t.Fatalf("read member: %v", err)
|
|
}
|
|
if h.Name == target && len(body) > 0 {
|
|
body[0] ^= 0xFF
|
|
}
|
|
h.Size = int64(len(body))
|
|
if err := tw.WriteHeader(h); err != nil {
|
|
t.Fatalf("write header: %v", err)
|
|
}
|
|
if _, err := tw.Write(body); err != nil {
|
|
t.Fatalf("write body: %v", err)
|
|
}
|
|
}
|
|
}
|