3 Commits

14 changed files with 1016 additions and 260 deletions

View File

@ -15,37 +15,37 @@ gen-proto:
--grpc-gateway_out=grpc --grpc-gateway_opt paths=source_relative \ --grpc-gateway_out=grpc --grpc-gateway_opt paths=source_relative \
./grpc/snapshot.proto ./grpc/snapshot.proto
# Запуск всех тестов # Run all tests
test: test:
go test -v ./... go test -v ./...
# Запуск модульных тестов # Run unit tests
test-unit: test-unit:
go test -v ./store/... ./hash/... ./archive/... go test -v ./store/... ./hash/... ./archive/...
# Запуск интеграционных тестов # Run integration tests
test-integration: test-integration:
go test -v -tags=integration ./... go test -v -tags=integration ./...
# Запуск функциональных тестов # Run functional tests
test-functional: test-functional:
go test -v -run TestFull ./... go test -v -run TestFull ./...
# Запуск тестов производительности # Run performance tests
test-performance: test-performance:
go test -v -run TestPerformanceMetrics ./... go test -v -run TestPerformanceMetrics ./...
go test -v -bench=. ./... go test -v -bench=. ./...
# Запуск тестов с покрытием кода # Run tests with code coverage
test-coverage: test-coverage:
go test -v -coverprofile=coverage.out ./... go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html go tool cover -html=coverage.out -o coverage.html
# Запуск линтера # Run linter
lint: lint:
golangci-lint run golangci-lint run
# Запуск всех проверок (тесты + линтер) # Run all checks (tests + linter)
check: test lint check: test lint
.PHONY: download-third-party gen-proto test test-unit test-integration test-functional test-performance test-coverage lint check .PHONY: download-third-party gen-proto test test-unit test-integration test-functional test-performance test-coverage lint check

224
api.go
View File

