243 lines
6.9 KiB
Go
243 lines
6.9 KiB
Go
package remote
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
stdgrpc "google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
|
|
agateGrpc "gitea.unprism.ru/KRBL/Agate/grpc"
|
|
"gitea.unprism.ru/KRBL/Agate/interfaces"
|
|
"gitea.unprism.ru/KRBL/Agate/store"
|
|
)
|
|
|
|
// Client represents a client for connecting to a remote snapshot server
|
|
// It implements the interfaces.SnapshotClient interface
|
|
type Client struct {
|
|
conn *stdgrpc.ClientConn
|
|
client agateGrpc.SnapshotServiceClient
|
|
}
|
|
|
|
// Ensure Client implements interfaces.SnapshotClient
|
|
var _ interfaces.SnapshotClient = (*Client)(nil)
|
|
|
|
// NewClient creates a new client connected to the specified address
|
|
func NewClient(address string) (*Client, error) {
|
|
// Connect to the server with insecure credentials (for simplicity)
|
|
conn, err := stdgrpc.Dial(address, stdgrpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to server at %s: %w", address, err)
|
|
}
|
|
|
|
// Create the gRPC client
|
|
client := agateGrpc.NewSnapshotServiceClient(conn)
|
|
|
|
return &Client{
|
|
conn: conn,
|
|
client: client,
|
|
}, nil
|
|
}
|
|
|
|
// Close closes the connection to the server
|
|
func (c *Client) Close() error {
|
|
if c.conn != nil {
|
|
return c.conn.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListSnapshots retrieves a list of snapshots from the remote server
|
|
func (c *Client) ListSnapshots(ctx context.Context) ([]store.SnapshotInfo, error) {
|
|
response, err := c.client.ListSnapshots(ctx, &agateGrpc.ListSnapshotsRequest{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list snapshots: %w", err)
|
|
}
|
|
|
|
// Convert gRPC snapshot info to store.SnapshotInfo
|
|
snapshots := make([]store.SnapshotInfo, 0, len(response.Snapshots))
|
|
for _, snapshot := range response.Snapshots {
|
|
snapshots = append(snapshots, store.SnapshotInfo{
|
|
ID: snapshot.Id,
|
|
Name: snapshot.Name,
|
|
ParentID: snapshot.ParentId,
|
|
CreationTime: snapshot.CreationTime.AsTime(),
|
|
})
|
|
}
|
|
|
|
return snapshots, nil
|
|
}
|
|
|
|
// FetchSnapshotDetails retrieves detailed information about a specific snapshot
|
|
func (c *Client) FetchSnapshotDetails(ctx context.Context, snapshotID string) (*store.Snapshot, error) {
|
|
response, err := c.client.GetSnapshotDetails(ctx, &agateGrpc.GetSnapshotDetailsRequest{
|
|
SnapshotId: snapshotID,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get snapshot details: %w", err)
|
|
}
|
|
|
|
// Convert gRPC snapshot details to store.Snapshot
|
|
snapshot := &store.Snapshot{
|
|
ID: response.Info.Id,
|
|
Name: response.Info.Name,
|
|
ParentID: response.Info.ParentId,
|
|
CreationTime: response.Info.CreationTime.AsTime(),
|
|
Files: make([]store.FileInfo, 0, len(response.Files)),
|
|
}
|
|
|
|
// Convert file info
|
|
for _, file := range response.Files {
|
|
snapshot.Files = append(snapshot.Files, store.FileInfo{
|
|
Path: file.Path,
|
|
Size: file.SizeBytes,
|
|
IsDir: file.IsDir,
|
|
SHA256: file.Sha256Hash,
|
|
})
|
|
}
|
|
|
|
return snapshot, nil
|
|
}
|
|
|
|
// DownloadSnapshot downloads a snapshot from the server
|
|
// This implementation downloads each file individually to optimize bandwidth usage
|
|
func (c *Client) DownloadSnapshot(ctx context.Context, snapshotID string, targetDir string, localParentID string) error {
|
|
// Get snapshot details
|
|
snapshot, err := c.FetchSnapshotDetails(ctx, snapshotID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get snapshot details: %w", err)
|
|
}
|
|
|
|
// Create target directory if it doesn't exist
|
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create target directory: %w", err)
|
|
}
|
|
|
|
// If a local parent is specified, get its details to compare files
|
|
var localParentFiles map[string]store.FileInfo
|
|
if localParentID != "" {
|
|
localParent, err := c.FetchSnapshotDetails(ctx, localParentID)
|
|
if err == nil {
|
|
// Create a map of file paths to file info for quick lookup
|
|
localParentFiles = make(map[string]store.FileInfo, len(localParent.Files))
|
|
for _, file := range localParent.Files {
|
|
localParentFiles[file.Path] = file
|
|
}
|
|
}
|
|
}
|
|
|
|
// Download each file
|
|
for _, file := range snapshot.Files {
|
|
// Skip directories, we'll create them when needed
|
|
if file.IsDir {
|
|
// Create directory
|
|
dirPath := filepath.Join(targetDir, file.Path)
|
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", dirPath, err)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Check if we can skip downloading this file
|
|
if localParentFiles != nil {
|
|
if parentFile, exists := localParentFiles[file.Path]; exists && parentFile.SHA256 == file.SHA256 {
|
|
// File exists in parent with same hash, copy it instead of downloading
|
|
parentFilePath := filepath.Join(targetDir, "..", localParentID, file.Path)
|
|
targetFilePath := filepath.Join(targetDir, file.Path)
|
|
|
|
// Ensure parent directory exists
|
|
if err := os.MkdirAll(filepath.Dir(targetFilePath), 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory for %s: %w", targetFilePath, err)
|
|
}
|
|
|
|
// Copy the file
|
|
if err := copyFile(parentFilePath, targetFilePath); err != nil {
|
|
// If copy fails, fall back to downloading
|
|
fmt.Printf("Failed to copy file %s, will download instead: %v\n", file.Path, err)
|
|
} else {
|
|
// Skip to next file
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// Download the file
|
|
if err := c.downloadFile(ctx, snapshotID, file.Path, filepath.Join(targetDir, file.Path)); err != nil {
|
|
return fmt.Errorf("failed to download file %s: %w", file.Path, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// downloadFile downloads a single file from the server
|
|
func (c *Client) downloadFile(ctx context.Context, snapshotID, filePath, targetPath string) error {
|
|
// Create the request
|
|
req := &agateGrpc.DownloadFileRequest{
|
|
SnapshotId: snapshotID,
|
|
FilePath: filePath,
|
|
}
|
|
|
|
// Start streaming the file
|
|
stream, err := c.client.DownloadFile(ctx, req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start file download: %w", err)
|
|
}
|
|
|
|
// Ensure the target directory exists
|
|
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory for %s: %w", targetPath, err)
|
|
}
|
|
|
|
// Create the target file
|
|
file, err := os.Create(targetPath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create file %s: %w", targetPath, err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Receive and write chunks
|
|
for {
|
|
resp, err := stream.Recv()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("error receiving file chunk: %w", err)
|
|
}
|
|
|
|
// Write the chunk to the file
|
|
if _, err := file.Write(resp.ChunkData); err != nil {
|
|
return fmt.Errorf("error writing to file: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper function to copy a file
|
|
func copyFile(src, dst string) error {
|
|
sourceFile, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sourceFile.Close()
|
|
|
|
destFile, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
_, err = io.Copy(destFile, sourceFile)
|
|
return err
|
|
}
|
|
|
|
// Connect creates a new client connected to the specified address
|
|
func Connect(address string) (*Client, error) {
|
|
return NewClient(address)
|
|
}
|