package dedup import ( "encoding/json" "fmt" "os" "path/filepath" "time" ) // Manifest describes a single backup as a list of chunks type Manifest struct { // ID is the unique identifier (typically timestamp-based) ID string `json:"id"` // Name is an optional human-readable name Name string `json:"name,omitempty"` // CreatedAt is when this backup was created CreatedAt time.Time `json:"created_at"` // Database information DatabaseType string `json:"database_type"` // postgres, mysql DatabaseName string `json:"database_name"` DatabaseHost string `json:"database_host"` // Chunks is the ordered list of chunk hashes // The file is reconstructed by concatenating chunks in order Chunks []ChunkRef `json:"chunks"` // Stats about the backup OriginalSize int64 `json:"original_size"` // Size before deduplication StoredSize int64 `json:"stored_size"` // Size after dedup (new chunks only) ChunkCount int `json:"chunk_count"` // Total chunks NewChunks int `json:"new_chunks"` // Chunks that weren't deduplicated DedupRatio float64 `json:"dedup_ratio"` // 1.0 = no dedup, 0.0 = 100% dedup // Encryption and compression settings used Encrypted bool `json:"encrypted"` Compressed bool `json:"compressed"` Decompressed bool `json:"decompressed,omitempty"` // Input was auto-decompressed before chunking // Verification SHA256 string `json:"sha256"` // Hash of reconstructed file VerifiedAt time.Time `json:"verified_at,omitempty"` } // ChunkRef references a chunk in the manifest type ChunkRef struct { Hash string `json:"h"` // SHA-256 hash (64 chars) Offset int64 `json:"o"` // Offset in original file Length int `json:"l"` // Chunk length } // ManifestStore manages backup manifests type ManifestStore struct { basePath string } // NewManifestStore creates a new manifest store func NewManifestStore(basePath string) (*ManifestStore, error) { manifestDir := filepath.Join(basePath, "manifests") if err := os.MkdirAll(manifestDir, 0700); err != nil { return nil, fmt.Errorf("failed to create manifest directory: %w", err) } return &ManifestStore{basePath: basePath}, nil } // manifestPath returns the path for a manifest ID func (s *ManifestStore) manifestPath(id string) string { return filepath.Join(s.basePath, "manifests", id+".manifest.json") } // Save writes a manifest to disk func (s *ManifestStore) Save(m *Manifest) error { path := s.manifestPath(m.ID) data, err := json.MarshalIndent(m, "", " ") if err != nil { return fmt.Errorf("failed to marshal manifest: %w", err) } // Atomic write tmpPath := path + ".tmp" if err := os.WriteFile(tmpPath, data, 0600); err != nil { return fmt.Errorf("failed to write manifest: %w", err) } if err := os.Rename(tmpPath, path); err != nil { os.Remove(tmpPath) return fmt.Errorf("failed to commit manifest: %w", err) } return nil } // Load reads a manifest from disk func (s *ManifestStore) Load(id string) (*Manifest, error) { path := s.manifestPath(id) data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read manifest %s: %w", id, err) } var m Manifest if err := json.Unmarshal(data, &m); err != nil { return nil, fmt.Errorf("failed to parse manifest %s: %w", id, err) } return &m, nil } // Delete removes a manifest func (s *ManifestStore) Delete(id string) error { path := s.manifestPath(id) if err := os.Remove(path); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to delete manifest %s: %w", id, err) } return nil } // List returns all manifest IDs func (s *ManifestStore) List() ([]string, error) { manifestDir := filepath.Join(s.basePath, "manifests") entries, err := os.ReadDir(manifestDir) if err != nil { return nil, fmt.Errorf("failed to list manifests: %w", err) } var ids []string for _, e := range entries { if e.IsDir() { continue } name := e.Name() if len(name) > 14 && name[len(name)-14:] == ".manifest.json" { ids = append(ids, name[:len(name)-14]) } } return ids, nil } // ListAll returns all manifests sorted by creation time (newest first) func (s *ManifestStore) ListAll() ([]*Manifest, error) { ids, err := s.List() if err != nil { return nil, err } var manifests []*Manifest for _, id := range ids { m, err := s.Load(id) if err != nil { continue // Skip corrupted manifests } manifests = append(manifests, m) } // Sort by creation time (newest first) for i := 0; i < len(manifests)-1; i++ { for j := i + 1; j < len(manifests); j++ { if manifests[j].CreatedAt.After(manifests[i].CreatedAt) { manifests[i], manifests[j] = manifests[j], manifests[i] } } } return manifests, nil } // GetChunkHashes returns all unique chunk hashes referenced by manifests func (s *ManifestStore) GetChunkHashes() (map[string]int, error) { manifests, err := s.ListAll() if err != nil { return nil, err } // Map hash -> reference count refs := make(map[string]int) for _, m := range manifests { for _, c := range m.Chunks { refs[c.Hash]++ } } return refs, nil }