@ -4,9 +4,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"gitea.unprism.ru/KRBL/Agate/archive"
"gitea.unprism.ru/KRBL/Agate/grpc" "gitea.unprism.ru/KRBL/Agate/grpc"
"gitea.unprism.ru/KRBL/Agate/interfaces"
"io"
"log"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"gitea.unprism.ru/KRBL/Agate/store" "gitea.unprism.ru/KRBL/Agate/store"
"gitea.unprism.ru/KRBL/Agate/stores" "gitea.unprism.ru/KRBL/Agate/stores"
@ -35,11 +40,18 @@ type AgateOptions struct {
// Use the stores package to initialize a custom implementation: // Use the stores package to initialize a custom implementation:
// blobStore, err := stores.NewDefaultBlobStore(blobsDir) // blobStore, err := stores.NewDefaultBlobStore(blobsDir)
BlobStore store.BlobStore BlobStore store.BlobStore
// CleanOnRestore specifies whether the target directory should be cleaned before restoring a snapshot.
CleanOnRestore bool
// Logger is the logger to use for output. If nil, logging is disabled.
Logger *log.Logger
} }
// Agate is the main entry point for the snapshot library. // Agate is the main entry point for the snapshot library.
type Agate struct { type Agate struct {
manager SnapshotManager mutex sync.Mutex
manager interfaces.SnapshotManager
options AgateOptions options AgateOptions
metadataDir string metadataDir string
blobsDir string blobsDir string
@ -53,6 +65,11 @@ func New(options AgateOptions) (*Agate, error) {
return nil, errors.New("work directory cannot be empty") return nil, errors.New("work directory cannot be empty")
} }
// Initialize logger if not provided
if options.Logger == nil {
options.Logger = log.New(io.Discard, "", 0)
}
// Create the work directory if it doesn't exist // Create the work directory if it doesn't exist
if err := os.MkdirAll(options.WorkDir, 0755); err != nil { if err := os.MkdirAll(options.WorkDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create work directory: %w", err) return nil, fmt.Errorf("failed to create work directory: %w", err)
@ -109,7 +126,7 @@ func New(options AgateOptions) (*Agate, error) {
} }
// Create the snapshot manager // Create the snapshot manager
manager, err := CreateSnapshotManager(metadataStore, blobStore) manager, err := CreateSnapshotManager(metadataStore, blobStore, options.Logger)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create snapshot manager: %w", err) return nil, fmt.Errorf("failed to create snapshot manager: %w", err)
} }
@ -160,6 +177,11 @@ func (a *Agate) GetBlobsDir() string {
// If parentID is empty, it will use the ID of the snapshot currently loaded in the active directory. // If parentID is empty, it will use the ID of the snapshot currently loaded in the active directory.
// Returns the ID of the created snapshot. // Returns the ID of the created snapshot.
func (a *Agate) SaveSnapshot(ctx context.Context, name string, parentID string) (string, error) { func (a *Agate) SaveSnapshot(ctx context.Context, name string, parentID string) (string, error) {
a.mutex.Lock()
defer a.mutex.Unlock()
a.options.Logger.Printf("Creating new snapshot with name: %s", name)
// Call CloseFunc if provided // Call CloseFunc if provided
if a.options.CloseFunc != nil { if a.options.CloseFunc != nil {
if err := a.options.CloseFunc(); err != nil { if err := a.options.CloseFunc(); err != nil {
@ -170,7 +192,7 @@ func (a *Agate) SaveSnapshot(ctx context.Context, name string, parentID string)
defer func() { defer func() {
if a.options.OpenFunc != nil { if a.options.OpenFunc != nil {
if err := a.options.OpenFunc(a.options.BlobStore.GetActiveDir()); err != nil { if err := a.options.OpenFunc(a.options.BlobStore.GetActiveDir()); err != nil {
fmt.Printf("Failed to open resources after snapshot: %v\n", err) a.options.Logger.Printf("ERROR: failed to open resources after snapshot creation: %v", err)
} }
} }
}() }()
@ -185,9 +207,12 @@ func (a *Agate) SaveSnapshot(ctx context.Context, name string, parentID string)
// Create the snapshot // Create the snapshot
snapshot, err := a.manager.CreateSnapshot(ctx, a.options.BlobStore.GetActiveDir(), name, effectiveParentID) snapshot, err := a.manager.CreateSnapshot(ctx, a.options.BlobStore.GetActiveDir(), name, effectiveParentID)
if err != nil { if err != nil {
a.options.Logger.Printf("ERROR: failed to create snapshot: %v", err)
return "", fmt.Errorf("failed to create snapshot: %w", err) return "", fmt.Errorf("failed to create snapshot: %w", err)
} }
a.options.Logger.Printf("Successfully created snapshot with ID: %s", snapshot.ID)
// Update the current snapshot ID to the newly created snapshot // Update the current snapshot ID to the newly created snapshot
a.currentSnapshotID = snapshot.ID a.currentSnapshotID = snapshot.ID
@ -201,6 +226,11 @@ func (a *Agate) SaveSnapshot(ctx context.Context, name string, parentID string)
// RestoreSnapshot extracts a snapshot to the active directory. // RestoreSnapshot extracts a snapshot to the active directory.
func (a *Agate) RestoreSnapshot(ctx context.Context, snapshotID string) error { func (a *Agate) RestoreSnapshot(ctx context.Context, snapshotID string) error {
a.mutex.Lock()
defer a.mutex.Unlock()
a.options.Logger.Printf("Restoring snapshot with ID: %s", snapshotID)
// Call CloseFunc if provided // Call CloseFunc if provided
if a.options.CloseFunc != nil { if a.options.CloseFunc != nil {
if err := a.options.CloseFunc(); err != nil { if err := a.options.CloseFunc(); err != nil {
@ -209,10 +239,13 @@ func (a *Agate) RestoreSnapshot(ctx context.Context, snapshotID string) error {
} }
// Extract the snapshot // Extract the snapshot
if err := a.manager.ExtractSnapshot(ctx, snapshotID, a.options.BlobStore.GetActiveDir()); err != nil { if err := a.manager.ExtractSnapshot(ctx, snapshotID, a.options.BlobStore.GetActiveDir(), a.options.CleanOnRestore); err != nil {
a.options.Logger.Printf("ERROR: failed to extract snapshot: %v", err)
return fmt.Errorf("failed to extract snapshot: %w", err) return fmt.Errorf("failed to extract snapshot: %w", err)
} }
a.options.Logger.Printf("Successfully restored snapshot with ID: %s", snapshotID)
// Save the ID of the snapshot that was restored // Save the ID of the snapshot that was restored
a.currentSnapshotID = snapshotID a.currentSnapshotID = snapshotID
@ -224,6 +257,7 @@ func (a *Agate) RestoreSnapshot(ctx context.Context, snapshotID string) error {
// Call OpenFunc if provided // Call OpenFunc if provided
if a.options.OpenFunc != nil { if a.options.OpenFunc != nil {
if err := a.options.OpenFunc(a.options.BlobStore.GetActiveDir()); err != nil { if err := a.options.OpenFunc(a.options.BlobStore.GetActiveDir()); err != nil {
a.options.Logger.Printf("ERROR: failed to open resources after restore: %v", err)
return fmt.Errorf("failed to open resources after restore: %w", err) return fmt.Errorf("failed to open resources after restore: %w", err)
} }
} }
@ -233,6 +267,9 @@ func (a *Agate) RestoreSnapshot(ctx context.Context, snapshotID string) error {
// RestoreSnapshot extracts a snapshot to the directory. // RestoreSnapshot extracts a snapshot to the directory.
func (a *Agate) RestoreSnapshotToDir(ctx context.Context, snapshotID string, dir string) error { func (a *Agate) RestoreSnapshotToDir(ctx context.Context, snapshotID string, dir string) error {
a.mutex.Lock()
defer a.mutex.Unlock()
// Call CloseFunc if provided // Call CloseFunc if provided
if a.options.CloseFunc != nil { if a.options.CloseFunc != nil {
if err := a.options.CloseFunc(); err != nil { if err := a.options.CloseFunc(); err != nil {
@ -243,13 +280,13 @@ func (a *Agate) RestoreSnapshotToDir(ctx context.Context, snapshotID string, dir
defer func() { defer func() {
if a.options.OpenFunc != nil { if a.options.OpenFunc != nil {
if err := a.options.OpenFunc(dir); err != nil { if err := a.options.OpenFunc(dir); err != nil {
fmt.Printf("Failed to open resources after snapshot: %v\n", err) a.options.Logger.Printf("ERROR: failed to open resources after snapshot restore: %v", err)
} }
} }
}() }()
// Extract the snapshot // Extract the snapshot
if err := a.manager.ExtractSnapshot(ctx, snapshotID, dir); err != nil { if err := a.manager.ExtractSnapshot(ctx, snapshotID, dir, a.options.CleanOnRestore); err != nil {
return fmt.Errorf("failed to extract snapshot: %w", err) return fmt.Errorf("failed to extract snapshot: %w", err)
} }
@ -268,7 +305,9 @@ func (a *Agate) RestoreSnapshotToDir(ctx context.Context, snapshotID string, dir
// ListSnapshots returns a list of all available snapshots. // ListSnapshots returns a list of all available snapshots.
func (a *Agate) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) { func (a *Agate) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) {
return a.manager.ListSnapshots(ctx) // Create empty ListOptions since we don't have filtering/pagination in this API yet
opts := store.ListOptions{}
return a.manager.ListSnapshots(ctx, opts)
} }
// GetSnapshotDetails returns detailed information about a specific snapshot. // GetSnapshotDetails returns detailed information about a specific snapshot.
@ -295,10 +334,15 @@ func (a *Agate) saveCurrentSnapshotID() error {
return os.WriteFile(a.currentIDFile, []byte(a.currentSnapshotID), 0644) return os.WriteFile(a.currentIDFile, []byte(a.currentSnapshotID), 0644)
} }
func (a *Agate) Open() error {
return a.options.OpenFunc(a.GetActiveDir())
}
// Close releases all resources used by the Agate instance. // Close releases all resources used by the Agate instance.
func (a *Agate) Close() error { func (a *Agate) Close() error {
// Currently, we don't have a way to close the manager directly if a.options.CloseFunc != nil {
// This would be a good addition in the future return a.options.CloseFunc()
}
return nil return nil
} }
@ -346,31 +390,155 @@ func (a *Agate) GetRemoteSnapshot(ctx context.Context, address string, snapshotI
} }
defer client.Close() defer client.Close()
// Create a temporary directory for the downloaded snapshot // Get the remote snapshot details
tempDir := filepath.Join(a.options.WorkDir, "temp", snapshotID) remoteSnapshot, err := client.FetchSnapshotDetails(ctx, snapshotID)
if err := os.MkdirAll(tempDir, 0755); err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err)
}
// Download the snapshot
if err := client.DownloadSnapshot(ctx, snapshotID, tempDir, localParentID); err != nil {
return fmt.Errorf("failed to download snapshot: %w", err)
}
// Get the snapshot details to create a local copy
details, err := client.FetchSnapshotDetails(ctx, snapshotID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get snapshot details: %w", err) return fmt.Errorf("failed to get snapshot details: %w", err)
} }
// Create a local snapshot from the downloaded files // Create a temporary directory for downloading files
_, err = a.manager.CreateSnapshot(ctx, tempDir, details.Name, localParentID) tempDownloadDir := filepath.Join(a.options.WorkDir, "temp_download", snapshotID)
if err != nil { if err := os.MkdirAll(tempDownloadDir, 0755); err != nil {
return fmt.Errorf("failed to create local snapshot: %w", err) return fmt.Errorf("failed to create temporary download directory: %w", err)
}
defer os.RemoveAll(tempDownloadDir) // Clean up when done
a.options.Logger.Printf("Downloading snapshot %s from %s", snapshotID, address)
// If localParentID is provided, try to reuse files from the local parent snapshot
if localParentID != "" {
a.options.Logger.Printf("Using local parent snapshot %s for incremental download", localParentID)
// Get the local parent snapshot details
localParent, err := a.GetSnapshotDetails(ctx, localParentID)
if err != nil {
a.options.Logger.Printf("Warning: Failed to get local parent snapshot details: %v", err)
} else {
// Extract the local parent snapshot to a temporary directory
localParentDir := filepath.Join(a.options.WorkDir, "temp_download", localParentID)
if err := os.MkdirAll(localParentDir, 0755); err != nil {
a.options.Logger.Printf("Warning: Failed to create temporary directory for local parent: %v", err)
} else {
defer os.RemoveAll(localParentDir) // Clean up when done
if err := a.manager.ExtractSnapshot(ctx, localParentID, localParentDir, true); err != nil {
a.options.Logger.Printf("Warning: Failed to extract local parent snapshot: %v", err)
} else {
// Copy unchanged files from the local parent to the download directory
for _, file := range remoteSnapshot.Files {
// Skip directories, they'll be created as needed
if file.IsDir {
continue
}
// Check if the file exists in the local parent with the same hash
var localFile *store.FileInfo
for _, lf := range localParent.Files {
if lf.Path == file.Path && lf.SHA256 == file.SHA256 {
localFile = &lf
break
}
}
if localFile != nil {
// File exists in local parent with the same hash, copy it
srcPath := filepath.Join(localParentDir, localFile.Path)
dstPath := filepath.Join(tempDownloadDir, file.Path)
// Ensure the destination directory exists
if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil {
a.options.Logger.Printf("Failed to create directory for %s: %v", dstPath, err)
continue
}
// Copy the file
srcFile, err := os.Open(srcPath)
if err != nil {
a.options.Logger.Printf("Failed to copy file %s, will download instead: %v", file.Path, err)
continue
}
defer srcFile.Close()
dstFile, err := os.Create(dstPath)
if err != nil {
a.options.Logger.Printf("Failed to create destination file %s: %v", dstPath, err)
continue
}
_, err = io.Copy(dstFile, srcFile)
dstFile.Close()
if err != nil {
a.options.Logger.Printf("Failed to copy file data for %s: %v", file.Path, err)
// If copy fails, the file will be downloaded
} else {
a.options.Logger.Printf("Reusing file %s from local parent", file.Path)
}
}
}
}
}
}
} }
// Clean up the temporary directory // Download the snapshot files
os.RemoveAll(tempDir) a.options.Logger.Printf("Downloading files for snapshot %s", snapshotID)
// Get snapshot details to know what files we need to download
remoteDetails, err := client.FetchSnapshotDetails(ctx, snapshotID)
if err != nil {
return fmt.Errorf("failed to get remote snapshot details: %w", err)
}
// Check which files we already have and which we need to download
for _, file := range remoteDetails.Files {
if file.IsDir {
continue // Skip directories
}
filePath := filepath.Join(tempDownloadDir, file.Path)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
// File doesn't exist yet, we'll need to download it
a.options.Logger.Printf("Downloading file %s", file.Path)
}
}
if err := client.DownloadSnapshot(ctx, snapshotID, tempDownloadDir, localParentID); err != nil {
return fmt.Errorf("failed to download snapshot: %w", err)
}
a.options.Logger.Printf("Creating archive from downloaded files")
// Create a zip archive from the downloaded files
zipPath := filepath.Join(a.options.WorkDir, "temp_download", snapshotID+".zip")
if err := archive.CreateArchive(tempDownloadDir, zipPath); err != nil {
return fmt.Errorf("failed to create zip archive: %w", err)
}
// Store the blob with the remote snapshot ID
zipFile, err := os.Open(zipPath)
if err != nil {
return fmt.Errorf("failed to open zip file: %w", err)
}
defer zipFile.Close()
defer os.Remove(zipPath) // Clean up the zip file when done
a.options.Logger.Printf("Storing blob with ID %s", remoteSnapshot.ID)
// Store the blob with the remote snapshot ID
_, err = a.options.BlobStore.StoreBlob(ctx, remoteSnapshot.ID, zipFile)
if err != nil {
return fmt.Errorf("failed to store blob: %w", err)
}
a.options.Logger.Printf("Saving snapshot metadata")
// Save the remote snapshot metadata
err = a.options.MetadataStore.SaveSnapshotMetadata(ctx, *remoteSnapshot)
if err != nil {
return fmt.Errorf("failed to save snapshot metadata: %w", err)
}
a.options.Logger.Printf("Successfully imported remote snapshot %s", snapshotID)
return nil return nil
} }

View File

@ -1,9 +1,12 @@
package agate package agate
import ( import (
"bytes"
"context" "context"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
) )
@ -252,6 +255,91 @@ func TestAPIListSnapshots(t *testing.T) {
} }
} }
func TestAgate_Logging(t *testing.T) {
// Create a temporary directory for tests
tempDir, err := os.MkdirTemp("", "agate-test-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir)
// Create a data directory
dataDir := filepath.Join(tempDir, "data")
if err := os.MkdirAll(dataDir, 0755); err != nil {
t.Fatalf("Failed to create data directory: %v", err)
}
// Create test files in the active directory
activeDir := filepath.Join(dataDir, "blobs", "active")
if err := os.MkdirAll(activeDir, 0755); err != nil {
t.Fatalf("Failed to create active directory: %v", err)
}
createAPITestFiles(t, activeDir)
// Create a buffer to capture log output
var logBuffer bytes.Buffer
logger := log.New(&logBuffer, "", 0)
// Create Agate options with the logger
options := AgateOptions{
WorkDir: dataDir,
OpenFunc: func(dir string) error {
return nil
},
CloseFunc: func() error {
return nil
},
Logger: logger,
}
// Create Agate instance
ag, err := New(options)
if err != nil {
t.Fatalf("Failed to create Agate instance: %v", err)
}
defer ag.Close()
// Perform operations that should generate logs
ctx := context.Background()
// Save a snapshot
snapshotID, err := ag.SaveSnapshot(ctx, "Test Snapshot", "")
if err != nil {
t.Fatalf("Failed to create snapshot: %v", err)
}
// Restore the snapshot
err = ag.RestoreSnapshot(ctx, snapshotID)
if err != nil {
t.Fatalf("Failed to restore snapshot: %v", err)
}
// Check that logs were generated
logs := logBuffer.String()
if logs == "" {
t.Errorf("No logs were generated")
}
// Check for expected log messages
expectedLogMessages := []string{
"Creating new snapshot",
"Restoring snapshot",
}
for _, msg := range expectedLogMessages {
if !strings.Contains(logs, msg) {
t.Errorf("Expected log message '%s' not found in logs", msg)
}
}
}
// Note: This test is a placeholder for when the ListSnapshots method is updated to accept ListOptions.
// Currently, the ListSnapshots method in api.go doesn't accept ListOptions, so we can't test that functionality directly.
// The test for ListOptions functionality is covered in TestListSnapshotsMetadata_WithOptions in store/sqlite/sqlite_test.go.
func TestAgate_ListSnapshotsWithOptions(t *testing.T) {
t.Skip("Skipping test as ListSnapshots in api.go doesn't yet support ListOptions")
}
func TestAPIDeleteSnapshot(t *testing.T) { func TestAPIDeleteSnapshot(t *testing.T) {
ag, _, cleanup := setupTestAPI(t) ag, _, cleanup := setupTestAPI(t)
defer cleanup() defer cleanup()

View File

@ -20,7 +20,8 @@ func TestFullWorkflow(t *testing.T) {
// Create Agate options // Create Agate options
options := AgateOptions{ options := AgateOptions{
WorkDir: tempDir, WorkDir: tempDir,
CleanOnRestore: true,
} }
// Create Agate instance // Create Agate instance
@ -123,12 +124,20 @@ func TestFullWorkflow(t *testing.T) {
} }
if string(content) != expectedContent { if string(content) != expectedContent {
t.Errorf("Restored file %s has wrong content: got %s, want %s", path, string(content), expectedContent) t.Errorf("Restored file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
} else {
t.Logf("SUCCESS: Restored file %s has correct content after restoring first snapshot", path)
} }
} }
// Check that file4.txt doesn't exist // Check that file4.txt doesn't exist
if _, err := os.Stat(filepath.Join(dataDir, "file4.txt")); !os.IsNotExist(err) { file4Path := filepath.Join(dataDir, "file4.txt")
_, err = os.Stat(file4Path)
if err == nil {
t.Errorf("File4.txt should not exist after restoring first snapshot") t.Errorf("File4.txt should not exist after restoring first snapshot")
} else if !os.IsNotExist(err) {
t.Errorf("Unexpected error checking if File4.txt exists: %v", err)
} else {
t.Logf("SUCCESS: File4.txt correctly does not exist after restoring first snapshot")
} }
// Step 9: Restore the third snapshot // Step 9: Restore the third snapshot
@ -152,12 +161,20 @@ func TestFullWorkflow(t *testing.T) {
} }
if string(content) != expectedContent { if string(content) != expectedContent {
t.Errorf("Restored file %s has wrong content: got %s, want %s", path, string(content), expectedContent) t.Errorf("Restored file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
} else {
t.Logf("SUCCESS: Restored file %s has correct content after restoring third snapshot", path)
} }
} }
// Check that file2.txt doesn't exist // Check that file2.txt doesn't exist
if _, err := os.Stat(filepath.Join(dataDir, "file2.txt")); !os.IsNotExist(err) { file2Path := filepath.Join(dataDir, "file2.txt")
_, err = os.Stat(file2Path)
if err == nil {
t.Errorf("File2.txt should not exist after restoring third snapshot") t.Errorf("File2.txt should not exist after restoring third snapshot")
} else if !os.IsNotExist(err) {
t.Errorf("Unexpected error checking if File2.txt exists: %v", err)
} else {
t.Logf("SUCCESS: File2.txt correctly does not exist after restoring third snapshot")
} }
// Step 11: Delete a snapshot // Step 11: Delete a snapshot
@ -173,15 +190,43 @@ func TestFullWorkflow(t *testing.T) {
t.Fatalf("Failed to list snapshots: %v", err) t.Fatalf("Failed to list snapshots: %v", err)
} }
if len(snapshots) != 2 { // Debug output
t.Errorf("Expected 2 snapshots after deletion, got %d", len(snapshots)) t.Logf("After deletion, found %d snapshots:", len(snapshots))
for i, snap := range snapshots {
t.Logf(" Snapshot %d: ID=%s, Name=%s, ParentID=%s", i+1, snap.ID, snap.Name, snap.ParentID)
} }
// Get detailed information about snapshot 3
snapshot3, err := ag.GetSnapshotDetails(ctx, snapshot3ID)
if err != nil {
t.Logf("Failed to get snapshot 3 details: %v", err)
} else {
t.Logf("Snapshot 3 details: ID=%s, Name=%s, ParentID=%s", snapshot3.ID, snapshot3.Name, snapshot3.ParentID)
}
// Verify that snapshot 3's parent ID has been updated to point to snapshot 1
if snapshot3 != nil && snapshot3.ParentID != snapshot1ID {
t.Errorf("Snapshot 3's parent ID should be updated to point to Snapshot 1 after Snapshot 2 is deleted. Got ParentID=%s, want ParentID=%s", snapshot3.ParentID, snapshot1ID)
} else {
t.Logf("SUCCESS: Snapshot 3's parent ID has been correctly updated to point to Snapshot 1: %s", snapshot3.ParentID)
}
if len(snapshots) != 2 {
t.Errorf("Expected 2 snapshots after deletion, got %d", len(snapshots))
} else {
t.Logf("SUCCESS: Found correct number of snapshots after deletion: %d", len(snapshots))
}
foundDeletedSnapshot := false
for _, snap := range snapshots { for _, snap := range snapshots {
if snap.ID == snapshot2ID { if snap.ID == snapshot2ID {
t.Errorf("Snapshot 2 should have been deleted") foundDeletedSnapshot = true
t.Errorf("Snapshot 2 (ID=%s) should have been deleted", snapshot2ID)
} }
} }
if !foundDeletedSnapshot {
t.Logf("SUCCESS: Snapshot 2 (ID=%s) was correctly deleted", snapshot2ID)
}
} }
// TestLargeFiles tests creating and restoring snapshots with large files // TestLargeFiles tests creating and restoring snapshots with large files
@ -200,7 +245,8 @@ func TestLargeFiles(t *testing.T) {
// Create Agate options // Create Agate options
options := AgateOptions{ options := AgateOptions{
WorkDir: tempDir, WorkDir: tempDir,
CleanOnRestore: true,
OpenFunc: func(dir string) error { OpenFunc: func(dir string) error {
return nil return nil
}, },

View File

@ -58,7 +58,11 @@ func (s *SnapshotServer) Stop(ctx context.Context) error {
// ListSnapshots implements the gRPC ListSnapshots method // ListSnapshots implements the gRPC ListSnapshots method
func (s *SnapshotServer) ListSnapshots(ctx context.Context, req *ListSnapshotsRequest) (*ListSnapshotsResponse, error) { func (s *SnapshotServer) ListSnapshots(ctx context.Context, req *ListSnapshotsRequest) (*ListSnapshotsResponse, error) {
snapshots, err := s.manager.ListSnapshots(ctx) // Create empty ListOptions since the proto doesn't have active filter/pagination fields yet
opts := store.ListOptions{}
// Call manager with the required ListOptions parameter
snapshots, err := s.manager.ListSnapshots(ctx, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list snapshots: %w", err) return nil, fmt.Errorf("failed to list snapshots: %w", err)
} }

View File

@ -1,9 +1,12 @@
package agate package agate
import ( import (
"bytes"
"context" "context"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"time" "time"
@ -202,3 +205,185 @@ func TestGRPCServerClient(t *testing.T) {
t.Errorf("file2.txt should not exist in the downloaded snapshot") t.Errorf("file2.txt should not exist in the downloaded snapshot")
} }
} }
// TestGRPC_GetRemoteSnapshot_Incremental tests the incremental download functionality
// of GetRemoteSnapshot, verifying that it reuses files from a local parent snapshot
// instead of downloading them again.
func TestGRPC_GetRemoteSnapshot_Incremental(t *testing.T) {
// Skip this test in short mode
if testing.Short() {
t.Skip("Skipping incremental GetRemoteSnapshot test in short mode")
}
// Create a temporary directory for the server
serverDir, err := os.MkdirTemp("", "agate-server-*")
if err != nil {
t.Fatalf("Failed to create server temp directory: %v", err)
}
defer os.RemoveAll(serverDir)
// Create a temporary directory for the client
clientDir, err := os.MkdirTemp("", "agate-client-*")
if err != nil {
t.Fatalf("Failed to create client temp directory: %v", err)
}
defer os.RemoveAll(clientDir)
// Create a buffer to capture client logs
var clientLogBuffer bytes.Buffer
clientLogger := log.New(&clientLogBuffer, "", 0)
// Create Agate options for the server
serverOptions := AgateOptions{
WorkDir: serverDir,
}
// Create Agate options for the client with logger
clientOptions := AgateOptions{
WorkDir: clientDir,
Logger: clientLogger,
}
// Create Agate instances for server and client
serverAgate, err := New(serverOptions)
if err != nil {
t.Fatalf("Failed to create server Agate instance: %v", err)
}
defer serverAgate.Close()
clientAgate, err := New(clientOptions)
if err != nil {
t.Fatalf("Failed to create client Agate instance: %v", err)
}
defer clientAgate.Close()
// Create a data directory on the server
serverDataDir := serverAgate.options.BlobStore.GetActiveDir()
if err := os.MkdirAll(serverDataDir, 0755); err != nil {
t.Fatalf("Failed to create server data directory: %v", err)
}
// Create test files for snapshot A on the server
if err := os.MkdirAll(filepath.Join(serverDataDir, "subdir"), 0755); err != nil {
t.Fatalf("Failed to create subdirectory: %v", err)
}
snapshotAFiles := map[string]string{
filepath.Join(serverDataDir, "file1.txt"): "Content of file 1",
filepath.Join(serverDataDir, "file2.txt"): "Content of file 2",
filepath.Join(serverDataDir, "subdir/file3.txt"): "Content of file 3",
}
for path, content := range snapshotAFiles {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create test file %s: %v", path, err)
}
}
// Create snapshot A on the server
ctx := context.Background()
snapshotAID, err := serverAgate.SaveSnapshot(ctx, "Snapshot A", "")
if err != nil {
t.Fatalf("Failed to create snapshot A: %v", err)
}
t.Logf("Created snapshot A with ID: %s", snapshotAID)
// Modify some files and add a new file for snapshot B
snapshotBChanges := map[string]string{
filepath.Join(serverDataDir, "file1.txt"): "Modified content of file 1", // Modified file
filepath.Join(serverDataDir, "file4.txt"): "Content of new file 4", // New file
filepath.Join(serverDataDir, "subdir/file5.txt"): "Content of new file 5", // New file in subdir
}
for path, content := range snapshotBChanges {
// Ensure parent directory exists
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
t.Fatalf("Failed to create directory for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatalf("Failed to create/modify test file %s: %v", path, err)
}
}
// Create snapshot B on the server (with A as parent)
snapshotBID, err := serverAgate.SaveSnapshot(ctx, "Snapshot B", snapshotAID)
if err != nil {
t.Fatalf("Failed to create snapshot B: %v", err)
}
t.Logf("Created snapshot B with ID: %s", snapshotBID)
// Start the gRPC server
serverAddress := "localhost:50052" // Use a different port than the other test
server, err := remote.RunServer(ctx, serverAgate.manager, serverAddress)
if err != nil {
t.Fatalf("Failed to start gRPC server: %v", err)
}
defer server.Stop(ctx)
// Give the server a moment to start
time.Sleep(100 * time.Millisecond)
// Step 1: Client downloads snapshot A
err = clientAgate.GetRemoteSnapshot(ctx, serverAddress, snapshotAID, "")
if err != nil {
t.Fatalf("Failed to download snapshot A: %v", err)
}
t.Log("Client successfully downloaded snapshot A")
// Clear the log buffer to capture only logs from the incremental download
clientLogBuffer.Reset()
// Step 2: Client downloads snapshot B, specifying A as the local parent
err = clientAgate.GetRemoteSnapshot(ctx, serverAddress, snapshotBID, snapshotAID)
if err != nil {
t.Fatalf("Failed to download snapshot B: %v", err)
}
t.Log("Client successfully downloaded snapshot B")
// Step 3: Verify that snapshot B was correctly imported
// Restore snapshot B to a directory
restoreDir := filepath.Join(clientDir, "restore")
if err := os.MkdirAll(restoreDir, 0755); err != nil {
t.Fatalf("Failed to create restore directory: %v", err)
}
err = clientAgate.RestoreSnapshotToDir(ctx, snapshotBID, restoreDir)
if err != nil {
t.Fatalf("Failed to restore snapshot B: %v", err)
}
// Verify the restored files match the expected content
expectedFiles := map[string]string{
filepath.Join(restoreDir, "file1.txt"): "Modified content of file 1", // Modified file
filepath.Join(restoreDir, "file2.txt"): "Content of file 2", // Unchanged file
filepath.Join(restoreDir, "file4.txt"): "Content of new file 4", // New file
filepath.Join(restoreDir, "subdir/file3.txt"): "Content of file 3", // Unchanged file
filepath.Join(restoreDir, "subdir/file5.txt"): "Content of new file 5", // New file
}
for path, expectedContent := range expectedFiles {
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("Failed to read restored file %s: %v", path, err)
}
if string(content) != expectedContent {
t.Errorf("Restored file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
}
}
// Step 4: Analyze logs to verify incremental download behavior
logs := clientLogBuffer.String()
// Check for evidence of file reuse
if !strings.Contains(logs, "Reusing file") {
t.Errorf("No evidence of file reuse in logs")
}
// Check for evidence of downloading only new/changed files
if !strings.Contains(logs, "Downloading file") {
t.Errorf("No evidence of downloading new files in logs")
}
// Log the relevant parts for debugging
t.Logf("Log evidence of incremental download:\n%s", logs)
}

View File

@ -7,27 +7,40 @@ import (
"gitea.unprism.ru/KRBL/Agate/store" "gitea.unprism.ru/KRBL/Agate/store"
) )
// SnapshotManager defines the interface that the server needs to interact with snapshots // SnapshotManager is an interface that defines operations for managing and interacting with snapshots.
type SnapshotManager interface { type SnapshotManager interface {
// GetSnapshotDetails retrieves detailed metadata for a specific snapshot // CreateSnapshot creates a new snapshot from the specified source directory, associating it with a given name and parent ID.
// Returns the created Snapshot with its metadata or an error if the process fails.
CreateSnapshot(ctx context.Context, sourceDir string, name string, parentID string) (*store.Snapshot, error)
// GetSnapshotDetails retrieves detailed metadata for a specific snapshot identified by its unique snapshotID.
// Returns a Snapshot object containing metadata
GetSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error) GetSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error)
// ListSnapshots retrieves a list of all available snapshots // ListSnapshots retrieves a list of available snapshots with filtering and pagination options.
ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) ListSnapshots(ctx context.Context, opts store.ListOptions) ([]store.SnapshotInfo, error)
// OpenFile retrieves and opens a file from the specified snapshot // DeleteSnapshot removes a snapshot identified by snapshotID. Returns an error if the snapshot does not exist or cannot be deleted.
DeleteSnapshot(ctx context.Context, snapshotID string) error
// OpenFile retrieves and opens a file from the specified snapshot, returning a readable stream and an error, if any.
OpenFile(ctx context.Context, snapshotID string, filePath string) (io.ReadCloser, error) OpenFile(ctx context.Context, snapshotID string, filePath string) (io.ReadCloser, error)
// CreateSnapshot creates a new snapshot from the specified source directory // ExtractSnapshot extracts the contents of a specified snapshot to a target directory at the given path.
CreateSnapshot(ctx context.Context, sourceDir string, name string, parentID string) (*store.Snapshot, error) // If cleanTarget is true, the target directory will be cleaned before extraction.
// Returns an error if the snapshot ID is invalid or the extraction fails.
ExtractSnapshot(ctx context.Context, snapshotID string, path string, cleanTarget bool) error
// UpdateSnapshotMetadata updates the metadata of an existing snapshot, allowing changes to its name.
UpdateSnapshotMetadata(ctx context.Context, snapshotID string, newName string) error
} }
// SnapshotServer defines the interface for a server that can share snapshots // SnapshotServer defines the interface for a server that can share snapshots
type SnapshotServer interface { type SnapshotServer interface {
// Start initializes and begins the server's operation // Start initializes and begins the server's operation, handling incoming requests or processes within the provided context.
Start(ctx context.Context) error Start(ctx context.Context) error
// Stop gracefully shuts down the server // Stop gracefully shuts down the server, releasing any allocated resources and ensuring all operations are completed.
Stop(ctx context.Context) error Stop(ctx context.Context) error
} }

View File

@ -6,9 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"time" "time"
@ -16,20 +16,26 @@ import (
"gitea.unprism.ru/KRBL/Agate/archive" "gitea.unprism.ru/KRBL/Agate/archive"
"gitea.unprism.ru/KRBL/Agate/hash" "gitea.unprism.ru/KRBL/Agate/hash"
"gitea.unprism.ru/KRBL/Agate/interfaces"
"gitea.unprism.ru/KRBL/Agate/store" "gitea.unprism.ru/KRBL/Agate/store"
) )
type SnapshotManagerData struct { type SnapshotManagerData struct {
metadataStore store.MetadataStore metadataStore store.MetadataStore
blobStore store.BlobStore 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 { if metadataStore == nil || blobStore == nil {
return nil, errors.New("parameters can't be 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) { 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 return snapshot, nil
} }
func (data *SnapshotManagerData) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) { func (data *SnapshotManagerData) ListSnapshots(ctx context.Context, opts store.ListOptions) ([]store.SnapshotInfo, error) {
// Retrieve list of snapshots from the metadata store // Retrieve list of snapshots from the metadata store with the provided options
snapshots, err := data.metadataStore.ListSnapshotsMetadata(ctx) snapshots, err := data.metadataStore.ListSnapshotsMetadata(ctx, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list snapshots: %w", err) 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") return errors.New("snapshot ID cannot be empty")
} }
// First check if the snapshot exists // First check if the snapshot exists and get its details
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID) snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil { if err != nil {
if errors.Is(err, ErrNotFound) { if errors.Is(err, ErrNotFound) {
// If snapshot doesn't exist, return success (idempotent operation) // If snapshot doesn't exist, return success (idempotent operation)
@ -187,6 +193,29 @@ func (data *SnapshotManagerData) DeleteSnapshot(ctx context.Context, snapshotID
return fmt.Errorf("failed to check if snapshot exists: %w", err) 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 {
// Используем новый, более надежный метод для обновления только parent_id
if err := data.metadataStore.UpdateSnapshotParentID(ctx, info.ID, parentID); 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 // Delete the metadata first
if err := data.metadataStore.DeleteSnapshotMetadata(ctx, snapshotID); err != nil { if err := data.metadataStore.DeleteSnapshotMetadata(ctx, snapshotID); err != nil {
return fmt.Errorf("failed to delete snapshot metadata: %w", err) return fmt.Errorf("failed to delete snapshot metadata: %w", err)
@ -197,7 +226,7 @@ func (data *SnapshotManagerData) DeleteSnapshot(ctx context.Context, snapshotID
// Note: We don't return here because we've already deleted the metadata // 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 // and the blob store should handle the case where the blob doesn't exist
// Log the error instead // 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 return nil
@ -248,7 +277,7 @@ func (data *SnapshotManagerData) OpenFile(ctx context.Context, snapshotID string
return pr, nil 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 == "" { if snapshotID == "" {
return errors.New("snapshot ID cannot be empty") return errors.New("snapshot ID cannot be empty")
} }
@ -258,8 +287,8 @@ func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID
path = data.blobStore.GetActiveDir() path = data.blobStore.GetActiveDir()
} }
// First check if the snapshot exists and get its metadata // First check if the snapshot exists
snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID) _, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
if err != nil { if err != nil {
if errors.Is(err, ErrNotFound) { if errors.Is(err, ErrNotFound) {
return ErrNotFound return ErrNotFound
@ -273,9 +302,20 @@ func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID
return fmt.Errorf("failed to get blob path: %w", err) return fmt.Errorf("failed to get blob path: %w", err)
} }
// Ensure the target directory exists // If cleanTarget is true, clean the target directory before extraction
if err := os.MkdirAll(path, 0755); err != nil { if cleanTarget {
return fmt.Errorf("failed to create target directory: %w", err) // 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 // Extract the archive to the target directory
@ -283,94 +323,6 @@ func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID
return fmt.Errorf("failed to extract snapshot: %w", err) 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 return nil
} }

View File

@ -90,8 +90,8 @@ func TestCreateAndGetSnapshot(t *testing.T) {
} }
createTestFiles(t, sourceDir) createTestFiles(t, sourceDir)
// Create a snapshot manager // Create a snapshot manager with nil logger
manager, err := CreateSnapshotManager(metadataStore, blobStore) manager, err := CreateSnapshotManager(metadataStore, blobStore, nil)
if err != nil { if err != nil {
t.Fatalf("Failed to create snapshot manager: %v", err) t.Fatalf("Failed to create snapshot manager: %v", err)
} }
@ -142,8 +142,8 @@ func TestListSnapshots(t *testing.T) {
} }
createTestFiles(t, sourceDir) createTestFiles(t, sourceDir)
// Create a snapshot manager // Create a snapshot manager with nil logger
manager, err := CreateSnapshotManager(metadataStore, blobStore) manager, err := CreateSnapshotManager(metadataStore, blobStore, nil)
if err != nil { if err != nil {
t.Fatalf("Failed to create snapshot manager: %v", err) t.Fatalf("Failed to create snapshot manager: %v", err)
} }
@ -165,8 +165,8 @@ func TestListSnapshots(t *testing.T) {
t.Fatalf("Failed to create snapshot: %v", err) t.Fatalf("Failed to create snapshot: %v", err)
} }
// List the snapshots // List the snapshots with empty options
snapshots, err := manager.ListSnapshots(ctx) snapshots, err := manager.ListSnapshots(ctx, store.ListOptions{})
if err != nil { if err != nil {
t.Fatalf("Failed to list snapshots: %v", err) t.Fatalf("Failed to list snapshots: %v", err)
} }
@ -209,8 +209,8 @@ func TestDeleteSnapshot(t *testing.T) {
} }
createTestFiles(t, sourceDir) createTestFiles(t, sourceDir)
// Create a snapshot manager // Create a snapshot manager with nil logger
manager, err := CreateSnapshotManager(metadataStore, blobStore) manager, err := CreateSnapshotManager(metadataStore, blobStore, nil)
if err != nil { if err != nil {
t.Fatalf("Failed to create snapshot manager: %v", err) t.Fatalf("Failed to create snapshot manager: %v", err)
} }
@ -235,7 +235,7 @@ func TestDeleteSnapshot(t *testing.T) {
} }
// List snapshots to confirm it's gone // List snapshots to confirm it's gone
snapshots, err := manager.ListSnapshots(ctx) snapshots, err := manager.ListSnapshots(ctx, store.ListOptions{})
if err != nil { if err != nil {
t.Fatalf("Failed to list snapshots: %v", err) t.Fatalf("Failed to list snapshots: %v", err)
} }
@ -255,8 +255,8 @@ func TestOpenFile(t *testing.T) {
} }
createTestFiles(t, sourceDir) createTestFiles(t, sourceDir)
// Create a snapshot manager // Create a snapshot manager with nil logger
manager, err := CreateSnapshotManager(metadataStore, blobStore) manager, err := CreateSnapshotManager(metadataStore, blobStore, nil)
if err != nil { if err != nil {
t.Fatalf("Failed to create snapshot manager: %v", err) t.Fatalf("Failed to create snapshot manager: %v", err)
} }
@ -309,8 +309,8 @@ func TestExtractSnapshot(t *testing.T) {
} }
createTestFiles(t, sourceDir) createTestFiles(t, sourceDir)
// Create a snapshot manager // Create a snapshot manager with nil logger
manager, err := CreateSnapshotManager(metadataStore, blobStore) manager, err := CreateSnapshotManager(metadataStore, blobStore, nil)
if err != nil { if err != nil {
t.Fatalf("Failed to create snapshot manager: %v", err) t.Fatalf("Failed to create snapshot manager: %v", err)
} }
@ -328,8 +328,8 @@ func TestExtractSnapshot(t *testing.T) {
t.Fatalf("Failed to create target directory: %v", err) t.Fatalf("Failed to create target directory: %v", err)
} }
// Extract the snapshot // Extract the snapshot with default behavior (cleanTarget=false)
err = manager.ExtractSnapshot(ctx, snapshot.ID, targetDir) err = manager.ExtractSnapshot(ctx, snapshot.ID, targetDir, false)
if err != nil { if err != nil {
t.Fatalf("Failed to extract snapshot: %v", err) t.Fatalf("Failed to extract snapshot: %v", err)
} }
@ -353,12 +353,159 @@ func TestExtractSnapshot(t *testing.T) {
} }
// Try to extract a non-existent snapshot // Try to extract a non-existent snapshot
err = manager.ExtractSnapshot(ctx, "nonexistent-id", targetDir) err = manager.ExtractSnapshot(ctx, "nonexistent-id", targetDir, false)
if err == nil { if err == nil {
t.Fatalf("Expected error when extracting non-existent snapshot, got nil") t.Fatalf("Expected error when extracting non-existent snapshot, got nil")
} }
} }
// TestExtractSnapshot_SafeRestore tests that ExtractSnapshot with cleanTarget=false
// does not remove extra files in the target directory
func TestExtractSnapshot_SafeRestore(t *testing.T) {
tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
defer cleanup()
// Create a source directory with test files
sourceDir := filepath.Join(tempDir, "source")
if err := os.MkdirAll(sourceDir, 0755); err != nil {
t.Fatalf("Failed to create source directory: %v", err)
}
createTestFiles(t, sourceDir)
// Create a snapshot manager with nil logger
manager, err := CreateSnapshotManager(metadataStore, blobStore, nil)
if err != nil {
t.Fatalf("Failed to create snapshot manager: %v", err)
}
// Create a snapshot (snapshot A)
ctx := context.Background()
snapshot, err := manager.CreateSnapshot(ctx, sourceDir, "Snapshot A", "")
if err != nil {
t.Fatalf("Failed to create snapshot: %v", err)
}
// Create a target directory and place an "extra" file in it
targetDir := filepath.Join(tempDir, "target")
if err := os.MkdirAll(targetDir, 0755); err != nil {
t.Fatalf("Failed to create target directory: %v", err)
}
extraFilePath := filepath.Join(targetDir, "extra.txt")
if err := os.WriteFile(extraFilePath, []byte("This is an extra file"), 0644); err != nil {
t.Fatalf("Failed to create extra file: %v", err)
}
// Extract the snapshot with cleanTarget=false
err = manager.ExtractSnapshot(ctx, snapshot.ID, targetDir, false)
if err != nil {
t.Fatalf("Failed to extract snapshot: %v", err)
}
// Check that all files from the snapshot were restored
testFiles := map[string]string{
filepath.Join(targetDir, "file1.txt"): "This is file 1",
filepath.Join(targetDir, "file2.txt"): "This is file 2",
filepath.Join(targetDir, "subdir/subfile1.txt"): "This is subfile 1",
filepath.Join(targetDir, "subdir/subfile2.txt"): "This is subfile 2",
}
for path, expectedContent := range testFiles {
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("Failed to read extracted file %s: %v", path, err)
}
if string(content) != expectedContent {
t.Errorf("Extracted file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
}
}
// Check that the extra file was NOT deleted
if _, err := os.Stat(extraFilePath); os.IsNotExist(err) {
t.Errorf("Extra file was deleted, but should have been preserved with cleanTarget=false")
} else if err != nil {
t.Fatalf("Failed to check if extra file exists: %v", err)
} else {
// Read the content to make sure it wasn't modified
content, err := os.ReadFile(extraFilePath)
if err != nil {
t.Fatalf("Failed to read extra file: %v", err)
}
if string(content) != "This is an extra file" {
t.Errorf("Extra file content was modified: got %s, want %s", string(content), "This is an extra file")
}
}
}
// TestExtractSnapshot_CleanRestore tests that ExtractSnapshot with cleanTarget=true
// completely cleans the target directory before restoration
func TestExtractSnapshot_CleanRestore(t *testing.T) {
tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
defer cleanup()
// Create a source directory with test files
sourceDir := filepath.Join(tempDir, "source")
if err := os.MkdirAll(sourceDir, 0755); err != nil {
t.Fatalf("Failed to create source directory: %v", err)
}
createTestFiles(t, sourceDir)
// Create a snapshot manager with nil logger
manager, err := CreateSnapshotManager(metadataStore, blobStore, nil)
if err != nil {
t.Fatalf("Failed to create snapshot manager: %v", err)
}
// Create a snapshot (snapshot A)
ctx := context.Background()
snapshot, err := manager.CreateSnapshot(ctx, sourceDir, "Snapshot A", "")
if err != nil {
t.Fatalf("Failed to create snapshot: %v", err)
}
// Create a target directory and place an "extra" file in it
targetDir := filepath.Join(tempDir, "target")
if err := os.MkdirAll(targetDir, 0755); err != nil {
t.Fatalf("Failed to create target directory: %v", err)
}
extraFilePath := filepath.Join(targetDir, "extra.txt")
if err := os.WriteFile(extraFilePath, []byte("This is an extra file"), 0644); err != nil {
t.Fatalf("Failed to create extra file: %v", err)
}
// Extract the snapshot with cleanTarget=true
err = manager.ExtractSnapshot(ctx, snapshot.ID, targetDir, true)
if err != nil {
t.Fatalf("Failed to extract snapshot: %v", err)
}
// Check that all files from the snapshot were restored
testFiles := map[string]string{
filepath.Join(targetDir, "file1.txt"): "This is file 1",
filepath.Join(targetDir, "file2.txt"): "This is file 2",
filepath.Join(targetDir, "subdir/subfile1.txt"): "This is subfile 1",
filepath.Join(targetDir, "subdir/subfile2.txt"): "This is subfile 2",
}
for path, expectedContent := range testFiles {
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("Failed to read extracted file %s: %v", path, err)
}
if string(content) != expectedContent {
t.Errorf("Extracted file %s has wrong content: got %s, want %s", path, string(content), expectedContent)
}
}
// Check that the extra file WAS deleted
if _, err := os.Stat(extraFilePath); os.IsNotExist(err) {
// This is the expected behavior
} else if err != nil {
t.Fatalf("Failed to check if extra file exists: %v", err)
} else {
t.Errorf("Extra file was not deleted, but should have been removed with cleanTarget=true")
}
}
func TestUpdateSnapshotMetadata(t *testing.T) { func TestUpdateSnapshotMetadata(t *testing.T) {
tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t) tempDir, metadataStore, blobStore, cleanup := setupTestEnvironment(t)
defer cleanup() defer cleanup()
@ -370,8 +517,8 @@ func TestUpdateSnapshotMetadata(t *testing.T) {
} }
createTestFiles(t, sourceDir) createTestFiles(t, sourceDir)
// Create a snapshot manager // Create a snapshot manager with nil logger
manager, err := CreateSnapshotManager(metadataStore, blobStore) manager, err := CreateSnapshotManager(metadataStore, blobStore, nil)
if err != nil { if err != nil {
t.Fatalf("Failed to create snapshot manager: %v", err) t.Fatalf("Failed to create snapshot manager: %v", err)
} }

View File

@ -59,7 +59,10 @@ func (s *Server) Stop(ctx context.Context) error {
// ListSnapshots implements the gRPC ListSnapshots method // ListSnapshots implements the gRPC ListSnapshots method
func (s *Server) ListSnapshots(ctx context.Context, req *agateGrpc.ListSnapshotsRequest) (*agateGrpc.ListSnapshotsResponse, error) { func (s *Server) ListSnapshots(ctx context.Context, req *agateGrpc.ListSnapshotsRequest) (*agateGrpc.ListSnapshotsResponse, error) {
snapshots, err := s.manager.ListSnapshots(ctx) // Create empty ListOptions since the proto doesn't have active filter/pagination fields yet
opts := store.ListOptions{}
snapshots, err := s.manager.ListSnapshots(ctx, opts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to list snapshots: %w", err) return nil, fmt.Errorf("failed to list snapshots: %w", err)
} }

View File

@ -1,53 +0,0 @@
package agate
import (
"context"
"gitea.unprism.ru/KRBL/Agate/store"
"io"
)
// SnapshotManager is an interface that defines operations for managing and interacting with snapshots.
type SnapshotManager interface {
// CreateSnapshot creates a new snapshot from the specified source directory, associating it with a given name and parent ID.
// Returns the created Snapshot with its metadata or an error if the process fails.
CreateSnapshot(ctx context.Context, sourceDir string, name string, parentID string) (*store.Snapshot, error)
// GetSnapshotDetails retrieves detailed metadata for a specific snapshot identified by its unique snapshotID.
// Returns a Snapshot object containing metadata
GetSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error)
// ListSnapshots retrieves a list of all available snapshots, returning their basic information as SnapshotInfo.
ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error)
// DeleteSnapshot removes a snapshot identified by snapshotID. Returns an error if the snapshot does not exist or cannot be deleted.
DeleteSnapshot(ctx context.Context, snapshotID string) error
// OpenFile retrieves and opens a file from the specified snapshot, returning a readable stream and an error, if any.
OpenFile(ctx context.Context, snapshotID string, filePath string) (io.ReadCloser, error)
// ExtractSnapshot extracts the contents of a specified snapshot to a target directory at the given path.
// Returns an error if the snapshot ID is invalid or the extraction fails.
ExtractSnapshot(ctx context.Context, snapshotID string, path string) error
// UpdateSnapshotMetadata updates the metadata of an existing snapshot, allowing changes to its name.
UpdateSnapshotMetadata(ctx context.Context, snapshotID string, newName string) error
}
type SnapshotServer interface {
// Start initializes and begins the server's operation, handling incoming requests or processes within the provided context.
Start(ctx context.Context) error
// Stop gracefully shuts down the server, releasing any allocated resources and ensuring all operations are completed.
Stop(ctx context.Context) error
}
type SnapshotClient interface {
// ListSnapshots retrieves a list of snapshots containing basic metadata, such as ID, name, parent ID, and creation time.
ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error)
// FetchSnapshotDetails retrieves detailed metadata about a specific snapshot identified by snapshotID.
FetchSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error)
// DownloadSnapshot retrieves the snapshot content for the given snapshotID and returns it as an io.ReadCloser.
DownloadSnapshot(ctx context.Context, snapshotID string) (io.ReadCloser, error)
}

