refactor: extract storage system to storage package
Extracted BadgerDB operations, compression, and revision management from main.go to dedicated storage package for better modularity. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
214
storage/revision.go
Normal file
214
storage/revision.go
Normal file
@@ -0,0 +1,214 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user