- Add internal/types package with type-safe wrappers for IDs, hashes, paths, and credentials (FileID, BlobID, ChunkHash, etc.) - Implement driver.Valuer and sql.Scanner for UUID-based types - Add `vaultik version` command showing version, commit, go version - Add `--verify` flag to restore command that checksums all restored files against expected chunk hashes with progress bar - Remove fetch.go (dead code, functionality in restore) - Clean up TODO.md, remove completed items - Update all database and snapshot code to use new custom types
97 lines
2.5 KiB
Go
97 lines
2.5 KiB
Go
package vaultik
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/vaultik/internal/types"
|
|
)
|
|
|
|
// SnapshotInfo contains information about a snapshot
|
|
type SnapshotInfo struct {
|
|
ID types.SnapshotID `json:"id"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
CompressedSize int64 `json:"compressed_size"`
|
|
}
|
|
|
|
// formatNumber formats a number with commas
|
|
func formatNumber(n int) string {
|
|
str := fmt.Sprintf("%d", n)
|
|
var result []string
|
|
for i, digit := range str {
|
|
if i > 0 && (len(str)-i)%3 == 0 {
|
|
result = append(result, ",")
|
|
}
|
|
result = append(result, string(digit))
|
|
}
|
|
return strings.Join(result, "")
|
|
}
|
|
|
|
// formatDuration formats a duration in a human-readable way
|
|
func formatDuration(d time.Duration) string {
|
|
if d < time.Second {
|
|
return fmt.Sprintf("%dms", d.Milliseconds())
|
|
}
|
|
if d < time.Minute {
|
|
return fmt.Sprintf("%.1fs", d.Seconds())
|
|
}
|
|
if d < time.Hour {
|
|
mins := int(d.Minutes())
|
|
secs := int(d.Seconds()) % 60
|
|
return fmt.Sprintf("%dm %ds", mins, secs)
|
|
}
|
|
hours := int(d.Hours())
|
|
mins := int(d.Minutes()) % 60
|
|
return fmt.Sprintf("%dh %dm", hours, mins)
|
|
}
|
|
|
|
// formatBytes formats bytes in a human-readable format
|
|
func formatBytes(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|
|
|
|
// parseSnapshotTimestamp extracts the timestamp from a snapshot ID
|
|
// Format: hostname_snapshotname_2026-01-12T14:41:15Z
|
|
func parseSnapshotTimestamp(snapshotID string) (time.Time, error) {
|
|
parts := strings.Split(snapshotID, "_")
|
|
if len(parts) < 2 {
|
|
return time.Time{}, fmt.Errorf("invalid snapshot ID format: expected hostname_snapshotname_timestamp")
|
|
}
|
|
|
|
// Last part is the RFC3339 timestamp
|
|
timestampStr := parts[len(parts)-1]
|
|
timestamp, err := time.Parse(time.RFC3339, timestampStr)
|
|
if err != nil {
|
|
return time.Time{}, fmt.Errorf("invalid timestamp: %w", err)
|
|
}
|
|
|
|
return timestamp.UTC(), nil
|
|
}
|
|
|
|
// parseDuration parses a duration string with support for days
|
|
func parseDuration(s string) (time.Duration, error) {
|
|
// Check for days suffix
|
|
if strings.HasSuffix(s, "d") {
|
|
daysStr := strings.TrimSuffix(s, "d")
|
|
days, err := strconv.Atoi(daysStr)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid days value: %w", err)
|
|
}
|
|
return time.Duration(days) * 24 * time.Hour, nil
|
|
}
|
|
|
|
// Otherwise use standard Go duration parsing
|
|
return time.ParseDuration(s)
|
|
}
|