View File

@ -167,58 +167,83 @@ func (s *sqliteStore) GetSnapshotMetadata(ctx context.Context, snapshotID string
return &snap, nil return &snap, nil
} }
// ListSnapshotsMetadata извлекает краткую информацию обо всех снапшотах. // ListSnapshotsMetadata retrieves basic information about snapshots with filtering and pagination.
func (s *sqliteStore) ListSnapshotsMetadata(ctx context.Context) ([]store.SnapshotInfo, error) { func (s *sqliteStore) ListSnapshotsMetadata(ctx context.Context, opts store.ListOptions) ([]store.SnapshotInfo, error) {
// Simplified implementation to debug the issue // Build the query with optional filtering
fmt.Println("ListSnapshotsMetadata called") var query string
var args []interface{}
// Get all snapshot IDs first if opts.FilterByName != "" {
query := `SELECT id FROM snapshots ORDER BY creation_time DESC;` query = `SELECT id, name, parent_id, creation_time FROM snapshots WHERE name LIKE ? ORDER BY creation_time DESC`
fmt.Println("Executing query:", query) args = append(args, "%"+opts.FilterByName+"%")
} else {
query = `SELECT id, name, parent_id, creation_time FROM snapshots ORDER BY creation_time DESC`
}
rows, err := s.db.QueryContext(ctx, query) // Add pagination if specified
if opts.Limit > 0 {
query += " LIMIT ?"
args = append(args, opts.Limit)
if opts.Offset > 0 {
query += " OFFSET ?"
args = append(args, opts.Offset)
}
}
// Execute the query
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query snapshot IDs: %w", err) return nil, fmt.Errorf("failed to query snapshots: %w", err)
} }
defer rows.Close() defer rows.Close()
var snapshots []store.SnapshotInfo var snapshots []store.SnapshotInfo
// For each ID, get the full snapshot details // Iterate through the results
for rows.Next() { for rows.Next() {
var id string var info store.SnapshotInfo
if err := rows.Scan(&id); err != nil { var parentID sql.NullString
return nil, fmt.Errorf("failed to scan snapshot ID: %w", err) var creationTimeStr string
if err := rows.Scan(&info.ID, &info.Name, &parentID, &creationTimeStr); err != nil {
return nil, fmt.Errorf("failed to scan snapshot row: %w", err)
} }
// Get the full snapshot details // Set parent ID if not NULL
snapshot, err := s.GetSnapshotMetadata(ctx, id) if parentID.Valid {
if err != nil { info.ParentID = parentID.String
return nil, fmt.Errorf("failed to get snapshot details for ID %s: %w", id, err)
} }
// Convert to SnapshotInfo // Parse creation time
info := store.SnapshotInfo{ const sqliteLayout = "2006-01-02 15:04:05" // Standard SQLite DATETIME format without timezone
ID: snapshot.ID, t, parseErr := time.Parse(sqliteLayout, creationTimeStr)
Name: snapshot.Name, if parseErr != nil {
ParentID: snapshot.ParentID, // Try format with milliseconds if the first one didn't work
CreationTime: snapshot.CreationTime, const sqliteLayoutWithMs = "2006-01-02 15:04:05.999999999"
t, parseErr = time.Parse(sqliteLayoutWithMs, creationTimeStr)
if parseErr != nil {
// Try RFC3339 if saved as UTC().Format(time.RFC3339)
t, parseErr = time.Parse(time.RFC3339, creationTimeStr)
if parseErr != nil {
return nil, fmt.Errorf("failed to parse creation time '%s' for snapshot %s: %w", creationTimeStr, info.ID, parseErr)
}
}
} }
info.CreationTime = t.UTC() // Store as UTC
snapshots = append(snapshots, info) snapshots = append(snapshots, info)
} }
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating snapshot IDs: %w", err) return nil, fmt.Errorf("error iterating snapshot rows: %w", err)
} }
// If no snapshots found, return an empty slice // If no snapshots found, return an empty slice
if len(snapshots) == 0 { if len(snapshots) == 0 {
fmt.Println("No snapshots found")
return []store.SnapshotInfo{}, nil return []store.SnapshotInfo{}, nil
} }
fmt.Printf("Found %d snapshots\n", len(snapshots))
return snapshots, nil return snapshots, nil
} }
@ -240,3 +265,13 @@ func (s *sqliteStore) DeleteSnapshotMetadata(ctx context.Context, snapshotID str
return nil // Не возвращаем ошибку, если запись не найдена return nil // Не возвращаем ошибку, если запись не найдена
} }
// UpdateSnapshotParentID обновляет ParentID для указанного снапшота.
func (s *sqliteStore) UpdateSnapshotParentID(ctx context.Context, snapshotID, newParentID string) error {
query := `UPDATE snapshots SET parent_id = ? WHERE id = ?;`
_, err := s.db.ExecContext(ctx, query, newParentID, snapshotID)
if err != nil {
return fmt.Errorf("failed to update parent ID for snapshot %s: %w", snapshotID, err)
}
return nil
}

