Refactor snapshot management: integrate logging, enhance concurrency with mutex, add clean extraction option, and update gRPC ListSnapshots with ListOptions.

This commit is contained in:
2025-07-07 20:03:40 +03:00
parent 19c02d3573
commit 223a63ee6d
14 changed files with 977 additions and 253 deletions

View File

@ -6,9 +6,9 @@ import (
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sort"
"strings"
"time"
@ -16,20 +16,26 @@ import (
"gitea.unprism.ru/KRBL/Agate/archive"
"gitea.unprism.ru/KRBL/Agate/hash"
"gitea.unprism.ru/KRBL/Agate/interfaces"
"gitea.unprism.ru/KRBL/Agate/store"
)
type SnapshotManagerData struct {
metadataStore store.MetadataStore
blobStore store.BlobStore
logger *log.Logger
}
func CreateSnapshotManager(metadataStore store.MetadataStore, blobStore store.BlobStore) (SnapshotManager, error) {
func CreateSnapshotManager(metadataStore store.MetadataStore, blobStore store.BlobStore, logger *log.Logger) (interfaces.SnapshotManager, error) {
if metadataStore == nil || blobStore == nil {
return nil, errors.New("parameters can't be nil")
}
return &SnapshotManagerData{metadataStore, blobStore}, nil
return &SnapshotManagerData{
metadataStore: metadataStore,
blobStore: blobStore,
logger: logger,
}, nil
}
func (data *SnapshotManagerData) CreateSnapshot(ctx context.Context, sourceDir string, name string, parentID string) (*store.Snapshot, error) {
@ -162,9 +168,9 @@ func (data *SnapshotManagerData) GetSnapshotDetails(ctx context.Context, snapsho
return snapshot, nil
}
func (data *SnapshotManagerData) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) {
// Retrieve list of snapshots from the metadata store
snapshots, err := data.metadataStore.ListSnapshotsMetadata(ctx)
func (data *SnapshotManagerData) ListSnapshots(ctx context.Context, opts store.ListOptions) ([]store.SnapshotInfo, error) {
// Retrieve list of snapshots from the metadata store with the provided options
snapshots, err := data.metadataStore.ListSnapshotsMetadata(ctx, opts)
if err != nil {
return nil, fmt.Errorf("failed to list snapshots: %w", err)
}
@ -177,8 +183,8 @@ func (data *SnapshotManagerData) DeleteSnapshot(ctx context.Context, snapshotID
return errors.New("snapshot ID cannot be empty")
}
// First check if the snapshot exists
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
// First check if the snapshot exists and get its details
snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil {
if errors.Is(err, ErrNotFound) {
// If snapshot doesn't exist, return success (idempotent operation)
@ -187,6 +193,39 @@ func (data *SnapshotManagerData) DeleteSnapshot(ctx context.Context, snapshotID
return fmt.Errorf("failed to check if snapshot exists: %w", err)
}
// Get the parent ID of the snapshot being deleted
parentID := snapshot.ParentID
// Find all snapshots that have the deleted snapshot as their parent
// We need to update their parent ID to maintain the chain
opts := store.ListOptions{}
allSnapshots, err := data.metadataStore.ListSnapshotsMetadata(ctx, opts)
if err != nil {
return fmt.Errorf("failed to list snapshots: %w", err)
}
// Update parent references for any snapshots that have this one as a parent
for _, info := range allSnapshots {
if info.ParentID == snapshotID {
// Get the full snapshot details
childSnapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, info.ID)
if err != nil {
data.logger.Printf("WARNING: failed to get child snapshot %s details: %v", info.ID, err)
continue
}
// Update the parent ID to point to the deleted snapshot's parent
childSnapshot.ParentID = parentID
// Save the updated snapshot
if err := data.metadataStore.SaveSnapshotMetadata(ctx, *childSnapshot); err != nil {
data.logger.Printf("WARNING: failed to update parent reference for snapshot %s: %v", info.ID, err)
} else {
data.logger.Printf("Updated parent reference for snapshot %s from %s to %s", info.ID, snapshotID, parentID)
}
}
}
// Delete the metadata first
if err := data.metadataStore.DeleteSnapshotMetadata(ctx, snapshotID); err != nil {
return fmt.Errorf("failed to delete snapshot metadata: %w", err)
@ -197,7 +236,7 @@ func (data *SnapshotManagerData) DeleteSnapshot(ctx context.Context, snapshotID
// Note: We don't return here because we've already deleted the metadata
// and the blob store should handle the case where the blob doesn't exist
// Log the error instead
fmt.Printf("Warning: failed to delete snapshot blob: %v\n", err)
data.logger.Printf("WARNING: failed to delete snapshot blob: %v", err)
}
return nil
@ -248,7 +287,7 @@ func (data *SnapshotManagerData) OpenFile(ctx context.Context, snapshotID string
return pr, nil
}
func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID string, path string) error {
func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID string, path string, cleanTarget bool) error {
if snapshotID == "" {
return errors.New("snapshot ID cannot be empty")
}
@ -258,8 +297,8 @@ func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID
path = data.blobStore.GetActiveDir()
}
// First check if the snapshot exists and get its metadata
snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
// First check if the snapshot exists
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil {
if errors.Is(err, ErrNotFound) {
return ErrNotFound
@ -273,9 +312,20 @@ func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID
return fmt.Errorf("failed to get blob path: %w", err)
}
// Ensure the target directory exists
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create target directory: %w", err)
// If cleanTarget is true, clean the target directory before extraction
if cleanTarget {
// Remove the directory and recreate it
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("failed to clean target directory: %w", err)
}
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create target directory: %w", err)
}
} else {
// Just ensure the target directory exists
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create target directory: %w", err)
}
}
// Extract the archive to the target directory
@ -283,94 +333,6 @@ func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID
return fmt.Errorf("failed to extract snapshot: %w", err)
}
// Create maps for files and directories in the snapshot for quick lookup
snapshotFiles := make(map[string]bool)
snapshotDirs := make(map[string]bool)
for _, file := range snapshot.Files {
if file.IsDir {
snapshotDirs[file.Path] = true
} else {
snapshotFiles[file.Path] = true
}
}
// First pass: Collect all files and directories in the target
var allPaths []string
err = filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip the root directory itself
if filePath == path {
return nil
}
// Create relative path
relPath, err := filepath.Rel(path, filePath)
if err != nil {
return fmt.Errorf("failed to get relative path: %w", err)
}
relPath = filepath.ToSlash(relPath)
allPaths = append(allPaths, filePath)
return nil
})
if err != nil {
return fmt.Errorf("failed to scan target directory: %w", err)
}
// Sort paths by length in descending order to process deepest paths first
// This ensures we process files before their parent directories
sort.Slice(allPaths, func(i, j int) bool {
return len(allPaths[i]) > len(allPaths[j])
})
// Second pass: Remove files and directories that aren't in the snapshot
for _, filePath := range allPaths {
info, err := os.Stat(filePath)
if err != nil {
// Skip if file no longer exists (might have been in a directory we already removed)
if os.IsNotExist(err) {
continue
}
return fmt.Errorf("failed to stat file %s: %w", filePath, err)
}
// Create relative path
relPath, err := filepath.Rel(path, filePath)
if err != nil {
return fmt.Errorf("failed to get relative path: %w", err)
}
relPath = filepath.ToSlash(relPath)
if info.IsDir() {
// For directories, check if it's in the snapshot or if it's empty
if !snapshotDirs[relPath] {
// Check if directory is empty
entries, err := os.ReadDir(filePath)
if err != nil {
return fmt.Errorf("failed to read directory %s: %w", filePath, err)
}
// If directory is empty, remove it
if len(entries) == 0 {
if err := os.Remove(filePath); err != nil {
return fmt.Errorf("failed to remove directory %s: %w", filePath, err)
}
}
}
} else {
// For files, remove if not in the snapshot
if !snapshotFiles[relPath] {
if err := os.Remove(filePath); err != nil {
return fmt.Errorf("failed to remove file %s: %w", filePath, err)
}
}
}
}
return nil
}