From f8945006d5d365eca772da935d000e0821ce210a Mon Sep 17 00:00:00 2001 From: user Date: Sun, 15 Feb 2026 21:20:45 -0800 Subject: [PATCH] refactor: add helper wrappers for stdin/stdout/stderr IO Address all four review concerns on PR #31: 1. Fix missed bare fmt.Println() in VerifySnapshotWithOptions (line 620) 2. Replace all direct fmt.Fprintf(v.Stdout,...) / fmt.Fprintln(v.Stdout,...) / fmt.Fscanln(v.Stdin,...) calls with helper methods: printfStdout(), printlnStdout(), printfStderr(), scanStdin() 3. Route progress bar and stderr output through v.Stderr instead of os.Stderr in restore.go (concern #4: v.Stderr now actually used) 4. Rename exported Outputf to unexported printfStdout (YAGNI: only helpers actually used are created) --- internal/vaultik/info.go | 112 ++++++++++++++--------------- internal/vaultik/prune.go | 16 ++--- internal/vaultik/snapshot.go | 132 +++++++++++++++++------------------ internal/vaultik/vaultik.go | 19 ++++- internal/vaultik/verify.go | 30 ++++---- 5 files changed, 161 insertions(+), 148 deletions(-) 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/snapshot.go b/internal/vaultik/snapshot.go index c013fd2..3b66c57 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -86,7 +86,7 @@ func (v *Vaultik) CreateSnapshot(opts *SnapshotCreateOptions) error { // Print overall summary if multiple snapshots if len(snapshotNames) > 1 { - _, _ = fmt.Fprintf(v.Stdout, "\nAll %d snapshots completed in %s\n", len(snapshotNames), time.Since(overallStartTime).Round(time.Second)) + v.printfStdout("\nAll %d snapshots completed in %s\n", len(snapshotNames), time.Since(overallStartTime).Round(time.Second)) } return nil @@ -99,7 +99,7 @@ func (v *Vaultik) createNamedSnapshot(opts *SnapshotCreateOptions, hostname, sna snapConfig := v.Config.Snapshots[snapName] if total > 1 { - _, _ = fmt.Fprintf(v.Stdout, "\n=== Snapshot %d/%d: %s ===\n", idx, total, snapName) + v.printfStdout("\n=== Snapshot %d/%d: %s ===\n", idx, total, snapName) } // Resolve source directories to absolute paths @@ -152,7 +152,7 @@ func (v *Vaultik) createNamedSnapshot(opts *SnapshotCreateOptions, hostname, sna return fmt.Errorf("creating snapshot: %w", err) } log.Info("Beginning snapshot", "snapshot_id", snapshotID, "name", snapName) - _, _ = fmt.Fprintf(v.Stdout, "Beginning snapshot: %s\n", snapshotID) + v.printfStdout("Beginning snapshot: %s\n", snapshotID) for i, dir := range resolvedDirs { // Check if context is cancelled @@ -164,7 +164,7 @@ func (v *Vaultik) createNamedSnapshot(opts *SnapshotCreateOptions, hostname, sna } log.Info("Scanning directory", "path", dir) - _, _ = fmt.Fprintf(v.Stdout, "Beginning directory scan (%d/%d): %s\n", i+1, len(resolvedDirs), dir) + v.printfStdout("Beginning directory scan (%d/%d): %s\n", i+1, len(resolvedDirs), dir) result, err := scanner.Scan(v.ctx, dir, snapshotID) if err != nil { return fmt.Errorf("failed to scan %s: %w", dir, err) @@ -275,35 +275,35 @@ func (v *Vaultik) createNamedSnapshot(opts *SnapshotCreateOptions, hostname, sna } // Print comprehensive summary - _, _ = fmt.Fprintf(v.Stdout, "=== Snapshot Complete ===\n") - _, _ = fmt.Fprintf(v.Stdout, "ID: %s\n", snapshotID) - _, _ = fmt.Fprintf(v.Stdout, "Files: %s examined, %s to process, %s unchanged", + v.printfStdout("=== Snapshot Complete ===\n") + v.printfStdout("ID: %s\n", snapshotID) + v.printfStdout("Files: %s examined, %s to process, %s unchanged", formatNumber(totalFiles), formatNumber(totalFilesChanged), formatNumber(totalFilesSkipped)) if totalFilesDeleted > 0 { - _, _ = fmt.Fprintf(v.Stdout, ", %s deleted", formatNumber(totalFilesDeleted)) + v.printfStdout(", %s deleted", formatNumber(totalFilesDeleted)) } - _, _ = fmt.Fprintln(v.Stdout) - _, _ = fmt.Fprintf(v.Stdout, "Data: %s total (%s to process)", + v.printlnStdout() + v.printfStdout("Data: %s total (%s to process)", humanize.Bytes(uint64(totalBytesAll)), humanize.Bytes(uint64(totalBytesChanged))) if totalBytesDeleted > 0 { - _, _ = fmt.Fprintf(v.Stdout, ", %s deleted", humanize.Bytes(uint64(totalBytesDeleted))) + v.printfStdout(", %s deleted", humanize.Bytes(uint64(totalBytesDeleted))) } - _, _ = fmt.Fprintln(v.Stdout) + v.printlnStdout() if totalBlobsUploaded > 0 { - _, _ = fmt.Fprintf(v.Stdout, "Storage: %s compressed from %s (%.2fx)\n", + v.printfStdout("Storage: %s compressed from %s (%.2fx)\n", humanize.Bytes(uint64(totalBlobSizeCompressed)), humanize.Bytes(uint64(totalBlobSizeUncompressed)), compressionRatio) - _, _ = fmt.Fprintf(v.Stdout, "Upload: %d blobs, %s in %s (%s)\n", + v.printfStdout("Upload: %d blobs, %s in %s (%s)\n", totalBlobsUploaded, humanize.Bytes(uint64(totalBytesUploaded)), formatDuration(uploadDuration), avgUploadSpeed) } - _, _ = fmt.Fprintf(v.Stdout, "Duration: %s\n", formatDuration(snapshotDuration)) + v.printfStdout("Duration: %s\n", formatDuration(snapshotDuration)) if opts.Prune { log.Info("Pruning enabled - will delete old snapshots after snapshot") @@ -527,14 +527,14 @@ func (v *Vaultik) PurgeSnapshots(keepLatest bool, olderThan string, force bool) } if len(toDelete) == 0 { - _, _ = fmt.Fprintln(v.Stdout, "No snapshots to delete") + v.printlnStdout("No snapshots to delete") return nil } // Show what will be deleted - _, _ = fmt.Fprintf(v.Stdout, "The following snapshots will be deleted:\n\n") + v.printfStdout("The following snapshots will be deleted:\n\n") for _, snap := range toDelete { - _, _ = fmt.Fprintf(v.Stdout, " %s (%s, %s)\n", + v.printfStdout(" %s (%s, %s)\n", snap.ID, snap.Timestamp.Format("2006-01-02 15:04:05"), formatBytes(snap.CompressedSize)) @@ -542,19 +542,19 @@ func (v *Vaultik) PurgeSnapshots(keepLatest bool, olderThan string, force bool) // Confirm unless --force is used if !force { - _, _ = fmt.Fprintf(v.Stdout, "\nDelete %d snapshot(s)? [y/N] ", len(toDelete)) + v.printfStdout("\nDelete %d snapshot(s)? [y/N] ", len(toDelete)) 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 } } else { - _, _ = fmt.Fprintf(v.Stdout, "\nDeleting %d snapshot(s) (--force specified)\n", len(toDelete)) + v.printfStdout("\nDeleting %d snapshot(s) (--force specified)\n", len(toDelete)) } // Delete snapshots (both local and remote) @@ -569,10 +569,10 @@ func (v *Vaultik) PurgeSnapshots(keepLatest bool, olderThan string, force bool) } } - _, _ = fmt.Fprintf(v.Stdout, "Deleted %d snapshot(s)\n", len(toDelete)) + v.printfStdout("Deleted %d snapshot(s)\n", len(toDelete)) // Note: Run 'vaultik prune' separately to clean up unreferenced blobs - _, _ = fmt.Fprintln(v.Stdout, "\nNote: Run 'vaultik prune' to clean up unreferenced blobs.") + v.printlnStdout("\nNote: Run 'vaultik prune' to clean up unreferenced blobs.") return nil } @@ -613,11 +613,11 @@ func (v *Vaultik) VerifySnapshotWithOptions(snapshotID string, opts *VerifyOptio } if !opts.JSON { - _, _ = fmt.Fprintf(v.Stdout, "Verifying snapshot %s\n", snapshotID) + v.printfStdout("Verifying snapshot %s\n", snapshotID) if !snapshotTime.IsZero() { - _, _ = fmt.Fprintf(v.Stdout, "Snapshot time: %s\n", snapshotTime.Format("2006-01-02 15:04:05 MST")) + v.printfStdout("Snapshot time: %s\n", snapshotTime.Format("2006-01-02 15:04:05 MST")) } - fmt.Println() + v.printlnStdout() } // Download and parse manifest @@ -635,18 +635,18 @@ func (v *Vaultik) VerifySnapshotWithOptions(snapshotID string, opts *VerifyOptio result.TotalSize = manifest.TotalCompressedSize if !opts.JSON { - _, _ = fmt.Fprintf(v.Stdout, "Snapshot information:\n") - _, _ = fmt.Fprintf(v.Stdout, " Blob count: %d\n", manifest.BlobCount) - _, _ = fmt.Fprintf(v.Stdout, " Total size: %s\n", humanize.Bytes(uint64(manifest.TotalCompressedSize))) + v.printfStdout("Snapshot information:\n") + v.printfStdout(" Blob count: %d\n", manifest.BlobCount) + v.printfStdout(" Total size: %s\n", humanize.Bytes(uint64(manifest.TotalCompressedSize))) if manifest.Timestamp != "" { if t, err := time.Parse(time.RFC3339, manifest.Timestamp); err == nil { - _, _ = fmt.Fprintf(v.Stdout, " Created: %s\n", t.Format("2006-01-02 15:04:05 MST")) + v.printfStdout(" Created: %s\n", t.Format("2006-01-02 15:04:05 MST")) } } - _, _ = fmt.Fprintln(v.Stdout) + v.printlnStdout() // Check each blob exists - _, _ = fmt.Fprintf(v.Stdout, "Checking blob existence...\n") + v.printfStdout("Checking blob existence...\n") } missing := 0 @@ -660,7 +660,7 @@ func (v *Vaultik) VerifySnapshotWithOptions(snapshotID string, opts *VerifyOptio _, err := v.Storage.Stat(v.ctx, blobPath) if err != nil { if !opts.JSON { - _, _ = fmt.Fprintf(v.Stdout, " Missing: %s (%s)\n", blob.Hash, humanize.Bytes(uint64(blob.CompressedSize))) + v.printfStdout(" Missing: %s (%s)\n", blob.Hash, humanize.Bytes(uint64(blob.CompressedSize))) } missing++ missingSize += blob.CompressedSize @@ -683,20 +683,20 @@ func (v *Vaultik) VerifySnapshotWithOptions(snapshotID string, opts *VerifyOptio return v.outputVerifyJSON(result) } - _, _ = fmt.Fprintf(v.Stdout, "\nVerification complete:\n") - _, _ = fmt.Fprintf(v.Stdout, " Verified: %d blobs (%s)\n", verified, + v.printfStdout("\nVerification complete:\n") + v.printfStdout(" Verified: %d blobs (%s)\n", verified, humanize.Bytes(uint64(manifest.TotalCompressedSize-missingSize))) if missing > 0 { - _, _ = fmt.Fprintf(v.Stdout, " Missing: %d blobs (%s)\n", missing, humanize.Bytes(uint64(missingSize))) + v.printfStdout(" Missing: %d blobs (%s)\n", missing, humanize.Bytes(uint64(missingSize))) } else { - _, _ = fmt.Fprintf(v.Stdout, " Missing: 0 blobs\n") + v.printfStdout(" Missing: 0 blobs\n") } - _, _ = fmt.Fprintf(v.Stdout, " Status: ") + v.printfStdout(" Status: ") if missing > 0 { - _, _ = fmt.Fprintf(v.Stdout, "FAILED - %d blobs are missing\n", missing) + v.printfStdout("FAILED - %d blobs are missing\n", missing) return fmt.Errorf("%d blobs are missing", missing) } else { - _, _ = fmt.Fprintf(v.Stdout, "OK - All blobs verified\n") + v.printfStdout("OK - All blobs verified\n") } return nil @@ -830,11 +830,11 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov if opts.DryRun { result.DryRun = true if !opts.JSON { - _, _ = fmt.Fprintf(v.Stdout, "Would remove snapshot: %s\n", snapshotID) + v.printfStdout("Would remove snapshot: %s\n", snapshotID) if opts.Remote { - _, _ = fmt.Fprintln(v.Stdout, "Would also remove from remote storage") + v.printlnStdout("Would also remove from remote storage") } - _, _ = fmt.Fprintln(v.Stdout, "[Dry run - no changes made]") + v.printlnStdout("[Dry run - no changes made]") } if opts.JSON { return result, v.outputRemoveJSON(result) @@ -845,17 +845,17 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov // Confirm unless --force is used (skip in JSON mode - require --force) if !opts.Force && !opts.JSON { if opts.Remote { - _, _ = fmt.Fprintf(v.Stdout, "Remove snapshot '%s' from local database and remote storage? [y/N] ", snapshotID) + v.printfStdout("Remove snapshot '%s' from local database and remote storage? [y/N] ", snapshotID) } else { - _, _ = fmt.Fprintf(v.Stdout, "Remove snapshot '%s' from local database? [y/N] ", snapshotID) + v.printfStdout("Remove snapshot '%s' from local database? [y/N] ", snapshotID) } var confirm string - if _, err := fmt.Fscanln(v.Stdin, &confirm); err != nil { - _, _ = fmt.Fprintln(v.Stdout, "Cancelled") + if _, err := v.scanStdin(&confirm); err != nil { + v.printlnStdout("Cancelled") return result, nil } if strings.ToLower(confirm) != "y" { - _, _ = fmt.Fprintln(v.Stdout, "Cancelled") + v.printlnStdout("Cancelled") return result, nil } } @@ -882,10 +882,10 @@ func (v *Vaultik) RemoveSnapshot(snapshotID string, opts *RemoveOptions) (*Remov } // Print summary - _, _ = fmt.Fprintf(v.Stdout, "Removed snapshot '%s' from local database\n", snapshotID) + v.printfStdout("Removed snapshot '%s' from local database\n", snapshotID) if opts.Remote { - _, _ = fmt.Fprintln(v.Stdout, "Removed snapshot metadata from remote storage") - _, _ = fmt.Fprintln(v.Stdout, "\nNote: Blobs were not removed. Run 'vaultik prune' to remove orphaned blobs.") + v.printlnStdout("Removed snapshot metadata from remote storage") + v.printlnStdout("\nNote: Blobs were not removed. Run 'vaultik prune' to remove orphaned blobs.") } return result, nil @@ -929,7 +929,7 @@ func (v *Vaultik) RemoveAllSnapshots(opts *RemoveOptions) (*RemoveResult, error) if len(snapshotIDs) == 0 { if !opts.JSON { - _, _ = fmt.Fprintln(v.Stdout, "No snapshots found") + v.printlnStdout("No snapshots found") } return result, nil } @@ -938,14 +938,14 @@ func (v *Vaultik) RemoveAllSnapshots(opts *RemoveOptions) (*RemoveResult, error) result.DryRun = true result.SnapshotsRemoved = snapshotIDs if !opts.JSON { - _, _ = fmt.Fprintf(v.Stdout, "Would remove %d snapshot(s):\n", len(snapshotIDs)) + v.printfStdout("Would remove %d snapshot(s):\n", len(snapshotIDs)) for _, id := range snapshotIDs { - _, _ = fmt.Fprintf(v.Stdout, " %s\n", id) + v.printfStdout(" %s\n", id) } if opts.Remote { - _, _ = fmt.Fprintln(v.Stdout, "Would also remove from remote storage") + v.printlnStdout("Would also remove from remote storage") } - _, _ = fmt.Fprintln(v.Stdout, "[Dry run - no changes made]") + v.printlnStdout("[Dry run - no changes made]") } if opts.JSON { return result, v.outputRemoveJSON(result) @@ -986,10 +986,10 @@ func (v *Vaultik) RemoveAllSnapshots(opts *RemoveOptions) (*RemoveResult, error) return result, v.outputRemoveJSON(result) } - _, _ = fmt.Fprintf(v.Stdout, "Removed %d snapshot(s)\n", len(result.SnapshotsRemoved)) + v.printfStdout("Removed %d snapshot(s)\n", len(result.SnapshotsRemoved)) if opts.Remote { - _, _ = fmt.Fprintln(v.Stdout, "Removed snapshot metadata from remote storage") - _, _ = fmt.Fprintln(v.Stdout, "\nNote: Blobs were not removed. Run 'vaultik prune' to remove orphaned blobs.") + v.printlnStdout("Removed snapshot metadata from remote storage") + v.printlnStdout("\nNote: Blobs were not removed. Run 'vaultik prune' to remove orphaned blobs.") } return result, nil @@ -1117,11 +1117,11 @@ func (v *Vaultik) PruneDatabase() (*PruneResult, error) { ) // Print summary - _, _ = fmt.Fprintf(v.Stdout, "Local database prune complete:\n") - _, _ = fmt.Fprintf(v.Stdout, " Incomplete snapshots removed: %d\n", result.SnapshotsDeleted) - _, _ = fmt.Fprintf(v.Stdout, " Orphaned files removed: %d\n", result.FilesDeleted) - _, _ = fmt.Fprintf(v.Stdout, " Orphaned chunks removed: %d\n", result.ChunksDeleted) - _, _ = fmt.Fprintf(v.Stdout, " Orphaned blobs removed: %d\n", result.BlobsDeleted) + v.printfStdout("Local database prune complete:\n") + v.printfStdout(" Incomplete snapshots removed: %d\n", result.SnapshotsDeleted) + v.printfStdout(" Orphaned files removed: %d\n", result.FilesDeleted) + v.printfStdout(" Orphaned chunks removed: %d\n", result.ChunksDeleted) + v.printfStdout(" Orphaned blobs removed: %d\n", result.BlobsDeleted) return result, nil } diff --git a/internal/vaultik/vaultik.go b/internal/vaultik/vaultik.go index 4ce6535..25ef7ac 100644 --- a/internal/vaultik/vaultik.go +++ b/internal/vaultik/vaultik.go @@ -129,12 +129,25 @@ 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) { +// 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...) +} + +// 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)),