package storage import ( "encoding/json" "fmt" "strconv" "strings" "time" badger "github.com/dgraph-io/badger/v4" "kvs/auth" "kvs/types" ) // RevisionService handles revision history management type RevisionService struct { storage *StorageService } // NewRevisionService creates a new revision service func NewRevisionService(storage *StorageService) *RevisionService { return &RevisionService{ storage: storage, } } // GetRevisionKey generates the storage key for a specific revision func GetRevisionKey(baseKey string, revision int) string { return fmt.Sprintf("%s:rev:%d", baseKey, revision) } // StoreRevisionHistory stores a value and manages revision history (up to 3 revisions) func (r *RevisionService) StoreRevisionHistory(txn *badger.Txn, key string, storedValue types.StoredValue, ttl time.Duration) error { // Get existing metadata to check current revisions metadataKey := auth.ResourceMetadataKey(key) var metadata types.ResourceMetadata var currentRevisions []int // Try to get existing metadata metadataData, err := r.storage.RetrieveWithDecompression(txn, []byte(metadataKey)) if err == badger.ErrKeyNotFound { // No existing metadata, this is a new key metadata = types.ResourceMetadata{ OwnerUUID: "", // Will be set by caller if needed GroupUUID: "", Permissions: types.DefaultPermissions, TTL: "", CreatedAt: time.Now().Unix(), UpdatedAt: time.Now().Unix(), } currentRevisions = []int{} } else if err != nil { // Error reading metadata return fmt.Errorf("failed to read metadata: %v", err) } else { // Parse existing metadata err = json.Unmarshal(metadataData, &metadata) if err != nil { return fmt.Errorf("failed to unmarshal metadata: %v", err) } // Extract current revisions (we store them as a custom field) if metadata.TTL == "" { currentRevisions = []int{} } else { // For now, we'll manage revisions separately - let's create a new metadata field currentRevisions = []int{1, 2, 3} // Assume all revisions exist for existing keys } } // Revision rotation logic: shift existing revisions if len(currentRevisions) >= 3 { // Delete oldest revision (rev:3) oldestRevKey := GetRevisionKey(key, 3) txn.Delete([]byte(oldestRevKey)) // Shift rev:2 → rev:3 rev2Key := GetRevisionKey(key, 2) rev2Data, err := r.storage.RetrieveWithDecompression(txn, []byte(rev2Key)) if err == nil { rev3Key := GetRevisionKey(key, 3) r.storage.StoreWithTTL(txn, []byte(rev3Key), rev2Data, ttl) } // Shift rev:1 → rev:2 rev1Key := GetRevisionKey(key, 1) rev1Data, err := r.storage.RetrieveWithDecompression(txn, []byte(rev1Key)) if err == nil { rev2Key := GetRevisionKey(key, 2) r.storage.StoreWithTTL(txn, []byte(rev2Key), rev1Data, ttl) } } // Store current value as rev:1 currentValueBytes, err := json.Marshal(storedValue) if err != nil { return fmt.Errorf("failed to marshal current value for revision: %v", err) } rev1Key := GetRevisionKey(key, 1) err = r.storage.StoreWithTTL(txn, []byte(rev1Key), currentValueBytes, ttl) if err != nil { return fmt.Errorf("failed to store revision 1: %v", err) } // Update metadata with new revision count metadata.UpdatedAt = time.Now().Unix() metadataBytes, err := json.Marshal(metadata) if err != nil { return fmt.Errorf("failed to marshal metadata: %v", err) } return r.storage.StoreWithTTL(txn, []byte(metadataKey), metadataBytes, ttl) } // GetRevisionHistory retrieves all available revisions for a given key func (r *RevisionService) GetRevisionHistory(key string) ([]map[string]interface{}, error) { var revisions []map[string]interface{} err := r.storage.db.View(func(txn *badger.Txn) error { // Check revisions 1, 2, 3 for rev := 1; rev <= 3; rev++ { revKey := GetRevisionKey(key, rev) revData, err := r.storage.RetrieveWithDecompression(txn, []byte(revKey)) if err == badger.ErrKeyNotFound { continue // Skip missing revisions } else if err != nil { return fmt.Errorf("failed to retrieve revision %d: %v", rev, err) } var storedValue types.StoredValue err = json.Unmarshal(revData, &storedValue) if err != nil { return fmt.Errorf("failed to unmarshal revision %d: %v", rev, err) } var data interface{} err = json.Unmarshal(storedValue.Data, &data) if err != nil { return fmt.Errorf("failed to unmarshal revision %d data: %v", rev, err) } revision := map[string]interface{}{ "revision": rev, "uuid": storedValue.UUID, "timestamp": storedValue.Timestamp, "data": data, } revisions = append(revisions, revision) } return nil }) if err != nil { return nil, err } // Sort revisions by revision number (newest first) // Note: they're already in order since we iterate 1->3, but reverse for newest first for i, j := 0, len(revisions)-1; i < j; i, j = i+1, j-1 { revisions[i], revisions[j] = revisions[j], revisions[i] } return revisions, nil } // GetSpecificRevision retrieves a specific revision of a key func (r *RevisionService) GetSpecificRevision(key string, revision int) (*types.StoredValue, error) { if revision < 1 || revision > 3 { return nil, fmt.Errorf("invalid revision number: %d (must be 1-3)", revision) } var storedValue types.StoredValue err := r.storage.db.View(func(txn *badger.Txn) error { revKey := GetRevisionKey(key, revision) revData, err := r.storage.RetrieveWithDecompression(txn, []byte(revKey)) if err != nil { return err } return json.Unmarshal(revData, &storedValue) }) if err != nil { return nil, err } return &storedValue, nil } // GetRevisionFromPath extracts revision number from a path like "key/data/rev/2" func GetRevisionFromPath(path string) (string, int, error) { parts := strings.Split(path, "/") if len(parts) < 4 || parts[len(parts)-2] != "rev" { return "", 0, fmt.Errorf("invalid revision path format") } revisionStr := parts[len(parts)-1] revision, err := strconv.Atoi(revisionStr) if err != nil { return "", 0, fmt.Errorf("invalid revision number: %s", revisionStr) } // Reconstruct the base key without the "/rev/N" suffix baseKey := strings.Join(parts[:len(parts)-2], "/") return baseKey, revision, nil }