Add RegisterLocalSnapshot and GetRemoteSnapshotMetadata methods
Introduce methods to register local snapshots from archives and to download or restore snapshot metadata, improving snapshot management capabilities. Update README with usage examples.
This commit is contained in:
41
README.md
41
README.md
@ -223,6 +223,41 @@ func main() {
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Registering a Local Snapshot
|
||||
|
||||
You can register a local snapshot from an existing archive file with a specified UUID:
|
||||
|
||||
```go
|
||||
// Register a local snapshot from an archive file
|
||||
archivePath := "/path/to/your/archive.zip"
|
||||
snapshotID := "custom-uuid-for-snapshot"
|
||||
snapshotName := "My Local Snapshot"
|
||||
|
||||
if err := ag.RegisterLocalSnapshot(ctx, archivePath, snapshotID, snapshotName); err != nil {
|
||||
log.Fatalf("Failed to register local snapshot: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### Downloading Only Snapshot Metadata
|
||||
|
||||
You can download only the metadata of a snapshot from a remote server without downloading the actual files:
|
||||
|
||||
```go
|
||||
// Download only the metadata of a snapshot from a remote server
|
||||
remoteAddress := "remote-server:50051"
|
||||
snapshotID := "snapshot-id-to-download"
|
||||
|
||||
if err := ag.GetRemoteSnapshotMetadata(ctx, remoteAddress, snapshotID); err != nil {
|
||||
log.Fatalf("Failed to download snapshot metadata: %v", err)
|
||||
}
|
||||
|
||||
// If you have a local blob but missing metadata, you can restore the metadata
|
||||
// by passing an empty address
|
||||
if err := ag.GetRemoteSnapshotMetadata(ctx, "", snapshotID); err != nil {
|
||||
log.Fatalf("Failed to restore snapshot metadata: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### Creating Incremental Snapshots
|
||||
|
||||
You can create incremental snapshots by specifying a parent snapshot ID:
|
||||
@ -294,6 +329,8 @@ The main entry point for the library.
|
||||
- `ConnectRemote(address string) (*grpc.SnapshotClient, error)` - Connect to a remote server
|
||||
- `GetRemoteSnapshotList(ctx context.Context, address string) ([]store.SnapshotInfo, error)` - List snapshots from a remote server
|
||||
- `GetRemoteSnapshot(ctx context.Context, address string, snapshotID string, localParentID string) error` - Download a snapshot from a remote server
|
||||
- `RegisterLocalSnapshot(ctx context.Context, archivePath string, snapshotID string, name string) error` - Register a local snapshot from an archive path with a specified UUID
|
||||
- `GetRemoteSnapshotMetadata(ctx context.Context, address string, snapshotID string) error` - Download only the metadata of a snapshot from a remote server
|
||||
|
||||
### AgateOptions
|
||||
|
||||
@ -304,7 +341,3 @@ Configuration options for the Agate library.
|
||||
- `CloseFunc func() error` - Called before a snapshot is created or restored
|
||||
- `MetadataStore store.MetadataStore` - Implementation of the metadata store
|
||||
- `BlobStore store.BlobStore` - Implementation of the blob store
|
||||
|
||||
## License
|
||||
|
||||
[Add your license information here]
|
237
api.go
237
api.go
@ -6,12 +6,14 @@ import (
|
||||
"fmt"
|
||||
"gitea.unprism.ru/KRBL/Agate/archive"
|
||||
"gitea.unprism.ru/KRBL/Agate/grpc"
|
||||
"gitea.unprism.ru/KRBL/Agate/hash"
|
||||
"gitea.unprism.ru/KRBL/Agate/interfaces"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitea.unprism.ru/KRBL/Agate/store"
|
||||
"gitea.unprism.ru/KRBL/Agate/stores"
|
||||
@ -381,6 +383,241 @@ func (a *Agate) GetRemoteSnapshotList(ctx context.Context, address string) ([]st
|
||||
return client.ListSnapshots(ctx)
|
||||
}
|
||||
|
||||
// RegisterLocalSnapshot registers a local snapshot from an archive path with a specified UUID.
|
||||
// The archive must be a valid ZIP file containing the snapshot files.
|
||||
// If the UUID already exists, an error will be returned.
|
||||
func (a *Agate) RegisterLocalSnapshot(ctx context.Context, archivePath string, snapshotID string, name string) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
a.options.Logger.Printf("Registering local snapshot from archive %s with ID %s", archivePath, snapshotID)
|
||||
|
||||
// Check if the archive file exists
|
||||
if _, err := os.Stat(archivePath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("archive file does not exist: %w", err)
|
||||
}
|
||||
|
||||
// Check if a snapshot with this ID already exists
|
||||
_, err := a.options.MetadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
||||
if err == nil {
|
||||
return fmt.Errorf("snapshot with ID %s already exists", snapshotID)
|
||||
} else if !errors.Is(err, store.ErrNotFound) {
|
||||
return fmt.Errorf("failed to check if snapshot exists: %w", err)
|
||||
}
|
||||
|
||||
// Create a temporary directory for extracting the archive
|
||||
tempDir := filepath.Join(a.options.WorkDir, "temp_extract", snapshotID)
|
||||
if err := os.MkdirAll(tempDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temporary directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir) // Clean up when done
|
||||
|
||||
// Extract the archive to the temporary directory to analyze its contents
|
||||
if err := extractArchive(archivePath, tempDir); err != nil {
|
||||
return fmt.Errorf("failed to extract archive: %w", err)
|
||||
}
|
||||
|
||||
// Get the list of files in the archive
|
||||
var files []store.FileInfo
|
||||
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip the root directory
|
||||
if path == tempDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the relative path
|
||||
relPath, err := filepath.Rel(tempDir, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get relative path: %w", err)
|
||||
}
|
||||
|
||||
// Calculate SHA256 for files (not directories)
|
||||
var sha256 string
|
||||
if !info.IsDir() {
|
||||
// Calculate the hash directly from the file path
|
||||
sha256, err = hash.CalculateFileHash(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate file hash: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add file info to the list
|
||||
files = append(files, store.FileInfo{
|
||||
Path: relPath,
|
||||
Size: info.Size(),
|
||||
IsDir: info.IsDir(),
|
||||
SHA256: sha256,
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to analyze archive contents: %w", err)
|
||||
}
|
||||
|
||||
// Copy the archive to the blob store
|
||||
archiveFile, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open archive file: %w", err)
|
||||
}
|
||||
defer archiveFile.Close()
|
||||
|
||||
// Store the blob with the specified snapshot ID
|
||||
_, err = a.options.BlobStore.StoreBlob(ctx, snapshotID, archiveFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store blob: %w", err)
|
||||
}
|
||||
|
||||
// Create and save the snapshot metadata
|
||||
snapshot := store.Snapshot{
|
||||
ID: snapshotID,
|
||||
Name: name,
|
||||
ParentID: "",
|
||||
CreationTime: time.Now(),
|
||||
Files: files,
|
||||
}
|
||||
|
||||
err = a.options.MetadataStore.SaveSnapshotMetadata(ctx, snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save snapshot metadata: %w", err)
|
||||
}
|
||||
|
||||
a.options.Logger.Printf("Successfully registered local snapshot with ID %s", snapshotID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRemoteSnapshotMetadata downloads only the metadata of a snapshot from a remote server.
|
||||
// If address is empty, it will try to restore the metadata from the local blob.
|
||||
func (a *Agate) GetRemoteSnapshotMetadata(ctx context.Context, address string, snapshotID string) error {
|
||||
a.mutex.Lock()
|
||||
defer a.mutex.Unlock()
|
||||
|
||||
// Check if the snapshot already exists locally
|
||||
_, err := a.options.MetadataStore.GetSnapshotMetadata(ctx, snapshotID)
|
||||
if err == nil {
|
||||
a.options.Logger.Printf("Snapshot %s already exists locally", snapshotID)
|
||||
return nil
|
||||
} else if !errors.Is(err, store.ErrNotFound) {
|
||||
return fmt.Errorf("failed to check if snapshot exists: %w", err)
|
||||
}
|
||||
|
||||
// If address is provided, download metadata from remote server
|
||||
if address != "" {
|
||||
a.options.Logger.Printf("Downloading metadata for snapshot %s from %s", snapshotID, address)
|
||||
|
||||
client, err := a.ConnectRemote(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to remote server: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
// Get the remote snapshot details
|
||||
remoteSnapshot, err := client.FetchSnapshotDetails(ctx, snapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get snapshot details: %w", err)
|
||||
}
|
||||
|
||||
// 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 downloaded metadata for snapshot %s", snapshotID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If no address is provided, try to restore metadata from the local blob
|
||||
a.options.Logger.Printf("Trying to restore metadata for snapshot %s from local blob", snapshotID)
|
||||
|
||||
// Check if the blob exists
|
||||
blobPath, err := a.options.BlobStore.GetBlobPath(ctx, snapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get blob path: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(blobPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("blob for snapshot %s does not exist", snapshotID)
|
||||
}
|
||||
|
||||
// Create a temporary directory for extracting the archive
|
||||
tempDir := filepath.Join(a.options.WorkDir, "temp_extract", snapshotID)
|
||||
if err := os.MkdirAll(tempDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create temporary directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir) // Clean up when done
|
||||
|
||||
// Extract the archive to the temporary directory to analyze its contents
|
||||
if err := extractArchive(blobPath, tempDir); err != nil {
|
||||
return fmt.Errorf("failed to extract archive: %w", err)
|
||||
}
|
||||
|
||||
// Get the list of files in the archive
|
||||
var files []store.FileInfo
|
||||
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip the root directory
|
||||
if path == tempDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the relative path
|
||||
relPath, err := filepath.Rel(tempDir, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get relative path: %w", err)
|
||||
}
|
||||
|
||||
// Calculate SHA256 for files (not directories)
|
||||
var sha256 string
|
||||
if !info.IsDir() {
|
||||
// Calculate the hash directly from the file path
|
||||
sha256, err = hash.CalculateFileHash(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to calculate file hash: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add file info to the list
|
||||
files = append(files, store.FileInfo{
|
||||
Path: relPath,
|
||||
Size: info.Size(),
|
||||
IsDir: info.IsDir(),
|
||||
SHA256: sha256,
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to analyze archive contents: %w", err)
|
||||
}
|
||||
|
||||
// Create and save the snapshot metadata
|
||||
snapshot := store.Snapshot{
|
||||
ID: snapshotID,
|
||||
Name: snapshotID, // Use the ID as the name since we don't have a better name
|
||||
ParentID: "",
|
||||
CreationTime: time.Now(),
|
||||
Files: files,
|
||||
}
|
||||
|
||||
err = a.options.MetadataStore.SaveSnapshotMetadata(ctx, snapshot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to save snapshot metadata: %w", err)
|
||||
}
|
||||
|
||||
a.options.Logger.Printf("Successfully restored metadata for snapshot %s from local blob", snapshotID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRemoteSnapshot downloads a snapshot from a remote server.
|
||||
// If localParentID is provided, it will be used to optimize the download by skipping files that already exist locally.
|
||||
func (a *Agate) GetRemoteSnapshot(ctx context.Context, address string, snapshotID string, localParentID string) error {
|
||||
|
Reference in New Issue
Block a user