diff --git a/cmd/zfs-restore/main.go b/cmd/zfs-restore/main.go index b12424f..f3fb6a5 100644 --- a/cmd/zfs-restore/main.go +++ b/cmd/zfs-restore/main.go @@ -103,6 +103,34 @@ func main() { os.Exit(1) } + case "mount": + // Mount a restored dataset to access files + if len(os.Args) < 3 { + fmt.Println("Usage: zfs-restore mount [mountpoint]") + fmt.Println("\nExamples:") + fmt.Println(" zfs-restore mount tank/restored /mnt/recover") + fmt.Println(" zfs-restore mount tank/restored # interactive") + os.Exit(1) + } + + dataset := os.Args[2] + mountpoint := "" + + if len(os.Args) > 3 { + mountpoint = os.Args[3] + } else { + fmt.Printf("Mountpoint [/mnt/recover]: ") + fmt.Scanln(&mountpoint) + if mountpoint == "" { + mountpoint = "/mnt/recover" + } + } + + if err := client.MountDataset(dataset, mountpoint); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + case "help", "-h", "--help": printUsage() @@ -119,11 +147,13 @@ func printUsage() { fmt.Println("\nCommands:") fmt.Println(" list - List available snapshots") fmt.Println(" restore <#|latest> [--force] - Restore snapshot to ZFS") + fmt.Println(" mount [mountpoint] - Mount dataset to recover files") fmt.Println(" help - Show this help message") fmt.Println("\nQuick Examples:") fmt.Println(" zfs-restore list - See available backups") fmt.Println(" zfs-restore restore latest tank/data - Restore most recent backup") fmt.Println(" zfs-restore restore 1 tank/restored - Restore snapshot #1") + fmt.Println(" zfs-restore mount tank/restored /mnt - Mount to recover files") fmt.Println("\nEnvironment Variables (can be set in .env file):") fmt.Println(" CLIENT_ID - Client identifier (default: client1)") fmt.Println(" API_KEY - API key for authentication (default: secret123)") diff --git a/internal/restore/restore.go b/internal/restore/restore.go index 32b2aa7..414cc4b 100644 --- a/internal/restore/restore.go +++ b/internal/restore/restore.go @@ -148,6 +148,26 @@ func (c *Client) RestoreSnapshot(snapshot *SnapshotMetadata, targetDataset strin } } + // If force is used, we need to handle carefully to avoid data loss + // First, let's try to download - only destroy if download succeeds + var originalExists bool + if force { + if _, err := zfs.GetDataset(targetDataset); err == nil { + 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) + } + } + } + // Request download from server downloadURL := fmt.Sprintf("%s/download?client_id=%s&api_key=%s&snapshot_id=%s", c.ServerURL, c.ClientID, c.APIKey, snapshot.SnapshotID) @@ -165,6 +185,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 +226,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 +281,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 +306,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 }