The remote snapshot table now shows the total plaintext size of all chunks referenced by each snapshot, plus the plaintext size of chunks newly referenced by that snapshot (chunks not in any earlier completed snapshot known to the local DB). The latter is the marginal data introduced by each backup — useful for spotting which snapshots actually added bytes vs. dedup'd against prior state. Both new columns are computed from the local database only. Snapshots that exist in remote storage but not in the local DB show "<remote only>" in those cells; their COMPRESSED SIZE column still reflects the value fetched from the remote manifest.
110 lines
3.5 KiB
Go
110 lines
3.5 KiB
Go
package vaultik
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"sneak.berlin/go/vaultik/internal/types"
|
|
)
|
|
|
|
// SnapshotInfo contains information about a snapshot.
|
|
// UncompressedSize and NewChunkSize are populated only when the snapshot
|
|
// is present in the local database; LocallyTracked indicates whether
|
|
// those values are meaningful.
|
|
type SnapshotInfo struct {
|
|
ID types.SnapshotID `json:"id"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
CompressedSize int64 `json:"compressed_size"`
|
|
UncompressedSize int64 `json:"uncompressed_size,omitempty"`
|
|
NewChunkSize int64 `json:"new_chunk_size,omitempty"`
|
|
LocallyTracked bool `json:"locally_tracked"`
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// parseSnapshotName extracts the snapshot name from a snapshot ID.
|
|
// Format: hostname_snapshotname_timestamp — the middle part(s) between hostname
|
|
// and the RFC3339 timestamp are the snapshot name (may contain underscores).
|
|
// Returns the snapshot name, or empty string if the ID is malformed.
|
|
func parseSnapshotName(snapshotID string) string {
|
|
parts := strings.Split(snapshotID, "_")
|
|
if len(parts) < 3 {
|
|
// Format: hostname_timestamp — no snapshot name
|
|
return ""
|
|
}
|
|
// Format: hostname_name_timestamp — middle parts are the name.
|
|
// The last part is the RFC3339 timestamp, the first part is the hostname,
|
|
// everything in between is the snapshot name (which may itself contain underscores).
|
|
return strings.Join(parts[1:len(parts)-1], "_")
|
|
}
|
|
|
|
// parseDuration parses a duration string with support for human-friendly units:
|
|
// d/day/days, w/week/weeks, mo/month/months, y/year/years, plus standard Go
|
|
// duration units (h, m, s).
|
|
func parseDuration(s string) (time.Duration, error) {
|
|
if d, err := time.ParseDuration(s); err == nil {
|
|
return d, nil
|
|
}
|
|
|
|
re := regexp.MustCompile(`(\d+)\s*([a-zA-Z]+)`)
|
|
matches := re.FindAllStringSubmatch(s, -1)
|
|
if len(matches) == 0 {
|
|
return 0, fmt.Errorf("invalid duration: %q", s)
|
|
}
|
|
|
|
var total time.Duration
|
|
for _, match := range matches {
|
|
n, err := strconv.Atoi(match[1])
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid number %q: %w", match[1], err)
|
|
}
|
|
unit := strings.ToLower(match[2])
|
|
switch unit {
|
|
case "d", "day", "days":
|
|
total += time.Duration(n) * 24 * time.Hour
|
|
case "w", "week", "weeks":
|
|
total += time.Duration(n) * 7 * 24 * time.Hour
|
|
case "mo", "month", "months":
|
|
total += time.Duration(n) * 30 * 24 * time.Hour
|
|
case "y", "year", "years":
|
|
total += time.Duration(n) * 365 * 24 * time.Hour
|
|
default:
|
|
return 0, fmt.Errorf("unknown time unit %q", unit)
|
|
}
|
|
}
|
|
return total, nil
|
|
}
|