Introduce internal/ui package and rewrite user-facing output
All user-facing output now goes through a single ui.Writer with a
uniform style:
》 (white) for begin / info / notice
》 (green) for complete / success
Warning: for warnings (orange)
ERROR: for errors (red)
》 (indented) for progress heartbeats
Color is enabled when stdout is a TTY and NO_COLOR is unset.
Standards:
- Complete-sentence messages with fully qualified terms ("backup
destination store", "local index database", "snapshot source
files enumeration").
- Every Complete has a matching Begin.
- Natural verb tense conveys state ("Uploading" -> "Uploaded"). The
words "begin"/"complete" never appear in message bodies; the marker
color carries that information.
- ETA means clock time, not duration. Progress lines say "estimated
remaining time (<dur>), finish at <time>" with both labeled.
Adds globals.CommitDate (populated by Makefile/Dockerfile/goreleaser
via ldflags from `git show -s --format=%cI HEAD`) and a startup banner
printed once per invocation.
Strips fx call-chain noise from startup errors so users see the actual
underlying error (e.g. "creating base path: mkdir /Volumes/BACKUPS:
permission denied" instead of three layers of "could not build
arguments for function ...").
README documents the output style and the ui package conventions.
This commit is contained in:
@@ -83,7 +83,7 @@ func (v *Vaultik) CreateSnapshot(opts *SnapshotCreateOptions) error {
|
||||
|
||||
// Print overall summary if multiple snapshots
|
||||
if len(snapshotNames) > 1 {
|
||||
v.printfStdout("\nAll %d snapshots completed in %s\n", len(snapshotNames), time.Since(overallStartTime).Round(time.Second))
|
||||
v.UI.Complete("All %d snapshots completed in %s.", len(snapshotNames), v.UI.Duration(time.Since(overallStartTime)))
|
||||
}
|
||||
|
||||
if opts.Prune {
|
||||
@@ -101,7 +101,7 @@ func (v *Vaultik) CreateSnapshot(opts *SnapshotCreateOptions) error {
|
||||
// snapshot of each name is kept.
|
||||
func (v *Vaultik) runPostBackupPrune(snapshotNames []string, keepNewerThan string) error {
|
||||
log.Info("Running post-backup prune", "snapshots", snapshotNames, "keep_newer_than", keepNewerThan)
|
||||
v.printlnStdout("\n=== Post-backup prune ===")
|
||||
v.UI.Begin("Running post-backup prune.")
|
||||
|
||||
purgeOpts := &SnapshotPurgeOptions{
|
||||
Force: true,
|
||||
@@ -146,7 +146,7 @@ func (v *Vaultik) createNamedSnapshot(opts *SnapshotCreateOptions, hostname, sna
|
||||
snapshotStartTime := time.Now()
|
||||
|
||||
if total > 1 {
|
||||
v.printfStdout("\n=== Snapshot %d/%d: %s ===\n", idx, total, snapName)
|
||||
v.UI.Info("Snapshot %d/%d: %s.", idx, total, snapName)
|
||||
}
|
||||
|
||||
resolvedDirs, err := v.resolveSnapshotPaths(snapName)
|
||||
@@ -156,7 +156,7 @@ func (v *Vaultik) createNamedSnapshot(opts *SnapshotCreateOptions, hostname, sna
|
||||
|
||||
scanner := v.ScannerFactory(snapshot.ScannerParams{
|
||||
EnableProgress: !opts.Cron,
|
||||
Output: v.Stdout,
|
||||
UI: v.UI,
|
||||
Fs: v.Fs,
|
||||
Exclude: v.Config.GetExcludes(snapName),
|
||||
SkipErrors: opts.SkipErrors,
|
||||
@@ -167,7 +167,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)
|
||||
v.printfStdout("Beginning snapshot: %s\n", snapshotID)
|
||||
v.UI.Begin("Creating snapshot %s.", v.UI.Snapshot(snapshotID))
|
||||
|
||||
stats, err := v.scanAllDirectories(scanner, resolvedDirs, snapshotID)
|
||||
if err != nil {
|
||||
@@ -231,7 +231,7 @@ func (v *Vaultik) scanAllDirectories(scanner *snapshot.Scanner, resolvedDirs []s
|
||||
}
|
||||
|
||||
log.Info("Scanning directory", "path", dir)
|
||||
v.printfStdout("Beginning directory scan (%d/%d): %s\n", i+1, len(resolvedDirs), dir)
|
||||
v.UI.Begin("Enumerating snapshot source files in %s (%d of %d).", v.UI.Path(dir), i+1, len(resolvedDirs))
|
||||
result, err := scanner.Scan(v.ctx, dir, snapshotID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan %s: %w", dir, err)
|
||||
@@ -335,35 +335,36 @@ func (v *Vaultik) printSnapshotSummary(snapshotID string, startTime time.Time, s
|
||||
compressionRatio = 1.0
|
||||
}
|
||||
|
||||
v.printfStdout("=== Snapshot Complete ===\n")
|
||||
v.printfStdout("ID: %s\n", snapshotID)
|
||||
v.printfStdout("Files: %s examined, %s to process, %s unchanged",
|
||||
formatNumber(stats.totalFiles),
|
||||
formatNumber(totalFilesChanged),
|
||||
formatNumber(stats.totalFilesSkipped))
|
||||
v.UI.Complete("Created snapshot %s.", v.UI.Snapshot(snapshotID))
|
||||
filesMsg := fmt.Sprintf("Files: %s examined, %s to process, %s unchanged",
|
||||
v.UI.Count(stats.totalFiles),
|
||||
v.UI.Count(totalFilesChanged),
|
||||
v.UI.Count(stats.totalFilesSkipped))
|
||||
if stats.totalFilesDeleted > 0 {
|
||||
v.printfStdout(", %s deleted", formatNumber(stats.totalFilesDeleted))
|
||||
filesMsg += fmt.Sprintf(", %s deleted", v.UI.Count(stats.totalFilesDeleted))
|
||||
}
|
||||
v.printlnStdout()
|
||||
v.printfStdout("Data: %s total (%s to process)",
|
||||
humanize.Bytes(uint64(totalBytesAll)),
|
||||
humanize.Bytes(uint64(stats.totalBytes)))
|
||||
v.UI.Info("%s.", filesMsg)
|
||||
|
||||
dataMsg := fmt.Sprintf("Data: %s total (%s to process)",
|
||||
v.UI.Size(totalBytesAll),
|
||||
v.UI.Size(stats.totalBytes))
|
||||
if stats.totalBytesDeleted > 0 {
|
||||
v.printfStdout(", %s deleted", humanize.Bytes(uint64(stats.totalBytesDeleted)))
|
||||
dataMsg += fmt.Sprintf(", %s deleted", v.UI.Size(stats.totalBytesDeleted))
|
||||
}
|
||||
v.printlnStdout()
|
||||
v.UI.Info("%s.", dataMsg)
|
||||
|
||||
if stats.totalBlobsUploaded > 0 {
|
||||
v.printfStdout("Storage: %s compressed from %s (%.2fx)\n",
|
||||
humanize.Bytes(uint64(totalBlobSizeCompressed)),
|
||||
humanize.Bytes(uint64(totalBlobSizeUncompressed)),
|
||||
v.UI.Info("Storage: %s compressed from %s (%.2fx ratio).",
|
||||
v.UI.Size(totalBlobSizeCompressed),
|
||||
v.UI.Size(totalBlobSizeUncompressed),
|
||||
compressionRatio)
|
||||
v.printfStdout("Upload: %d blobs, %s in %s (%s)\n",
|
||||
v.UI.Info("Upload: %d blobs, %s in %s (%s).",
|
||||
stats.totalBlobsUploaded,
|
||||
humanize.Bytes(uint64(stats.totalBytesUploaded)),
|
||||
formatDuration(stats.uploadDuration),
|
||||
v.UI.Size(stats.totalBytesUploaded),
|
||||
v.UI.Duration(stats.uploadDuration),
|
||||
formatUploadSpeed(stats.totalBytesUploaded, stats.uploadDuration))
|
||||
}
|
||||
v.printfStdout("Duration: %s\n", formatDuration(snapshotDuration))
|
||||
v.UI.Info("Snapshot create duration: %s.", v.UI.Duration(snapshotDuration))
|
||||
}
|
||||
|
||||
// getSnapshotBlobSizes returns total compressed and uncompressed blob sizes for a snapshot
|
||||
@@ -422,11 +423,11 @@ func (v *Vaultik) ListSnapshots(jsonOutput bool) error {
|
||||
}
|
||||
}
|
||||
if len(stale) > 0 {
|
||||
v.printfStdout("\nWarning: %d local snapshot(s) not found in remote storage:\n", len(stale))
|
||||
v.UI.Warning("%d local snapshot record(s) not found in backup destination store:", len(stale))
|
||||
for _, id := range stale {
|
||||
v.printfStdout(" %s\n", id)
|
||||
v.UI.Info("%s", v.UI.Snapshot(id))
|
||||
}
|
||||
v.printlnStdout("Run 'vaultik snapshot cleanup' to remove stale local records.")
|
||||
v.UI.Info("Run 'vaultik snapshot cleanup' to remove stale local records.")
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1272,6 +1273,7 @@ type PruneResult struct {
|
||||
// before starting a new backup or on-demand via the prune command.
|
||||
func (v *Vaultik) PruneDatabase() (*PruneResult, error) {
|
||||
log.Info("Pruning local database: removing incomplete snapshots and orphaned data")
|
||||
v.UI.Begin("Pruning local index database (removing incomplete snapshots and orphaned data).")
|
||||
|
||||
result := &PruneResult{}
|
||||
|
||||
@@ -1327,12 +1329,8 @@ func (v *Vaultik) PruneDatabase() (*PruneResult, error) {
|
||||
"orphaned_blobs", result.BlobsDeleted,
|
||||
)
|
||||
|
||||
// Print summary
|
||||
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)
|
||||
v.UI.Complete("Pruned local index database: %d incomplete snapshots, %d orphaned files, %d orphaned chunks, %d orphaned blobs removed.",
|
||||
result.SnapshotsDeleted, result.FilesDeleted, result.ChunksDeleted, result.BlobsDeleted)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user