Agate/manager.go
Alexander Lazarenko 7e9cea9227
Add initial implementation of Agate snapshot library
Introduces core functionality for the Agate library, including snapshot creation, restoration, listing, and deletion. Adds examples for basic usage, gRPC proto definitions, and build/configuration files such as `go.mod` and `Makefile`. The implementation establishes the framework for store integration and placeholder server functionality.
2025-04-24 02:44:16 +03:00

373 lines
10 KiB
Go

package agate
import (
"archive/zip"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/uuid"
"unprism.ru/KRBL/agate/archive"
"unprism.ru/KRBL/agate/hash"
"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()
// Create a temporary file for the archive
tempFile, err := os.CreateTemp("", "agate-snapshot-*.zip")
if err != nil {
return nil, fmt.Errorf("failed to create temporary file: %w", err)
}
tempFilePath := tempFile.Name()
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 path == "" {
return errors.New("target 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 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
}