|
|
|
|
@@ -119,7 +119,7 @@ func (c *Client) DisplaySnapshots(snapshots []*SnapshotMetadata) {
|
|
|
|
|
|
|
|
|
|
// RestoreSnapshot downloads and restores a snapshot to a local ZFS dataset.
|
|
|
|
|
// If force is true, existing datasets will be overwritten.
|
|
|
|
|
func (c *Client) RestoreSnapshot(snapshot *SnapshotMetadata, targetDataset string, force bool) error {
|
|
|
|
|
func (c *Client) RestoreSnapshot(snapshot *SnapshotMetadata, targetDataset string, force bool, allSnapshots []*SnapshotMetadata) error {
|
|
|
|
|
fmt.Printf("\n=== Restoring Snapshot ===\n")
|
|
|
|
|
fmt.Printf("Source: %s\n", snapshot.SnapshotID)
|
|
|
|
|
fmt.Printf("Target: %s\n", targetDataset)
|
|
|
|
|
@@ -128,23 +128,80 @@ func (c *Client) RestoreSnapshot(snapshot *SnapshotMetadata, targetDataset strin
|
|
|
|
|
fmt.Printf("Compressed: %v\n", snapshot.Compressed)
|
|
|
|
|
fmt.Printf("Incremental: %v\n\n", snapshot.Incremental)
|
|
|
|
|
|
|
|
|
|
// For incremental snapshots, we need special handling
|
|
|
|
|
if snapshot.Incremental && force {
|
|
|
|
|
// Check if target dataset exists
|
|
|
|
|
if _, err := zfs.GetDataset(targetDataset); err == nil {
|
|
|
|
|
fmt.Printf("→ Destroying existing dataset for incremental restore...\n")
|
|
|
|
|
// Destroy the existing dataset to allow clean restore
|
|
|
|
|
cmd := exec.Command("zfs", "destroy", "-r", targetDataset)
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
fmt.Printf(" Warning: could not destroy dataset (may not exist): %v\n", err)
|
|
|
|
|
// For incremental snapshots, we need to restore base first
|
|
|
|
|
if snapshot.Incremental && snapshot.BaseSnapshot != "" {
|
|
|
|
|
fmt.Printf("\n⚠ This is an INCREMENTAL backup.\n")
|
|
|
|
|
fmt.Printf(" Base snapshot needed: %s\n\n", snapshot.BaseSnapshot)
|
|
|
|
|
|
|
|
|
|
// Find the base snapshot in the list
|
|
|
|
|
var baseSnap *SnapshotMetadata
|
|
|
|
|
for _, s := range allSnapshots {
|
|
|
|
|
if s.SnapshotID == snapshot.BaseSnapshot {
|
|
|
|
|
baseSnap = s
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if baseSnap == nil {
|
|
|
|
|
return fmt.Errorf("base snapshot %s not found on server. Cannot restore incremental without base", snapshot.BaseSnapshot)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("Found base snapshot:\n")
|
|
|
|
|
fmt.Printf(" - %s from %s (%.2f GB)\n\n",
|
|
|
|
|
baseSnap.SnapshotID,
|
|
|
|
|
baseSnap.Timestamp.Format("2006-01-02 15:04:05"),
|
|
|
|
|
float64(baseSnap.SizeBytes)/(1024*1024*1024))
|
|
|
|
|
|
|
|
|
|
fmt.Printf("To restore this incremental, I need to:\n")
|
|
|
|
|
fmt.Printf(" 1. Restore base snapshot: %s\n", baseSnap.SnapshotID)
|
|
|
|
|
fmt.Printf(" 2. Apply incremental: %s\n\n", snapshot.SnapshotID)
|
|
|
|
|
|
|
|
|
|
// Ask for confirmation
|
|
|
|
|
fmt.Printf("Continue? [y/N]: ")
|
|
|
|
|
var confirm string
|
|
|
|
|
fmt.Scanln(&confirm)
|
|
|
|
|
if confirm != "y" && confirm != "Y" {
|
|
|
|
|
fmt.Println("Cancelled.")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// First restore the base snapshot
|
|
|
|
|
fmt.Printf("\n→ Restoring base snapshot...\n")
|
|
|
|
|
if err := c.restoreOneSnapshot(baseSnap, targetDataset, true); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to restore base snapshot: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then apply the incremental
|
|
|
|
|
fmt.Printf("\n→ Applying incremental snapshot...\n")
|
|
|
|
|
if err := c.restoreOneSnapshot(snapshot, targetDataset, false); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to apply incremental: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("\n✓ Incremental restore completed!\n")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if target dataset exists
|
|
|
|
|
if !force {
|
|
|
|
|
return c.restoreOneSnapshot(snapshot, targetDataset, force)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// restoreOneSnapshot downloads and restores a single snapshot
|
|
|
|
|
func (c *Client) restoreOneSnapshot(snapshot *SnapshotMetadata, targetDataset string, force bool) error {
|
|
|
|
|
// First, let's try to download - only destroy if download succeeds
|
|
|
|
|
var originalExists bool
|
|
|
|
|
if force {
|
|
|
|
|
if _, err := zfs.GetDataset(targetDataset); err == nil {
|
|
|
|
|
return fmt.Errorf("target dataset %s already exists. Use --force to overwrite", targetDataset)
|
|
|
|
|
originalExists = true
|
|
|
|
|
fmt.Printf("→ Target dataset exists, will overwrite\n")
|
|
|
|
|
} else {
|
|
|
|
|
originalExists = false
|
|
|
|
|
fmt.Printf("→ Target dataset does not exist, will create new\n")
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Check if target dataset exists
|
|
|
|
|
if !force {
|
|
|
|
|
if _, err := zfs.GetDataset(targetDataset); err == nil {
|
|
|
|
|
return fmt.Errorf("target dataset %s already exists. Use --force to overwrite", targetDataset)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -165,6 +222,18 @@ func (c *Client) RestoreSnapshot(snapshot *SnapshotMetadata, targetDataset strin
|
|
|
|
|
return fmt.Errorf("download failed: %s", body)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Download succeeded - now safe to destroy if needed
|
|
|
|
|
if force && originalExists {
|
|
|
|
|
fmt.Printf("→ Destroying existing dataset %s...\n", targetDataset)
|
|
|
|
|
cmd := exec.Command("zfs", "destroy", "-r", targetDataset)
|
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf(" Destroy output: %s\n", string(output))
|
|
|
|
|
return fmt.Errorf("failed to destroy existing dataset: %v", err)
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf(" Destroyed successfully\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create decompression reader if needed
|
|
|
|
|
var reader io.Reader = resp.Body
|
|
|
|
|
if snapshot.Compressed {
|
|
|
|
|
@@ -194,6 +263,13 @@ func (c *Client) RestoreSnapshot(snapshot *SnapshotMetadata, targetDataset strin
|
|
|
|
|
fmt.Printf("\n✓ Snapshot restored successfully!\n")
|
|
|
|
|
fmt.Printf(" Dataset: %s\n", targetDataset)
|
|
|
|
|
|
|
|
|
|
// Verify the dataset exists after restore
|
|
|
|
|
if _, err := zfs.GetDataset(targetDataset); err == nil {
|
|
|
|
|
fmt.Printf(" Verified: dataset exists\n")
|
|
|
|
|
} else {
|
|
|
|
|
fmt.Printf(" Warning: could not verify dataset exists: %v\n", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -242,15 +318,22 @@ func (c *Client) RestoreToFile(snapshot *SnapshotMetadata, outputFile string) er
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MountSnapshot mounts a restored dataset to a specified mountpoint.
|
|
|
|
|
// This allows browsing the restored files.
|
|
|
|
|
func (c *Client) MountSnapshot(dataset, mountpoint string) error {
|
|
|
|
|
// MountDataset mounts a restored dataset to a specified mountpoint for file recovery.
|
|
|
|
|
func (c *Client) MountDataset(dataset, mountpoint string) error {
|
|
|
|
|
fmt.Printf("\n=== Mounting Dataset ===\n")
|
|
|
|
|
fmt.Printf("Dataset: %s\n", dataset)
|
|
|
|
|
fmt.Printf("Mountpoint: %s\n\n", mountpoint)
|
|
|
|
|
|
|
|
|
|
ds, err := zfs.GetDataset(dataset)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("dataset not found: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create mountpoint if it doesn't exist
|
|
|
|
|
// Check current mountpoint
|
|
|
|
|
currentMP, _ := ds.GetProperty("mountpoint")
|
|
|
|
|
fmt.Printf("Current mountpoint: %s\n", currentMP)
|
|
|
|
|
|
|
|
|
|
// Create mountpoint directory if it doesn't exist
|
|
|
|
|
if err := os.MkdirAll(mountpoint, 0755); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to create mountpoint: %v", err)
|
|
|
|
|
}
|
|
|
|
|
@@ -260,13 +343,17 @@ func (c *Client) MountSnapshot(dataset, mountpoint string) error {
|
|
|
|
|
return fmt.Errorf("failed to set mountpoint: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mount the dataset
|
|
|
|
|
// Mount the dataset if not already mounted
|
|
|
|
|
cmd := exec.Command("zfs", "mount", dataset)
|
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
|
|
|
return fmt.Errorf("failed to mount: %v", err)
|
|
|
|
|
// Might already be mounted, that's OK
|
|
|
|
|
fmt.Printf(" (dataset may already be mounted)\n")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fmt.Printf("✓ Mounted %s at %s\n", dataset, mountpoint)
|
|
|
|
|
fmt.Printf("\n✓ Mounted successfully!\n")
|
|
|
|
|
fmt.Printf(" Access files at: %s\n", mountpoint)
|
|
|
|
|
fmt.Printf(" When done, run: umount %s\n", mountpoint)
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|