Check blob sizes and the database in shallow verify (closes #169)
check / check (pull_request) Successful in 1m21s
check / check (pull_request) Successful in 1m21s
Shallow `snapshot verify` only checked that each blob object existed and then reported "All blobs verified", overstating what it did. It now compares each blob's stored size against the manifest's compressed_size, using the same comparison as the deep path, and checks that the snapshot's encrypted database (db.zst.age) is present. A blob of the wrong size no longer counts as verified. The final line reports only what was checked: presence and size, not contents. The README verify description and the CLI short/long text are corrected to match. Removed the now-unused resolveAndDownloadManifest helper and errBlobsMissing sentinel. Model: opus-4-8
This commit is contained in:
@@ -71,7 +71,7 @@ Requirements that no existing tool meets:
|
|||||||
## daily use
|
## daily use
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# verify a snapshot (shallow: checks all blobs exist)
|
# verify a snapshot (shallow: checks all blobs are present with the listed size)
|
||||||
vaultik snapshot verify <snapshot-id>
|
vaultik snapshot verify <snapshot-id>
|
||||||
|
|
||||||
# deep verify (downloads and cryptographically verifies every blob)
|
# deep verify (downloads and cryptographically verifies every blob)
|
||||||
@@ -312,7 +312,9 @@ local index alone, and still exits zero.
|
|||||||
logger, so stdout stays a single parseable document.
|
logger, so stdout stays a single parseable document.
|
||||||
|
|
||||||
**`snapshot verify`**: Verify snapshot integrity.
|
**`snapshot verify`**: Verify snapshot integrity.
|
||||||
* Default (shallow): checks that all blobs referenced in the manifest exist in storage
|
* Default (shallow): checks that every blob the manifest lists is present in
|
||||||
|
storage with the size the manifest records, and that the encrypted database is
|
||||||
|
present. It does not read blob contents.
|
||||||
* `--deep`: Downloads and decrypts each blob, verifies chunk hashes against the
|
* `--deep`: Downloads and decrypts each blob, verifies chunk hashes against the
|
||||||
encrypted metadata database
|
encrypted metadata database
|
||||||
* Accepts the same identifiers as `snapshot restore`: a snapshot ID, or a
|
* Accepts the same identifiers as `snapshot restore`: a snapshot ID, or a
|
||||||
|
|||||||
@@ -188,8 +188,11 @@ func newSnapshotVerifyCommand() *cobra.Command {
|
|||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "verify <snapshot-id>",
|
Use: "verify <snapshot-id>",
|
||||||
Short: "Verify snapshot integrity",
|
Short: "Check a snapshot's blobs are present with the listed size",
|
||||||
Long: "Verifies that all blobs referenced in a snapshot exist.\n\n" +
|
Long: "Checks that every blob the snapshot's manifest lists is present\n" +
|
||||||
|
"in storage with the size the manifest records, and that the\n" +
|
||||||
|
"snapshot's encrypted database is present. It does not read blob\n" +
|
||||||
|
"contents; use --deep to download and cryptographically verify them.\n\n" +
|
||||||
"The snapshot may be named by its ID or, on a host with no local\n" +
|
"The snapshot may be named by its ID or, on a host with no local\n" +
|
||||||
"index, by the remote key that 'snapshot list' prints for a\n" +
|
"index, by the remote key that 'snapshot list' prints for a\n" +
|
||||||
"remote-only snapshot (an unambiguous leading part is enough).",
|
"remote-only snapshot (an unambiguous leading part is enough).",
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package vaultik_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"sneak.berlin/go/vaultik/internal/log"
|
||||||
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
||||||
|
"sneak.berlin/go/vaultik/internal/ui"
|
||||||
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestShallowVerifyDetectsWrongBlobSize backs up a real snapshot, runs
|
||||||
|
// shallow verify (which passes and reports exactly the blobs it checked),
|
||||||
|
// then grows one stored blob so its size no longer matches the manifest.
|
||||||
|
// Shallow verify must then fail, count the grown blob as a size mismatch,
|
||||||
|
// and drop it from the verified count rather than continuing to report it
|
||||||
|
// as checked.
|
||||||
|
func TestShallowVerifyDetectsWrongBlobSize(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")
|
||||||
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
||||||
|
|
||||||
|
chunkSize := int64(32 * 1024)
|
||||||
|
maxBlobSize := int64(128 * 1024)
|
||||||
|
|
||||||
|
// Enough data to span several blobs, so the mismatch count and the
|
||||||
|
// dropped verified count are both meaningful.
|
||||||
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs,
|
||||||
|
filepath.Join(dataDir, "data.bin"),
|
||||||
|
bytesPattern("shallow-", int(maxBlobSize*4)), 0o644))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cfg, storer, snapshotID := runFileStorageBackup(
|
||||||
|
ctx, t, fs, dataDir, storeDir, dbPath, chunkSize, maxBlobSize)
|
||||||
|
|
||||||
|
newVerifier := func(out io.Writer) *vaultik.Vaultik {
|
||||||
|
v := &vaultik.Vaultik{
|
||||||
|
Config: cfg,
|
||||||
|
Storage: storer,
|
||||||
|
Fs: fs,
|
||||||
|
Stdout: out,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
UI: ui.NewWithColor(io.Discard, false),
|
||||||
|
}
|
||||||
|
v.SetContext(ctx)
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
|
||||||
|
require.NoError(t,
|
||||||
|
newVerifier(&out).VerifySnapshotWithOptions(
|
||||||
|
snapshotID, &vaultik.VerifyOptions{JSON: true}),
|
||||||
|
"shallow verify should pass on a healthy snapshot")
|
||||||
|
|
||||||
|
healthy := decodeVerifyResult(t, out.Bytes())
|
||||||
|
require.Equal(t, "ok", healthy.Status)
|
||||||
|
require.Positive(t, healthy.BlobCount)
|
||||||
|
require.Equal(t, healthy.BlobCount, healthy.Verified,
|
||||||
|
"shallow verify must report exactly the blobs it checked")
|
||||||
|
require.Zero(t, healthy.Mismatched)
|
||||||
|
|
||||||
|
// Grow one stored blob so its size no longer matches the manifest.
|
||||||
|
growOneBlob(t, fs, filepath.Join(storeDir, "blobs"))
|
||||||
|
|
||||||
|
out.Reset()
|
||||||
|
err := newVerifier(&out).VerifySnapshotWithOptions(
|
||||||
|
snapshotID, &vaultik.VerifyOptions{JSON: true})
|
||||||
|
require.Error(t, err,
|
||||||
|
"shallow verify must fail when a blob's stored size differs from the manifest")
|
||||||
|
|
||||||
|
bad := decodeVerifyResult(t, out.Bytes())
|
||||||
|
require.Equal(t, "failed", bad.Status)
|
||||||
|
require.Equal(t, 1, bad.Mismatched)
|
||||||
|
require.Equal(t, healthy.BlobCount-1, bad.Verified,
|
||||||
|
"the wrong-sized blob must not be counted as verified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestShallowVerifyDetectsMissingDatabase backs up a real snapshot,
|
||||||
|
// confirms shallow verify passes, then deletes the snapshot's encrypted
|
||||||
|
// database. Shallow verify must fail: a snapshot without its database is
|
||||||
|
// not restorable, even when every blob is present.
|
||||||
|
func TestShallowVerifyDetectsMissingDatabase(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")
|
||||||
|
dbPath := filepath.Join(tempDir, "index.sqlite")
|
||||||
|
|
||||||
|
chunkSize := int64(32 * 1024)
|
||||||
|
maxBlobSize := int64(128 * 1024)
|
||||||
|
|
||||||
|
require.NoError(t, fs.MkdirAll(dataDir, 0o755))
|
||||||
|
require.NoError(t, afero.WriteFile(fs,
|
||||||
|
filepath.Join(dataDir, "data.bin"),
|
||||||
|
bytesPattern("shallow-db-", int(maxBlobSize*2)), 0o644))
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cfg, storer, snapshotID := runFileStorageBackup(
|
||||||
|
ctx, t, fs, dataDir, storeDir, dbPath, chunkSize, maxBlobSize)
|
||||||
|
|
||||||
|
newVerifier := func() *vaultik.Vaultik {
|
||||||
|
v := &vaultik.Vaultik{
|
||||||
|
Config: cfg,
|
||||||
|
Storage: storer,
|
||||||
|
Fs: fs,
|
||||||
|
Stdout: io.Discard,
|
||||||
|
Stderr: io.Discard,
|
||||||
|
UI: ui.NewWithColor(io.Discard, false),
|
||||||
|
}
|
||||||
|
v.SetContext(ctx)
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t,
|
||||||
|
newVerifier().VerifySnapshotWithOptions(
|
||||||
|
snapshotID, &vaultik.VerifyOptions{}),
|
||||||
|
"shallow verify should pass on a healthy snapshot")
|
||||||
|
|
||||||
|
// The database lives under the hashed remote key, not the human ID.
|
||||||
|
dbObject := filepath.Join(storeDir, "metadata",
|
||||||
|
snapshot.RemoteSnapshotKey(snapshotID), "db.zst.age")
|
||||||
|
require.NoError(t, os.Remove(dbObject))
|
||||||
|
|
||||||
|
require.Error(t,
|
||||||
|
newVerifier().VerifySnapshotWithOptions(
|
||||||
|
snapshotID, &vaultik.VerifyOptions{}),
|
||||||
|
"shallow verify must fail when db.zst.age is absent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// growOneBlob appends bytes to the first blob file found under blobsDir,
|
||||||
|
// changing its on-disk size so it no longer matches the manifest.
|
||||||
|
func growOneBlob(t *testing.T, fs afero.Fs, blobsDir string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var blobPath string
|
||||||
|
|
||||||
|
err := afero.Walk(fs, blobsDir,
|
||||||
|
func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if blobPath == "" && !info.IsDir() {
|
||||||
|
blobPath = path
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, blobPath, "expected at least one blob on disk")
|
||||||
|
|
||||||
|
data, err := afero.ReadFile(fs, blobPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
data = append(data, []byte("extra")...)
|
||||||
|
require.NoError(t, afero.WriteFile(fs, blobPath, data, 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeVerifyResult(t *testing.T, b []byte) vaultik.VerifyResult {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var result vaultik.VerifyResult
|
||||||
|
|
||||||
|
require.NoError(t, json.Unmarshal(b, &result))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
var (
|
var (
|
||||||
errSnapshotNotInConfig = errors.New("snapshot not found in config")
|
errSnapshotNotInConfig = errors.New("snapshot not found in config")
|
||||||
errNoSnapshotsInConfig = errors.New("no snapshots configured")
|
errNoSnapshotsInConfig = errors.New("no snapshots configured")
|
||||||
errBlobsMissing = errors.New("blobs are missing")
|
|
||||||
errSnapshotVerifyFailed = errors.New("verification failed")
|
errSnapshotVerifyFailed = errors.New("verification failed")
|
||||||
errRemoveAllNeedsForce = errors.New("--all requires --force")
|
errRemoveAllNeedsForce = errors.New("--all requires --force")
|
||||||
errInvalidTableName = errors.New("invalid table name")
|
errInvalidTableName = errors.New("invalid table name")
|
||||||
@@ -670,11 +669,24 @@ func (v *Vaultik) VerifySnapshotWithOptions(
|
|||||||
|
|
||||||
v.printVerifyHeader(snapshotID, opts)
|
v.printVerifyHeader(snapshotID, opts)
|
||||||
|
|
||||||
// Resolve the identifier to the snapshot's remote key and download the
|
// Resolve the identifier to the snapshot's remote key. A human ID is
|
||||||
// manifest. A human ID is hashed; a remote key (or its abbreviation,
|
// hashed; a remote key (or its abbreviation, as printed for a
|
||||||
// as printed for a remote-only snapshot) is used as-is, so a host with
|
// remote-only snapshot) is used as-is, so a host with no local index
|
||||||
// no local index can verify a snapshot it can only see on the store.
|
// can verify a snapshot it can only see on the store. The key is kept
|
||||||
manifest, err := v.resolveAndDownloadManifest(snapshotID)
|
// so we can also check for the snapshot's encrypted database below.
|
||||||
|
remoteKey, err := v.resolveSnapshotRemoteKey(snapshotID)
|
||||||
|
if err != nil {
|
||||||
|
if opts.JSON {
|
||||||
|
result.Status = verifyStatusFailed
|
||||||
|
result.ErrorMessage = fmt.Sprintf("resolving snapshot identifier: %v", err)
|
||||||
|
|
||||||
|
return v.outputVerifyJSON(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("resolving snapshot identifier: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest, err := v.downloadManifestByKey(remoteKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if opts.JSON {
|
if opts.JSON {
|
||||||
result.Status = verifyStatusFailed
|
result.Status = verifyStatusFailed
|
||||||
@@ -704,14 +716,24 @@ func (v *Vaultik) VerifySnapshotWithOptions(
|
|||||||
|
|
||||||
v.printlnStdout()
|
v.printlnStdout()
|
||||||
|
|
||||||
// Check each blob exists
|
// Check each blob is present with the size the manifest records.
|
||||||
v.stdoutf("Checking blob existence...\n")
|
v.stdoutf("Checking blob presence and sizes...\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Verified, result.Missing, result.MissingSize =
|
// A snapshot is only restorable if its encrypted database is present
|
||||||
v.verifyManifestBlobsExist(manifest, opts)
|
// alongside the blobs. Shallow verify checks that the object exists; it
|
||||||
|
// does not decrypt it (that is deep verify's job).
|
||||||
|
dbPath := fmt.Sprintf("metadata/%s/db.zst.age", remoteKey)
|
||||||
|
|
||||||
return v.formatVerifyResult(result, manifest, opts)
|
_, dbErr := v.Storage.Stat(v.ctx, dbPath)
|
||||||
|
if dbErr != nil {
|
||||||
|
result.DatabaseMissing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Verified, result.Missing, result.Mismatched, result.MissingSize =
|
||||||
|
v.verifyManifestBlobs(manifest, opts)
|
||||||
|
|
||||||
|
return v.formatVerifyResult(result, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// printVerifyHeader prints the snapshot ID and parsed timestamp for
|
// printVerifyHeader prints the snapshot ID and parsed timestamp for
|
||||||
@@ -736,25 +758,27 @@ func (v *Vaultik) printVerifyHeader(snapshotID string, opts *VerifyOptions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyManifestBlobsExist checks that each blob in the manifest exists
|
// verifyManifestBlobs checks that each blob in the manifest is present in
|
||||||
// in storage, returning the verified count, missing count, and total
|
// storage with the size the manifest records, returning the counts of
|
||||||
// missing bytes.
|
// blobs that were present with the right size, absent, and present but the
|
||||||
func (v *Vaultik) verifyManifestBlobsExist(
|
// wrong size, plus the total bytes of the absent blobs. It does not read
|
||||||
|
// blob contents; deep verification (RunDeepVerify) does that. The size
|
||||||
|
// comparison matches the deep path (see verifyBlobExistenceFromDB).
|
||||||
|
func (v *Vaultik) verifyManifestBlobs(
|
||||||
manifest *snapshot.Manifest, opts *VerifyOptions,
|
manifest *snapshot.Manifest, opts *VerifyOptions,
|
||||||
) (int, int, int64) {
|
) (int, int, int, int64) {
|
||||||
var (
|
var (
|
||||||
verified, missing int
|
verified, missing, mismatched int
|
||||||
missingSize int64
|
missingSize int64
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, blob := range manifest.Blobs {
|
for _, blob := range manifest.Blobs {
|
||||||
blobPath := fmt.Sprintf("blobs/%s/%s/%s",
|
blobPath := fmt.Sprintf("blobs/%s/%s/%s",
|
||||||
blob.Hash[:2], blob.Hash[2:4], blob.Hash)
|
blob.Hash[:2], blob.Hash[2:4], blob.Hash)
|
||||||
|
|
||||||
// Shallow: check existence only (deep verification is handled
|
stat, err := v.Storage.Stat(v.ctx, blobPath)
|
||||||
// by RunDeepVerify).
|
switch {
|
||||||
_, err := v.Storage.Stat(v.ctx, blobPath)
|
case err != nil:
|
||||||
if err != nil {
|
|
||||||
if !opts.JSON {
|
if !opts.JSON {
|
||||||
v.stdoutf(" Missing: %s (%s)\n",
|
v.stdoutf(" Missing: %s (%s)\n",
|
||||||
blob.Hash, ubytes(blob.CompressedSize))
|
blob.Hash, ubytes(blob.CompressedSize))
|
||||||
@@ -762,23 +786,32 @@ func (v *Vaultik) verifyManifestBlobsExist(
|
|||||||
|
|
||||||
missing++
|
missing++
|
||||||
missingSize += blob.CompressedSize
|
missingSize += blob.CompressedSize
|
||||||
} else {
|
case stat.Size != blob.CompressedSize:
|
||||||
|
if !opts.JSON {
|
||||||
|
v.stdoutf(" Wrong size: %s (store has %s, manifest lists %s)\n",
|
||||||
|
blob.Hash, ubytes(stat.Size), ubytes(blob.CompressedSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
mismatched++
|
||||||
|
default:
|
||||||
verified++
|
verified++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return verified, missing, missingSize
|
return verified, missing, mismatched, missingSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatVerifyResult outputs the final verification results as JSON or
|
// formatVerifyResult outputs the final verification results as JSON or
|
||||||
// human-readable text.
|
// human-readable text.
|
||||||
func (v *Vaultik) formatVerifyResult(
|
func (v *Vaultik) formatVerifyResult(
|
||||||
result *VerifyResult, manifest *snapshot.Manifest, opts *VerifyOptions,
|
result *VerifyResult, opts *VerifyOptions,
|
||||||
) error {
|
) error {
|
||||||
|
failure := shallowVerifyFailure(result)
|
||||||
|
|
||||||
if opts.JSON {
|
if opts.JSON {
|
||||||
if result.Missing > 0 {
|
if failure != "" {
|
||||||
result.Status = verifyStatusFailed
|
result.Status = verifyStatusFailed
|
||||||
result.ErrorMessage = fmt.Sprintf("%d blobs are missing", result.Missing)
|
result.ErrorMessage = failure
|
||||||
} else {
|
} else {
|
||||||
result.Status = "ok"
|
result.Status = "ok"
|
||||||
}
|
}
|
||||||
@@ -787,29 +820,57 @@ func (v *Vaultik) formatVerifyResult(
|
|||||||
}
|
}
|
||||||
|
|
||||||
v.stdoutf("\nVerification complete:\n")
|
v.stdoutf("\nVerification complete:\n")
|
||||||
v.stdoutf(" Verified: %d blobs (%s)\n", result.Verified,
|
v.stdoutf(" Present with listed size: %d blobs\n", result.Verified)
|
||||||
ubytes(manifest.TotalCompressedSize-result.MissingSize))
|
|
||||||
|
|
||||||
if result.Missing > 0 {
|
if result.Missing > 0 {
|
||||||
v.stdoutf(" Missing: %d blobs (%s)\n",
|
v.stdoutf(" Missing: %d blobs (%s)\n",
|
||||||
result.Missing, ubytes(result.MissingSize))
|
result.Missing, ubytes(result.MissingSize))
|
||||||
} else {
|
}
|
||||||
v.stdoutf(" Missing: 0 blobs\n")
|
|
||||||
|
if result.Mismatched > 0 {
|
||||||
|
v.stdoutf(" Wrong size: %d blobs\n", result.Mismatched)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.DatabaseMissing {
|
||||||
|
v.stdoutf(" Encrypted database: missing\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
v.stdoutf(" Status: ")
|
v.stdoutf(" Status: ")
|
||||||
|
|
||||||
if result.Missing > 0 {
|
if failure != "" {
|
||||||
v.stdoutf("FAILED - %d blobs are missing\n", result.Missing)
|
v.stdoutf("FAILED - %s\n", failure)
|
||||||
|
|
||||||
return fmt.Errorf("%d %w", result.Missing, errBlobsMissing)
|
return fmt.Errorf("%w: %s", errSnapshotVerifyFailed, failure)
|
||||||
}
|
}
|
||||||
|
|
||||||
v.stdoutf("OK - All blobs verified\n")
|
// Report only what was actually checked: presence and size, not contents.
|
||||||
|
v.stdoutf("OK - all %d blobs listed in the manifest are present with the "+
|
||||||
|
"listed size; contents not checked (use --deep)\n", result.Verified)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shallowVerifyFailure returns a human-readable description of everything
|
||||||
|
// that failed shallow verification, or the empty string if it passed.
|
||||||
|
func shallowVerifyFailure(result *VerifyResult) string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
if result.Missing > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("%d blobs are missing", result.Missing))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Mismatched > 0 {
|
||||||
|
parts = append(parts,
|
||||||
|
fmt.Sprintf("%d blobs have the wrong size", result.Mismatched))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.DatabaseMissing {
|
||||||
|
parts = append(parts, "the encrypted database is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
// outputVerifyJSON outputs the verification result as JSON
|
// outputVerifyJSON outputs the verification result as JSON
|
||||||
func (v *Vaultik) outputVerifyJSON(result *VerifyResult) error {
|
func (v *Vaultik) outputVerifyJSON(result *VerifyResult) error {
|
||||||
encoder := json.NewEncoder(v.Stdout)
|
encoder := json.NewEncoder(v.Stdout)
|
||||||
|
|||||||
@@ -69,20 +69,6 @@ func (v *Vaultik) resolveSnapshotRemoteKey(identifier string) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveAndDownloadManifest resolves a snapshot identifier to its remote
|
|
||||||
// key (see resolveSnapshotRemoteKey) and downloads that snapshot's
|
|
||||||
// manifest.
|
|
||||||
func (v *Vaultik) resolveAndDownloadManifest(
|
|
||||||
identifier string,
|
|
||||||
) (*snapshot.Manifest, error) {
|
|
||||||
remoteKey, err := v.resolveSnapshotRemoteKey(identifier)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.downloadManifestByKey(remoteKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// isRemoteKeyOrPrefix reports whether s is a full remote key or the
|
// isRemoteKeyOrPrefix reports whether s is a full remote key or the
|
||||||
// leading part of one: 1 to 64 lowercase hex characters. A human snapshot
|
// leading part of one: 1 to 64 lowercase hex characters. A human snapshot
|
||||||
// ID is never all hex, so this shape test is enough to tell the two apart.
|
// ID is never all hex, so this shape test is enough to tell the two apart.
|
||||||
|
|||||||
@@ -47,15 +47,20 @@ type VerifyOptions struct {
|
|||||||
//
|
//
|
||||||
//nolint:tagliatelle // snake_case is the established JSON output format
|
//nolint:tagliatelle // snake_case is the established JSON output format
|
||||||
type VerifyResult struct {
|
type VerifyResult struct {
|
||||||
SnapshotID string `json:"snapshot_id"`
|
SnapshotID string `json:"snapshot_id"`
|
||||||
Status string `json:"status"` // "ok" or "failed"
|
Status string `json:"status"` // "ok" or "failed"
|
||||||
Mode string `json:"mode"` // "shallow" or "deep"
|
Mode string `json:"mode"` // "shallow" or "deep"
|
||||||
BlobCount int `json:"blob_count"`
|
BlobCount int `json:"blob_count"`
|
||||||
TotalSize int64 `json:"total_size"`
|
TotalSize int64 `json:"total_size"`
|
||||||
Verified int `json:"verified"`
|
Verified int `json:"verified"`
|
||||||
Missing int `json:"missing"`
|
Missing int `json:"missing"`
|
||||||
MissingSize int64 `json:"missing_size,omitempty"`
|
MissingSize int64 `json:"missing_size,omitempty"`
|
||||||
ErrorMessage string `json:"error,omitempty"`
|
Mismatched int `json:"mismatched,omitempty"`
|
||||||
|
// DatabaseMissing is set by shallow verify when the snapshot's
|
||||||
|
// encrypted database (metadata/<key>/db.zst.age) is absent, which
|
||||||
|
// makes the snapshot unrestorable regardless of the blobs.
|
||||||
|
DatabaseMissing bool `json:"database_missing,omitempty"`
|
||||||
|
ErrorMessage string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// deepVerifyFailure records a failure in the result and returns it appropriately
|
// deepVerifyFailure records a failure in the result and returns it appropriately
|
||||||
|
|||||||
Reference in New Issue
Block a user