Introduced `GetActiveDir` and `CleanActiveDir` methods in the blob store to manage a dedicated directory for active snapshot operations. This ensures a clean working state before starting new operations and prevents conflicts. Updated related logic in snapshot creation and restoration to utilize the active directory.
388 lines
11 KiB
Go
388 lines
11 KiB
Go
package agate
|
|
|
|
import (
|
|
"archive/zip"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"gitea.unprism.ru/KRBL/Agate/archive"
|
|
"gitea.unprism.ru/KRBL/Agate/hash"
|
|
"gitea.unprism.ru/KRBL/Agate/store"
|
|
)
|
|
|
|
type SnapshotManagerData struct {
|
|
metadataStore store.MetadataStore
|
|
blobStore store.BlobStore
|
|
}
|
|
|
|
func CreateSnapshotManager(metadataStore store.MetadataStore, blobStore store.BlobStore) (SnapshotManager, error) {
|
|
if metadataStore == nil || blobStore == nil {
|
|
return nil, errors.New("parameters can't be nil")
|
|
}
|
|
|
|
return &SnapshotManagerData{metadataStore, blobStore}, nil
|
|
}
|
|
|
|
func (data *SnapshotManagerData) CreateSnapshot(ctx context.Context, sourceDir string, name string, parentID string) (*store.Snapshot, error) {
|
|
// Validate source directory
|
|
info, err := os.Stat(sourceDir)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, ErrSourceNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to access source directory: %w", err)
|
|
}
|
|
|
|
if !info.IsDir() {
|
|
return nil, ErrSourceNotDirectory
|
|
}
|
|
|
|
// Check if parent exists if specified
|
|
if parentID != "" {
|
|
_, err := data.metadataStore.GetSnapshotMetadata(ctx, parentID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFound) {
|
|
return nil, ErrParentNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to check parent snapshot: %w", err)
|
|
}
|
|
}
|
|
|
|
// Generate a unique ID for the snapshot
|
|
snapshotID := uuid.New().String()
|
|
|
|
// Clean the active directory to avoid conflicts
|
|
if err := data.blobStore.CleanActiveDir(ctx); err != nil {
|
|
return nil, fmt.Errorf("failed to clean active directory: %w", err)
|
|
}
|
|
|
|
// Get the active directory for operations
|
|
activeDir := data.blobStore.GetActiveDir()
|
|
|
|
// Create a temporary file for the archive in the active directory
|
|
tempFilePath := filepath.Join(activeDir, "temp-"+snapshotID+".zip")
|
|
tempFile, err := os.Create(tempFilePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create temporary file in active directory: %w", err)
|
|
}
|
|
tempFile.Close() // Close it as CreateArchive will reopen it
|
|
defer os.Remove(tempFilePath) // Clean up temp file after we're done
|
|
|
|
// Create archive of the source directory
|
|
if err := archive.CreateArchive(sourceDir, tempFilePath); err != nil {
|
|
return nil, fmt.Errorf("failed to create archive: %w", err)
|
|
}
|
|
|
|
// Scan the directory to collect file information
|
|
var files []store.FileInfo
|
|
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Skip the root directory itself
|
|
if path == sourceDir {
|
|
return nil
|
|
}
|
|
|
|
// Create relative path
|
|
relPath, err := filepath.Rel(sourceDir, path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get relative path: %w", err)
|
|
}
|
|
|
|
fileInfo := store.FileInfo{
|
|
Path: filepath.ToSlash(relPath),
|
|
Size: info.Size(),
|
|
IsDir: info.IsDir(),
|
|
}
|
|
|
|
// Calculate hash for files (not directories)
|
|
if !info.IsDir() {
|
|
hash, err := hash.CalculateFileHash(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to calculate hash for %s: %w", path, err)
|
|
}
|
|
fileInfo.SHA256 = hash
|
|
}
|
|
|
|
files = append(files, fileInfo)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan directory: %w", err)
|
|
}
|
|
|
|
// Open the archive file for reading
|
|
archiveFile, err := os.Open(tempFilePath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open archive file: %w", err)
|
|
}
|
|
defer archiveFile.Close()
|
|
|
|
// Store the blob
|
|
_, err = data.blobStore.StoreBlob(ctx, snapshotID, archiveFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to store blob: %w", err)
|
|
}
|
|
|
|
// Create snapshot metadata
|
|
snapshot := store.Snapshot{
|
|
ID: snapshotID,
|
|
Name: name,
|
|
ParentID: parentID,
|
|
CreationTime: time.Now(),
|
|
Files: files,
|
|
}
|
|
|
|
// Save metadata
|
|
if err := data.metadataStore.SaveSnapshotMetadata(ctx, snapshot); err != nil {
|
|
// If metadata save fails, try to clean up the blob
|
|
_ = data.blobStore.DeleteBlob(ctx, snapshotID)
|
|
return nil, fmt.Errorf("failed to save snapshot metadata: %w", err)
|
|
}
|
|
|
|
return &snapshot, nil
|
|
}
|
|
|
|
func (data *SnapshotManagerData) GetSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error) {
|
|
if snapshotID == "" {
|
|
return nil, errors.New("snapshot ID cannot be empty")
|
|
}
|
|
|
|
// Retrieve snapshot metadata from the store
|
|
snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFound) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to retrieve snapshot details: %w", err)
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list snapshots: %w", err)
|
|
}
|
|
|
|
return snapshots, nil
|
|
}
|
|
|
|
func (data *SnapshotManagerData) DeleteSnapshot(ctx context.Context, snapshotID string) error {
|
|
if snapshotID == "" {
|
|
return errors.New("snapshot ID cannot be empty")
|
|
}
|
|
|
|
// First check if the snapshot exists
|
|
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFound) {
|
|
// If snapshot doesn't exist, return success (idempotent operation)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to check if snapshot exists: %w", err)
|
|
}
|
|
|
|
// Delete the metadata first
|
|
if err := data.metadataStore.DeleteSnapshotMetadata(ctx, snapshotID); err != nil {
|
|
return fmt.Errorf("failed to delete snapshot metadata: %w", err)
|
|
}
|
|
|
|
// Then delete the blob
|
|
if err := data.blobStore.DeleteBlob(ctx, snapshotID); err != nil {
|
|
// 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)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (data *SnapshotManagerData) OpenFile(ctx context.Context, snapshotID string, filePath string) (io.ReadCloser, error) {
|
|
if snapshotID == "" {
|
|
return nil, errors.New("snapshot ID cannot be empty")
|
|
}
|
|
if filePath == "" {
|
|
return nil, errors.New("file path cannot be empty")
|
|
}
|
|
|
|
// First check if the snapshot exists
|
|
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFound) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to check if snapshot exists: %w", err)
|
|
}
|
|
|
|
// Get the blob path from the blob store
|
|
blobPath, err := data.blobStore.GetBlobPath(ctx, snapshotID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get blob path: %w", err)
|
|
}
|
|
|
|
// Create a pipe to stream the file content
|
|
pr, pw := io.Pipe()
|
|
|
|
// Extract the file in a goroutine to avoid blocking
|
|
go func() {
|
|
// Close the write end of the pipe when done
|
|
defer pw.Close()
|
|
|
|
// Extract the file from the archive
|
|
err := archive.ExtractFileFromArchive(blobPath, filePath, pw)
|
|
if err != nil {
|
|
if errors.Is(err, archive.ErrFileNotFoundInArchive) {
|
|
pw.CloseWithError(ErrFileNotFound)
|
|
return
|
|
}
|
|
pw.CloseWithError(fmt.Errorf("failed to extract file from archive: %w", err))
|
|
}
|
|
}()
|
|
|
|
return pr, nil
|
|
}
|
|
|
|
func (data *SnapshotManagerData) ExtractSnapshot(ctx context.Context, snapshotID string, path string) error {
|
|
if snapshotID == "" {
|
|
return errors.New("snapshot ID cannot be empty")
|
|
}
|
|
|
|
// If no specific path is provided, use the active directory
|
|
if path == "" {
|
|
// Clean the active directory to avoid conflicts
|
|
if err := data.blobStore.CleanActiveDir(ctx); err != nil {
|
|
return fmt.Errorf("failed to clean active directory: %w", err)
|
|
}
|
|
|
|
path = filepath.Join(data.blobStore.GetActiveDir(), snapshotID)
|
|
}
|
|
|
|
// First check if the snapshot exists
|
|
_, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFound) {
|
|
return ErrNotFound
|
|
}
|
|
return fmt.Errorf("failed to check if snapshot exists: %w", err)
|
|
}
|
|
|
|
// Get the blob path from the blob store
|
|
blobPath, err := data.blobStore.GetBlobPath(ctx, snapshotID)
|
|
if err != nil {
|
|
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)
|
|
}
|
|
|
|
// Extract the archive to the target directory
|
|
if err := extractArchive(blobPath, path); err != nil {
|
|
return fmt.Errorf("failed to extract snapshot: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// extractArchive extracts a ZIP archive to a target directory
|
|
func extractArchive(archivePath, targetDir string) error {
|
|
// Open the ZIP archive
|
|
zipReader, err := zip.OpenReader(archivePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open archive file: %w", err)
|
|
}
|
|
defer zipReader.Close()
|
|
|
|
// Extract each file
|
|
for _, file := range zipReader.File {
|
|
// Construct the full path for the file
|
|
filePath := filepath.Join(targetDir, file.Name)
|
|
|
|
// Check for path traversal attacks
|
|
if !strings.HasPrefix(filePath, targetDir) {
|
|
return fmt.Errorf("invalid file path in archive: %s", file.Name)
|
|
}
|
|
|
|
if file.FileInfo().IsDir() {
|
|
// Create directory
|
|
if err := os.MkdirAll(filePath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", filePath, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Ensure the parent directory exists
|
|
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
|
return fmt.Errorf("failed to create parent directory for %s: %w", filePath, err)
|
|
}
|
|
|
|
// Create the file
|
|
outFile, err := os.Create(filePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file %s: %w", filePath, err)
|
|
}
|
|
|
|
// Open the file in the archive
|
|
inFile, err := file.Open()
|
|
if err != nil {
|
|
outFile.Close()
|
|
return fmt.Errorf("failed to open file %s in archive: %w", file.Name, err)
|
|
}
|
|
|
|
// Copy the content
|
|
_, err = io.Copy(outFile, inFile)
|
|
outFile.Close()
|
|
inFile.Close()
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy content for %s: %w", filePath, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (data *SnapshotManagerData) UpdateSnapshotMetadata(ctx context.Context, snapshotID string, newName string) error {
|
|
if snapshotID == "" {
|
|
return errors.New("snapshot ID cannot be empty")
|
|
}
|
|
if newName == "" {
|
|
return errors.New("new name cannot be empty")
|
|
}
|
|
|
|
// Get the current snapshot metadata
|
|
snapshot, err := data.metadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFound) {
|
|
return ErrNotFound
|
|
}
|
|
return fmt.Errorf("failed to get snapshot metadata: %w", err)
|
|
}
|
|
|
|
// Update the name
|
|
snapshot.Name = newName
|
|
|
|
// Save the updated metadata
|
|
if err := data.metadataStore.SaveSnapshotMetadata(ctx, *snapshot); err != nil {
|
|
return fmt.Errorf("failed to update snapshot metadata: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|