check / check (push) Waiting to run
When a finished blob already existed at the destination, the upload was skipped but the blob row, and so the manifest, kept the size of the local copy. That copy can differ from the stored object when compression_level or the recipients changed since it was written, and then shallow and deep verify failed on a good snapshot. Marking a blob uploaded now records the size of the object in storage: for an existing blob, the size Stat reports. Judgement call: the test adds a recipient rather than changing the compression level, because an extra age recipient always makes the local copy larger, while two zstd levels need not compress the test data differently. Model: opus-5-5
84 lines
2.5 KiB
Go
84 lines
2.5 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"filippo.io/age"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// TestVerifyPassesWhenBackupReusesStoredBlob backs up a file, then backs it
|
|
// up again from an empty local index, as after `database delete`, with an
|
|
// extra age recipient. The second backup builds the same blob, finds it
|
|
// already stored and skips the upload, but its own copy is larger, because
|
|
// age writes a header entry for each recipient. The second snapshot must
|
|
// record the size of the stored object, so shallow and deep verify pass.
|
|
func TestVerifyPassesWhenBackupReusesStoredBlob(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
fs := afero.NewOsFs()
|
|
tempDir := t.TempDir()
|
|
|
|
dataDir := filepath.Join(tempDir, "source")
|
|
storeDir := filepath.Join(tempDir, "remote")
|
|
secondDBPath := filepath.Join(tempDir, "second.sqlite")
|
|
|
|
chunkSize := int64(64 * 1024)
|
|
maxBlobSize := int64(512 * 1024)
|
|
|
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
|
require.NoError(t, afero.WriteFile(fs,
|
|
filepath.Join(dataDir, "data.bin"),
|
|
bytesPattern("reuse-", int(chunkSize*3)), 0o644))
|
|
|
|
ctx := context.Background()
|
|
|
|
runFileStorageBackup(ctx, t, fs, dataDir, storeDir,
|
|
filepath.Join(tempDir, "first.sqlite"), chunkSize, maxBlobSize)
|
|
|
|
env := setupDedupBackupEnv(
|
|
ctx, t, fs, storeDir, secondDBPath, chunkSize, maxBlobSize)
|
|
|
|
defer func() { _ = env.db.Close() }()
|
|
|
|
extra, err := age.GenerateX25519Identity()
|
|
require.NoError(t, err)
|
|
|
|
env.cfg.AgeRecipients = append(env.cfg.AgeRecipients,
|
|
extra.Recipient().String())
|
|
|
|
snapshotID, result := runDedupSnapshot(ctx, t, env.sm, env.makeScanner(),
|
|
env.cfg.Hostname, "second", dataDir, secondDBPath)
|
|
require.Positive(t, result.BlobsCreated)
|
|
require.Zero(t, result.BlobsUploaded,
|
|
"the second backup should find every blob already stored")
|
|
|
|
require.NoError(t, env.db.Close())
|
|
|
|
verifier := &vaultik.Vaultik{
|
|
Config: env.cfg,
|
|
Storage: env.storer,
|
|
Fs: fs,
|
|
Stdout: io.Discard,
|
|
Stderr: io.Discard,
|
|
UI: ui.NewWithColor(io.Discard, false),
|
|
}
|
|
verifier.SetContext(ctx)
|
|
|
|
require.NoError(t,
|
|
verifier.VerifySnapshotWithOptions(snapshotID, &vaultik.VerifyOptions{}),
|
|
"shallow verify should pass")
|
|
require.NoError(t,
|
|
verifier.VerifySnapshotWithOptions(snapshotID,
|
|
&vaultik.VerifyOptions{Deep: true}),
|
|
"deep verify should pass")
|
|
}
|