diff --git a/TODO.md b/TODO.md index 64f0e47..69d31aa 100644 --- a/TODO.md +++ b/TODO.md @@ -24,6 +24,12 @@ owner's; a `v*` tag runs the release build in # Completed Steps +- 2026-11-10: Printed the status and progress lines of `remote info` + and `snapshot verify` through `internal/ui`, with its marker and + color, as the README describes + ([issue #410](https://git.eeqj.de/sneak/vaultik/issues/410)). Their + reports stay plain. + - 2026-11-10: Made `config init` write the `apps` snapshot, which backs up `/Applications`, only on macOS ([issue #408](https://git.eeqj.de/sneak/vaultik/issues/408)). On diff --git a/internal/vaultik/info.go b/internal/vaultik/info.go index 9cd034f..c4cc18c 100644 --- a/internal/vaultik/info.go +++ b/internal/vaultik/info.go @@ -246,7 +246,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { v.stdoutf("Type: %s\n", storageInfo.Type) v.stdoutf("Location: %s\n", storageInfo.Location) v.printlnStdout() - v.stdoutf("Scanning snapshot metadata...\n") + v.UI.Beginf("Scanning snapshot metadata...") } snapshotMetadata, snapshotIDs, skippedManifestCount, err := v.collectSnapshotMetadata() @@ -265,7 +265,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { } } - v.stdoutf("Downloading %d manifest(s)...\n", manifestCount) + v.UI.Beginf("Downloading %d manifest(s)...", manifestCount) } referencedBlobs, unreadableManifests := v.collectReferencedBlobsFromManifests( @@ -447,7 +447,7 @@ func (v *Vaultik) scanRemoteBlobStorage( result *RemoteInfoResult, referencedBlobs map[string]int64, showText bool, ) error { if showText { - v.stdoutf("Scanning blobs...\n") + v.UI.Beginf("Scanning blobs...") } // Use prune's listing, so a file under blobs/ that prune would never diff --git a/internal/vaultik/remote_info_test.go b/internal/vaultik/remote_info_test.go index 76bac72..5d68a03 100644 --- a/internal/vaultik/remote_info_test.go +++ b/internal/vaultik/remote_info_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/snapshot" + "sneak.berlin/go/vaultik/internal/ui" ) // testBlobHashB is a blob that the manifest written by addRemote does @@ -259,3 +260,24 @@ func TestRemoteInfo_DirectoryWithoutManifestLeavesOrphansKnown(t *testing.T) { assert.NotContains(t, doc, "unreadable_manifests") assert.NotContains(t, doc, "skipped_manifest_count") } + +// TestRemoteInfo_StatusLineUsesUIMarker checks that the remote info +// status lines go through internal/ui: marker-prefixed, and silenced by +// --quiet. +func TestRemoteInfo_StatusLineUsesUIMarker(t *testing.T) { + log.Initialize(log.Config{}) + t.Parallel() + + env := newListEnv(t) + env.addRemote(t, listRemoteID, time.Date(2026, 3, 2, 0, 0, 0, 0, time.UTC)) + + const statusLine = ui.Marker + " Scanning snapshot metadata...\n" + + require.NoError(t, env.v.RemoteInfo(false)) + assert.Contains(t, env.stdout.String(), statusLine) + + env.stdout.Reset() + env.v.UI.SetQuiet(true) + require.NoError(t, env.v.RemoteInfo(false)) + assert.NotContains(t, env.stdout.String(), "Scanning snapshot metadata") +} diff --git a/internal/vaultik/snapshot.go b/internal/vaultik/snapshot.go index 68b7d00..663656e 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -1019,7 +1019,7 @@ func (v *Vaultik) VerifySnapshotWithOptions( v.printlnStdout() // Check each blob is present with the size the manifest records. - v.stdoutf("Checking blob presence and sizes...\n") + v.UI.Beginf("Checking blob presence and sizes...") } v.checkSnapshotDatabase(remoteKey, result) diff --git a/internal/vaultik/verify.go b/internal/vaultik/verify.go index 6c88d93..ba6c2d3 100644 --- a/internal/vaultik/verify.go +++ b/internal/vaultik/verify.go @@ -190,7 +190,7 @@ func (v *Vaultik) loadVerificationData( log.Info("Downloading manifest", "remote_key", remoteKey) if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Downloading manifest...\n") + v.UI.Beginf("Downloading manifest...") } manifest, err := v.downloadManifestByKey(remoteKey) @@ -205,9 +205,9 @@ func (v *Vaultik) loadVerificationData( "manifest_total_size", ubytes(manifest.TotalCompressedSize)) if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Manifest loaded: %d blobs (%s)\n", + v.UI.Completef("Manifest loaded: %d blobs (%s)", manifest.BlobCount, ubytes(manifest.TotalCompressedSize)) - v.stdoutf("Downloading and decrypting database...\n") + v.UI.Beginf("Downloading and decrypting database...") } tdb, err := v.downloadVerifiedSnapshotDB( @@ -235,7 +235,7 @@ func (v *Vaultik) loadVerificationData( "db_total_size", ubytes(dbTotalSize)) if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Database loaded: %d blobs (%s)\n", + v.UI.Completef("Database loaded: %d blobs (%s)", len(dbBlobs), ubytes(dbTotalSize)) } @@ -295,7 +295,7 @@ func (v *Vaultik) runVerificationSteps( identities []age.Identity, ) error { if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Verifying manifest against database...\n") + v.UI.Beginf("Verifying manifest against database...") } err := v.verifyManifestAgainstDatabase(manifest, dbBlobs) @@ -304,8 +304,8 @@ func (v *Vaultik) runVerificationSteps( } if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("Manifest verified.\n") - v.stdoutf("Checking blob existence in remote storage...\n") + v.UI.Completef("Manifest verified.") + v.UI.Beginf("Checking blob existence in remote storage...") } err = v.verifyBlobExistenceFromDB(dbBlobs, result) @@ -314,8 +314,8 @@ func (v *Vaultik) runVerificationSteps( } if !opts.JSON && !v.UI.Quiet() { - v.stdoutf("All blobs exist.\n") - v.stdoutf("Downloading and verifying blob contents (%d blobs, %s)...\n", + v.UI.Completef("All blobs exist.") + v.UI.Beginf("Downloading and verifying blob contents (%d blobs, %s)...", len(dbBlobs), ubytes(totalSize)) } @@ -790,7 +790,7 @@ func (v *Vaultik) performDeepVerificationFromDB( ) if !opts.JSON && !v.UI.Quiet() { - v.stdoutf(" Verified %d/%d blobs (%d remaining) - %s/%s - elapsed %s, eta %s\n", + v.UI.Progressf("Verified %d/%d blobs (%d remaining) - %s/%s - elapsed %s, eta %s", i+1, len(blobs), remaining, ubytes(bytesProcessed), ubytes(totalBytesExpected), diff --git a/internal/vaultik/verify_quiet_test.go b/internal/vaultik/verify_quiet_test.go index c8dd630..bc3b26e 100644 --- a/internal/vaultik/verify_quiet_test.go +++ b/internal/vaultik/verify_quiet_test.go @@ -100,3 +100,51 @@ func TestVerify_QuietSuppressesReport(t *testing.T) { require.Empty(t, stdout.String(), "a failed verify must write no report under --quiet") } + +// TestVerify_DeepStatusLineUsesUIMarker checks that the deep verify +// status lines go through internal/ui: marker-prefixed, and silenced by +// --quiet. +func TestVerify_DeepStatusLineUsesUIMarker(t *testing.T) { + log.Initialize(log.Config{}) + t.Parallel() + + fs := afero.NewOsFs() + tempDir := t.TempDir() + + dataDir := filepath.Join(tempDir, "source") + storeDir := t.TempDir() + dbPath := filepath.Join(tempDir, "index.sqlite") + + require.NoError(t, fs.MkdirAll(dataDir, 0o755)) + require.NoError(t, afero.WriteFile(fs, + filepath.Join(dataDir, "data.bin"), + bytesPattern("marker-", 64*1024), 0o644)) + + ctx := context.Background() + + cfg, storer, snapshotID := runFileStorageBackup( + ctx, t, fs, dataDir, storeDir, dbPath, 32*1024, 128*1024) + + var stdout bytes.Buffer + + v := &vaultik.Vaultik{ + Config: cfg, + Storage: storer, + Fs: fs, + Stdout: &stdout, + Stderr: io.Discard, + UI: ui.NewWithColor(&stdout, false), + } + v.SetContext(ctx) + + opts := &vaultik.VerifyOptions{Deep: true} + + require.NoError(t, v.VerifySnapshotWithOptions(snapshotID, opts)) + require.Contains(t, stdout.String(), + "\n"+ui.Marker+" Downloading manifest...\n") + + stdout.Reset() + v.UI.SetQuiet(true) + require.NoError(t, v.VerifySnapshotWithOptions(snapshotID, opts)) + require.NotContains(t, stdout.String(), "Downloading manifest") +}