diff --git a/internal/vaultik/info.go b/internal/vaultik/info.go index 6202345..53cfc2c 100644 --- a/internal/vaultik/info.go +++ b/internal/vaultik/info.go @@ -15,99 +15,99 @@ import ( // ShowInfo displays system and configuration information func (v *Vaultik) ShowInfo() error { // System Information - _, _ = fmt.Fprintf(v.Stdout, "=== System Information ===\n") - _, _ = fmt.Fprintf(v.Stdout, "OS/Architecture: %s/%s\n", runtime.GOOS, runtime.GOARCH) - _, _ = fmt.Fprintf(v.Stdout, "Version: %s\n", v.Globals.Version) - _, _ = fmt.Fprintf(v.Stdout, "Commit: %s\n", v.Globals.Commit) - _, _ = fmt.Fprintf(v.Stdout, "Go Version: %s\n", runtime.Version()) - _, _ = fmt.Fprintln(v.Stdout, ) + v.printfStdout("=== System Information ===\n") + v.printfStdout("OS/Architecture: %s/%s\n", runtime.GOOS, runtime.GOARCH) + v.printfStdout("Version: %s\n", v.Globals.Version) + v.printfStdout("Commit: %s\n", v.Globals.Commit) + v.printfStdout("Go Version: %s\n", runtime.Version()) + v.printlnStdout() // Storage Configuration - _, _ = fmt.Fprintf(v.Stdout, "=== Storage Configuration ===\n") - _, _ = fmt.Fprintf(v.Stdout, "S3 Bucket: %s\n", v.Config.S3.Bucket) + v.printfStdout("=== Storage Configuration ===\n") + v.printfStdout("S3 Bucket: %s\n", v.Config.S3.Bucket) if v.Config.S3.Prefix != "" { - _, _ = fmt.Fprintf(v.Stdout, "S3 Prefix: %s\n", v.Config.S3.Prefix) + v.printfStdout("S3 Prefix: %s\n", v.Config.S3.Prefix) } - _, _ = fmt.Fprintf(v.Stdout, "S3 Endpoint: %s\n", v.Config.S3.Endpoint) - _, _ = fmt.Fprintf(v.Stdout, "S3 Region: %s\n", v.Config.S3.Region) - _, _ = fmt.Fprintln(v.Stdout, ) + v.printfStdout("S3 Endpoint: %s\n", v.Config.S3.Endpoint) + v.printfStdout("S3 Region: %s\n", v.Config.S3.Region) + v.printlnStdout() // Backup Settings - _, _ = fmt.Fprintf(v.Stdout, "=== Backup Settings ===\n") + v.printfStdout("=== Backup Settings ===\n") // Show configured snapshots - _, _ = fmt.Fprintf(v.Stdout, "Snapshots:\n") + v.printfStdout("Snapshots:\n") for _, name := range v.Config.SnapshotNames() { snap := v.Config.Snapshots[name] - _, _ = fmt.Fprintf(v.Stdout, " %s:\n", name) + v.printfStdout(" %s:\n", name) for _, path := range snap.Paths { - _, _ = fmt.Fprintf(v.Stdout, " - %s\n", path) + v.printfStdout(" - %s\n", path) } if len(snap.Exclude) > 0 { - _, _ = fmt.Fprintf(v.Stdout, " exclude: %s\n", strings.Join(snap.Exclude, ", ")) + v.printfStdout(" exclude: %s\n", strings.Join(snap.Exclude, ", ")) } } // Global exclude patterns if len(v.Config.Exclude) > 0 { - _, _ = fmt.Fprintf(v.Stdout, "Global Exclude: %s\n", strings.Join(v.Config.Exclude, ", ")) + v.printfStdout("Global Exclude: %s\n", strings.Join(v.Config.Exclude, ", ")) } - _, _ = fmt.Fprintf(v.Stdout, "Compression: zstd level %d\n", v.Config.CompressionLevel) - _, _ = fmt.Fprintf(v.Stdout, "Chunk Size: %s\n", humanize.Bytes(uint64(v.Config.ChunkSize))) - _, _ = fmt.Fprintf(v.Stdout, "Blob Size Limit: %s\n", humanize.Bytes(uint64(v.Config.BlobSizeLimit))) - _, _ = fmt.Fprintln(v.Stdout, ) + v.printfStdout("Compression: zstd level %d\n", v.Config.CompressionLevel) + v.printfStdout("Chunk Size: %s\n", humanize.Bytes(uint64(v.Config.ChunkSize))) + v.printfStdout("Blob Size Limit: %s\n", humanize.Bytes(uint64(v.Config.BlobSizeLimit))) + v.printlnStdout() // Encryption Configuration - _, _ = fmt.Fprintf(v.Stdout, "=== Encryption Configuration ===\n") - _, _ = fmt.Fprintf(v.Stdout, "Recipients:\n") + v.printfStdout("=== Encryption Configuration ===\n") + v.printfStdout("Recipients:\n") for _, recipient := range v.Config.AgeRecipients { - _, _ = fmt.Fprintf(v.Stdout, " - %s\n", recipient) + v.printfStdout(" - %s\n", recipient) } - _, _ = fmt.Fprintln(v.Stdout, ) + v.printlnStdout() // Daemon Settings (if applicable) if v.Config.BackupInterval > 0 || v.Config.MinTimeBetweenRun > 0 { - _, _ = fmt.Fprintf(v.Stdout, "=== Daemon Settings ===\n") + v.printfStdout("=== Daemon Settings ===\n") if v.Config.BackupInterval > 0 { - _, _ = fmt.Fprintf(v.Stdout, "Backup Interval: %s\n", v.Config.BackupInterval) + v.printfStdout("Backup Interval: %s\n", v.Config.BackupInterval) } if v.Config.MinTimeBetweenRun > 0 { - _, _ = fmt.Fprintf(v.Stdout, "Minimum Time: %s\n", v.Config.MinTimeBetweenRun) + v.printfStdout("Minimum Time: %s\n", v.Config.MinTimeBetweenRun) } - _, _ = fmt.Fprintln(v.Stdout, ) + v.printlnStdout() } // Local Database - _, _ = fmt.Fprintf(v.Stdout, "=== Local Database ===\n") - _, _ = fmt.Fprintf(v.Stdout, "Index Path: %s\n", v.Config.IndexPath) + v.printfStdout("=== Local Database ===\n") + v.printfStdout("Index Path: %s\n", v.Config.IndexPath) // Check if index file exists and get its size if info, err := v.Fs.Stat(v.Config.IndexPath); err == nil { - _, _ = fmt.Fprintf(v.Stdout, "Index Size: %s\n", humanize.Bytes(uint64(info.Size()))) + v.printfStdout("Index Size: %s\n", humanize.Bytes(uint64(info.Size()))) // Get snapshot count from database query := `SELECT COUNT(*) FROM snapshots WHERE completed_at IS NOT NULL` var snapshotCount int if err := v.DB.Conn().QueryRowContext(v.ctx, query).Scan(&snapshotCount); err == nil { - _, _ = fmt.Fprintf(v.Stdout, "Snapshots: %d\n", snapshotCount) + v.printfStdout("Snapshots: %d\n", snapshotCount) } // Get blob count from database query = `SELECT COUNT(*) FROM blobs` var blobCount int if err := v.DB.Conn().QueryRowContext(v.ctx, query).Scan(&blobCount); err == nil { - _, _ = fmt.Fprintf(v.Stdout, "Blobs: %d\n", blobCount) + v.printfStdout("Blobs: %d\n", blobCount) } // Get file count from database query = `SELECT COUNT(*) FROM files` var fileCount int if err := v.DB.Conn().QueryRowContext(v.ctx, query).Scan(&fileCount); err == nil { - _, _ = fmt.Fprintf(v.Stdout, "Files: %d\n", fileCount) + v.printfStdout("Files: %d\n", fileCount) } } else { - _, _ = fmt.Fprintf(v.Stdout, "Index Size: (not created)\n") + v.printfStdout("Index Size: (not created)\n") } return nil @@ -157,15 +157,15 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { result.StorageLocation = storageInfo.Location if !jsonOutput { - _, _ = fmt.Fprintf(v.Stdout, "=== Remote Storage ===\n") - _, _ = fmt.Fprintf(v.Stdout, "Type: %s\n", storageInfo.Type) - _, _ = fmt.Fprintf(v.Stdout, "Location: %s\n", storageInfo.Location) - _, _ = fmt.Fprintln(v.Stdout, ) + v.printfStdout("=== Remote Storage ===\n") + v.printfStdout("Type: %s\n", storageInfo.Type) + v.printfStdout("Location: %s\n", storageInfo.Location) + v.printlnStdout() } // List all snapshot metadata if !jsonOutput { - _, _ = fmt.Fprintf(v.Stdout, "Scanning snapshot metadata...\n") + v.printfStdout("Scanning snapshot metadata...\n") } snapshotMetadata := make(map[string]*SnapshotMetadataInfo) @@ -210,7 +210,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { // Download and parse all manifests to get referenced blobs if !jsonOutput { - _, _ = fmt.Fprintf(v.Stdout, "Downloading %d manifest(s)...\n", len(snapshotIDs)) + v.printfStdout("Downloading %d manifest(s)...\n", len(snapshotIDs)) } referencedBlobs := make(map[string]int64) // hash -> compressed size @@ -260,7 +260,7 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { // List all blobs on remote if !jsonOutput { - _, _ = fmt.Fprintf(v.Stdout, "Scanning blobs...\n") + v.printfStdout("Scanning blobs...\n") } allBlobs := make(map[string]int64) // hash -> size from storage @@ -298,14 +298,14 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { } // Human-readable output - _, _ = fmt.Fprintf(v.Stdout, "\n=== Snapshot Metadata ===\n") + v.printfStdout("\n=== Snapshot Metadata ===\n") if len(result.Snapshots) == 0 { - _, _ = fmt.Fprintf(v.Stdout, "No snapshots found\n") + v.printfStdout("No snapshots found\n") } else { - _, _ = fmt.Fprintf(v.Stdout, "%-45s %12s %12s %12s %10s %12s\n", "SNAPSHOT", "MANIFEST", "DATABASE", "TOTAL", "BLOBS", "BLOB SIZE") - _, _ = fmt.Fprintf(v.Stdout, "%-45s %12s %12s %12s %10s %12s\n", strings.Repeat("-", 45), strings.Repeat("-", 12), strings.Repeat("-", 12), strings.Repeat("-", 12), strings.Repeat("-", 10), strings.Repeat("-", 12)) + v.printfStdout("%-45s %12s %12s %12s %10s %12s\n", "SNAPSHOT", "MANIFEST", "DATABASE", "TOTAL", "BLOBS", "BLOB SIZE") + v.printfStdout("%-45s %12s %12s %12s %10s %12s\n", strings.Repeat("-", 45), strings.Repeat("-", 12), strings.Repeat("-", 12), strings.Repeat("-", 12), strings.Repeat("-", 10), strings.Repeat("-", 12)) for _, info := range result.Snapshots { - _, _ = fmt.Fprintf(v.Stdout, "%-45s %12s %12s %12s %10s %12s\n", + v.printfStdout("%-45s %12s %12s %12s %10s %12s\n", truncateString(info.SnapshotID, 45), humanize.Bytes(uint64(info.ManifestSize)), humanize.Bytes(uint64(info.DatabaseSize)), @@ -314,23 +314,23 @@ func (v *Vaultik) RemoteInfo(jsonOutput bool) error { humanize.Bytes(uint64(info.BlobsSize)), ) } - _, _ = fmt.Fprintf(v.Stdout, "%-45s %12s %12s %12s %10s %12s\n", strings.Repeat("-", 45), strings.Repeat("-", 12), strings.Repeat("-", 12), strings.Repeat("-", 12), strings.Repeat("-", 10), strings.Repeat("-", 12)) - _, _ = fmt.Fprintf(v.Stdout, "%-45s %12s %12s %12s\n", fmt.Sprintf("Total (%d snapshots)", result.TotalMetadataCount), "", "", humanize.Bytes(uint64(result.TotalMetadataSize))) + v.printfStdout("%-45s %12s %12s %12s %10s %12s\n", strings.Repeat("-", 45), strings.Repeat("-", 12), strings.Repeat("-", 12), strings.Repeat("-", 12), strings.Repeat("-", 10), strings.Repeat("-", 12)) + v.printfStdout("%-45s %12s %12s %12s\n", fmt.Sprintf("Total (%d snapshots)", result.TotalMetadataCount), "", "", humanize.Bytes(uint64(result.TotalMetadataSize))) } - _, _ = fmt.Fprintf(v.Stdout, "\n=== Blob Storage ===\n") - _, _ = fmt.Fprintf(v.Stdout, "Total blobs on remote: %s (%s)\n", + v.printfStdout("\n=== Blob Storage ===\n") + v.printfStdout("Total blobs on remote: %s (%s)\n", humanize.Comma(int64(result.TotalBlobCount)), humanize.Bytes(uint64(result.TotalBlobSize))) - _, _ = fmt.Fprintf(v.Stdout, "Referenced by snapshots: %s (%s)\n", + v.printfStdout("Referenced by snapshots: %s (%s)\n", humanize.Comma(int64(result.ReferencedBlobCount)), humanize.Bytes(uint64(result.ReferencedBlobSize))) - _, _ = fmt.Fprintf(v.Stdout, "Orphaned (unreferenced): %s (%s)\n", + v.printfStdout("Orphaned (unreferenced): %s (%s)\n", humanize.Comma(int64(result.OrphanedBlobCount)), humanize.Bytes(uint64(result.OrphanedBlobSize))) if result.OrphanedBlobCount > 0 { - _, _ = fmt.Fprintf(v.Stdout, "\nRun 'vaultik prune --remote' to remove orphaned blobs.\n") + v.printfStdout("\nRun 'vaultik prune --remote' to remove orphaned blobs.\n") } return nil diff --git a/internal/vaultik/prune.go b/internal/vaultik/prune.go index 0f81801..dff9dd9 100644 --- a/internal/vaultik/prune.go +++ b/internal/vaultik/prune.go @@ -122,27 +122,27 @@ func (v *Vaultik) PruneBlobs(opts *PruneOptions) error { if opts.JSON { return v.outputPruneBlobsJSON(result) } - _, _ = fmt.Fprintln(v.Stdout, "No unreferenced blobs to remove.") + v.printlnStdout("No unreferenced blobs to remove.") return nil } // Show what will be deleted log.Info("Found unreferenced blobs", "count", len(unreferencedBlobs), "total_size", humanize.Bytes(uint64(totalSize))) if !opts.JSON { - _, _ = fmt.Fprintf(v.Stdout, "Found %d unreferenced blob(s) totaling %s\n", len(unreferencedBlobs), humanize.Bytes(uint64(totalSize))) + v.printfStdout("Found %d unreferenced blob(s) totaling %s\n", len(unreferencedBlobs), humanize.Bytes(uint64(totalSize))) } // Confirm unless --force is used (skip in JSON mode - require --force) if !opts.Force && !opts.JSON { - _, _ = fmt.Fprintf(v.Stdout, "\nDelete %d unreferenced blob(s)? [y/N] ", len(unreferencedBlobs)) + v.printfStdout("\nDelete %d unreferenced blob(s)? [y/N] ", len(unreferencedBlobs)) var confirm string - if _, err := fmt.Fscanln(v.Stdin, &confirm); err != nil { + if _, err := v.scanStdin(&confirm); err != nil { // Treat EOF or error as "no" - _, _ = fmt.Fprintln(v.Stdout, "Cancelled") + v.printlnStdout("Cancelled") return nil } if strings.ToLower(confirm) != "y" { - _, _ = fmt.Fprintln(v.Stdout, "Cancelled") + v.printlnStdout("Cancelled") return nil } } @@ -187,9 +187,9 @@ func (v *Vaultik) PruneBlobs(opts *PruneOptions) error { return v.outputPruneBlobsJSON(result) } - _, _ = fmt.Fprintf(v.Stdout, "\nDeleted %d blob(s) totaling %s\n", deletedCount, humanize.Bytes(uint64(deletedSize))) + v.printfStdout("\nDeleted %d blob(s) totaling %s\n", deletedCount, humanize.Bytes(uint64(deletedSize))) if deletedCount < len(unreferencedBlobs) { - _, _ = fmt.Fprintf(v.Stdout, "Failed to delete %d blob(s)\n", len(unreferencedBlobs)-deletedCount) + v.printfStdout("Failed to delete %d blob(s)\n", len(unreferencedBlobs)-deletedCount) } return nil diff --git a/internal/vaultik/vaultik.go b/internal/vaultik/vaultik.go index 7fe5a3b..4ad2c77 100644 --- a/internal/vaultik/vaultik.go +++ b/internal/vaultik/vaultik.go @@ -129,17 +129,12 @@ func (v *Vaultik) GetFilesystem() afero.Fs { return v.Fs } -// Outputf writes formatted output to stdout for user-facing messages. -// This should be used for all non-log user output. -func (v *Vaultik) Outputf(format string, args ...any) { - _, _ = fmt.Fprintf(v.Stdout, format, args...) -} - -// printfStdout writes formatted output to stdout. +// printfStdout writes formatted output to stdout for user-facing messages. func (v *Vaultik) printfStdout(format string, args ...any) { _, _ = fmt.Fprintf(v.Stdout, format, args...) } + // printlnStdout writes a line to stdout. func (v *Vaultik) printlnStdout(args ...any) { _, _ = fmt.Fprintln(v.Stdout, args...) @@ -163,6 +158,16 @@ func (v *Vaultik) FetchBlob(ctx context.Context, blobHash string, expectedSize i return reader, expectedSize, nil } +// printfStderr writes formatted output to stderr. +func (v *Vaultik) printfStderr(format string, args ...any) { + _, _ = fmt.Fprintf(v.Stderr, format, args...) +} + +// scanStdin reads a line of input from stdin. +func (v *Vaultik) scanStdin(a ...any) (int, error) { + return fmt.Fscanln(v.Stdin, a...) +} + // TestVaultik wraps a Vaultik with captured stdout/stderr for testing type TestVaultik struct { *Vaultik diff --git a/internal/vaultik/verify.go b/internal/vaultik/verify.go index 3c793db..55213ef 100644 --- a/internal/vaultik/verify.go +++ b/internal/vaultik/verify.go @@ -58,14 +58,14 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error { ) if !opts.JSON { - v.Outputf("Deep verification of snapshot: %s\n\n", snapshotID) + v.printfStdout("Deep verification of snapshot: %s\n\n", snapshotID) } // Step 1: Download manifest manifestPath := fmt.Sprintf("metadata/%s/manifest.json.zst", snapshotID) log.Info("Downloading manifest", "path", manifestPath) if !opts.JSON { - v.Outputf("Downloading manifest...\n") + v.printfStdout("Downloading manifest...\n") } manifestReader, err := v.Storage.Get(v.ctx, manifestPath) @@ -95,14 +95,14 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error { "manifest_total_size", humanize.Bytes(uint64(manifest.TotalCompressedSize)), ) if !opts.JSON { - v.Outputf("Manifest loaded: %d blobs (%s)\n", manifest.BlobCount, humanize.Bytes(uint64(manifest.TotalCompressedSize))) + v.printfStdout("Manifest loaded: %d blobs (%s)\n", manifest.BlobCount, humanize.Bytes(uint64(manifest.TotalCompressedSize))) } // Step 2: Download and decrypt database (authoritative source) dbPath := fmt.Sprintf("metadata/%s/db.zst.age", snapshotID) log.Info("Downloading encrypted database", "path", dbPath) if !opts.JSON { - v.Outputf("Downloading and decrypting database...\n") + v.printfStdout("Downloading and decrypting database...\n") } dbReader, err := v.Storage.Get(v.ctx, dbPath) @@ -155,8 +155,8 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error { "db_total_size", humanize.Bytes(uint64(totalSize)), ) if !opts.JSON { - v.Outputf("Database loaded: %d blobs (%s)\n", len(dbBlobs), humanize.Bytes(uint64(totalSize))) - v.Outputf("Verifying manifest against database...\n") + v.printfStdout("Database loaded: %d blobs (%s)\n", len(dbBlobs), humanize.Bytes(uint64(totalSize))) + v.printfStdout("Verifying manifest against database...\n") } // Step 4: Verify manifest matches database @@ -171,8 +171,8 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error { // Step 5: Verify all blobs exist in S3 (using database as source) if !opts.JSON { - v.Outputf("Manifest verified.\n") - v.Outputf("Checking blob existence in remote storage...\n") + v.printfStdout("Manifest verified.\n") + v.printfStdout("Checking blob existence in remote storage...\n") } if err := v.verifyBlobExistenceFromDB(dbBlobs); err != nil { result.Status = "failed" @@ -185,8 +185,8 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error { // Step 6: Deep verification - download and verify blob contents if !opts.JSON { - v.Outputf("All blobs exist.\n") - v.Outputf("Downloading and verifying blob contents (%d blobs, %s)...\n", len(dbBlobs), humanize.Bytes(uint64(totalSize))) + v.printfStdout("All blobs exist.\n") + v.printfStdout("Downloading and verifying blob contents (%d blobs, %s)...\n", len(dbBlobs), humanize.Bytes(uint64(totalSize))) } if err := v.performDeepVerificationFromDB(dbBlobs, tempDB.DB, opts); err != nil { result.Status = "failed" @@ -211,10 +211,10 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error { "blobs_verified", len(dbBlobs), ) - v.Outputf("\n✓ Verification completed successfully\n") - v.Outputf(" Snapshot: %s\n", snapshotID) - v.Outputf(" Blobs verified: %d\n", len(dbBlobs)) - v.Outputf(" Total size: %s\n", humanize.Bytes(uint64(totalSize))) + v.printfStdout("\n✓ Verification completed successfully\n") + v.printfStdout(" Snapshot: %s\n", snapshotID) + v.printfStdout(" Blobs verified: %d\n", len(dbBlobs)) + v.printfStdout(" Total size: %s\n", humanize.Bytes(uint64(totalSize))) return nil } @@ -569,7 +569,7 @@ func (v *Vaultik) performDeepVerificationFromDB(blobs []snapshot.BlobInfo, db *s ) if !opts.JSON { - v.Outputf(" Verified %d/%d blobs (%d remaining) - %s/%s - elapsed %s, eta %s\n", + v.printfStdout(" Verified %d/%d blobs (%d remaining) - %s/%s - elapsed %s, eta %s\n", i+1, len(blobs), remaining, humanize.Bytes(uint64(bytesProcessed)), humanize.Bytes(uint64(totalBytesExpected)),