View File

@ -154,8 +154,8 @@ func TestListSnapshotsMetadata(t *testing.T) {
} }
} }
// List the snapshots // List the snapshots with empty options
snapshots, err := s.ListSnapshotsMetadata(ctx) snapshots, err := s.ListSnapshotsMetadata(ctx, store.ListOptions{})
if err != nil { if err != nil {
t.Fatalf("Failed to list snapshots: %v", err) t.Fatalf("Failed to list snapshots: %v", err)
} }
@ -189,6 +189,164 @@ func TestListSnapshotsMetadata(t *testing.T) {
} }
} }
func TestListSnapshotsMetadata_WithOptions(t *testing.T) {
// Create a temporary directory for tests
tempDir, err := os.MkdirTemp("", "agate-test-*")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tempDir) // Clean up after test
// Create a new store
dbPath := filepath.Join(tempDir, "test.db")
s, err := NewSQLiteStore(dbPath)
if err != nil {
t.Fatalf("Failed to create SQLite store: %v", err)
}
defer s.Close()
// Create test snapshots with different names
ctx := context.Background()
now := time.Now().UTC().Truncate(time.Second)
testSnapshots := []store.Snapshot{
{
ID: "alpha-1",
Name: "alpha-1",
ParentID: "",
CreationTime: now.Add(-3 * time.Hour),
Files: []store.FileInfo{},
},
{
ID: "alpha-2",
Name: "alpha-2",
ParentID: "alpha-1",
CreationTime: now.Add(-2 * time.Hour),
Files: []store.FileInfo{},
},
{
ID: "beta-1",
Name: "beta-1",
ParentID: "",
CreationTime: now.Add(-1 * time.Hour),
Files: []store.FileInfo{},
},
}
// Save the snapshots
for _, snap := range testSnapshots {
err = s.SaveSnapshotMetadata(ctx, snap)
if err != nil {
t.Fatalf("Failed to save snapshot metadata: %v", err)
}
}
// Test different ListOptions scenarios
t.Run("FilterByName", func(t *testing.T) {
// Filter snapshots by name "alpha"
opts := store.ListOptions{
FilterByName: "alpha",
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with filter: %v", err)
}
// Should return 2 snapshots (alpha-1 and alpha-2)
if len(snapshots) != 2 {
t.Errorf("Wrong number of snapshots returned: got %d, want %d", len(snapshots), 2)
}
// Check that only alpha snapshots are returned
for _, snap := range snapshots {
if snap.ID != "alpha-1" && snap.ID != "alpha-2" {
t.Errorf("Unexpected snapshot ID in filtered results: %s", snap.ID)
}
}
})
t.Run("Limit", func(t *testing.T) {
// Limit to 1 snapshot (should return the newest one)
opts := store.ListOptions{
Limit: 1,
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with limit: %v", err)
}
// Should return 1 snapshot
if len(snapshots) != 1 {
t.Errorf("Wrong number of snapshots returned: got %d, want %d", len(snapshots), 1)
}
// The newest snapshot should be beta-1
if snapshots[0].ID != "beta-1" {
t.Errorf("Wrong snapshot returned with limit: got %s, want %s", snapshots[0].ID, "beta-1")
}
})
t.Run("Offset", func(t *testing.T) {
// Limit to 1 snapshot with offset 1 (should return the second newest)
opts := store.ListOptions{
Limit: 1,
Offset: 1,
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with offset: %v", err)
}
// Should return 1 snapshot
if len(snapshots) != 1 {
t.Errorf("Wrong number of snapshots returned: got %d, want %d", len(snapshots), 1)
}
// The second newest snapshot should be alpha-2
if snapshots[0].ID != "alpha-2" {
t.Errorf("Wrong snapshot returned with offset: got %s, want %s", snapshots[0].ID, "alpha-2")
}
})
t.Run("FilterAndPagination", func(t *testing.T) {
// Filter by "alpha" with limit 1
opts := store.ListOptions{
FilterByName: "alpha",
Limit: 1,
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with filter and pagination: %v", err)
}
// Should return 1 snapshot
if len(snapshots) != 1 {
t.Errorf("Wrong number of snapshots returned: got %d, want %d", len(snapshots), 1)
}
// The newest alpha snapshot should be alpha-2
if snapshots[0].ID != "alpha-2" {
t.Errorf("Wrong snapshot returned with filter and limit: got %s, want %s", snapshots[0].ID, "alpha-2")
}
})
t.Run("NoResults", func(t *testing.T) {
// Filter by a name that doesn't exist
opts := store.ListOptions{
FilterByName: "gamma",
}
snapshots, err := s.ListSnapshotsMetadata(ctx, opts)
if err != nil {
t.Fatalf("Failed to list snapshots with non-matching filter: %v", err)
}
// Should return 0 snapshots
if len(snapshots) != 0 {
t.Errorf("Expected 0 snapshots, got %d", len(snapshots))
}
})
}
func TestDeleteSnapshotMetadata(t *testing.T) { func TestDeleteSnapshotMetadata(t *testing.T) {
// Create a temporary directory for tests // Create a temporary directory for tests
tempDir, err := os.MkdirTemp("", "agate-test-*") tempDir, err := os.MkdirTemp("", "agate-test-*")

View File

@ -31,6 +31,13 @@ type SnapshotInfo struct {
CreationTime time.Time // Время создания CreationTime time.Time // Время создания
} }
// ListOptions provides options for filtering and paginating snapshot lists
type ListOptions struct {
FilterByName string // Filter snapshots by name (substring match)
Limit int // Maximum number of snapshots to return
Offset int // Number of snapshots to skip
}
// MetadataStore определяет интерфейс для хранения и извлечения метаданных снапшотов. // MetadataStore определяет интерфейс для хранения и извлечения метаданных снапшотов.
type MetadataStore interface { type MetadataStore interface {
// SaveSnapshotMetadata сохраняет полные метаданные снапшота, включая список файлов. // SaveSnapshotMetadata сохраняет полные метаданные снапшота, включая список файлов.
@ -41,13 +48,16 @@ type MetadataStore interface {
// Возвращает agate.ErrNotFound, если снапшот не найден. // Возвращает agate.ErrNotFound, если снапшот не найден.
GetSnapshotMetadata(ctx context.Context, snapshotID string) (*Snapshot, error) GetSnapshotMetadata(ctx context.Context, snapshotID string) (*Snapshot, error)
// ListSnapshotsMetadata извлекает краткую информацию обо всех снапшотах. // ListSnapshotsMetadata извлекает краткую информацию о снапшотах с фильтрацией и пагинацией.
ListSnapshotsMetadata(ctx context.Context) ([]SnapshotInfo, error) ListSnapshotsMetadata(ctx context.Context, opts ListOptions) ([]SnapshotInfo, error)
// DeleteSnapshotMetadata удаляет метаданные снапшота по его ID. // DeleteSnapshotMetadata удаляет метаданные снапшота по его ID.
// Не должен возвращать ошибку, если снапшот не найден. // Не должен возвращать ошибку, если снапшот не найден.
DeleteSnapshotMetadata(ctx context.Context, snapshotID string) error DeleteSnapshotMetadata(ctx context.Context, snapshotID string) error
// UpdateSnapshotParentID обновляет ParentID для указанного снапшота.
UpdateSnapshotParentID(ctx context.Context, snapshotID, newParentID string) error
// Close закрывает соединение с хранилищем метаданных. // Close закрывает соединение с хранилищем метаданных.
Close() error Close() error
} }