package backup import ( client "backea/internal/client" "bufio" "context" "fmt" "log" "os" "os/exec" "path/filepath" "strings" ) // RestoreManager handles restoring files from Kopia backups type RestoreManager struct { B2Client *client.Client } // NewRestoreManager creates a new restore manager func NewRestoreManager(b2Client *client.Client) *RestoreManager { return &RestoreManager{ B2Client: b2Client, } } // getStoredPassword retrieves a password from kopia.env file func (r *RestoreManager) getStoredPassword(serviceName string) (string, error) { // Define the expected key in the env file passwordKey := fmt.Sprintf("KOPIA_%s_PASSWORD", strings.ToUpper(serviceName)) // Try to read from kopia.env kopiaEnvPath := "kopia.env" if _, err := os.Stat(kopiaEnvPath); err != nil { return "", fmt.Errorf("kopia.env file not found: %w", err) } // File exists, check if the password is there file, err := os.Open(kopiaEnvPath) if err != nil { return "", fmt.Errorf("failed to open kopia.env: %w", err) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, passwordKey+"=") { parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { // Found the password, remove quotes if present password := parts[1] password = strings.Trim(password, "\"") return password, nil } } } if err := scanner.Err(); err != nil { return "", fmt.Errorf("error reading kopia.env: %w", err) } return "", fmt.Errorf("password for service %s not found in kopia.env", serviceName) } // connectToRepository connects to an existing Kopia repository func (r *RestoreManager) connectToRepository(ctx context.Context, serviceName string) error { if r.B2Client == nil { return fmt.Errorf("B2 client not initialized") } // Load environment variables for B2 credentials keyID := os.Getenv("B2_KEY_ID") applicationKey := os.Getenv("B2_APPLICATION_KEY") if keyID == "" || applicationKey == "" { return fmt.Errorf("B2_KEY_ID and B2_APPLICATION_KEY must be set in .env file") } // Get stored password for this service password, err := r.getStoredPassword(serviceName) if err != nil { return fmt.Errorf("failed to get stored password: %w", err) } // Generate bucket name from service name bucketName := fmt.Sprintf("backea-%s", strings.ToLower(serviceName)) // Check if bucket exists _, err = r.B2Client.GetBucket(ctx, bucketName) if err != nil { return fmt.Errorf("bucket %s not found: %w", bucketName, err) } // Check if kopia repository is already connected cmd := exec.Command("kopia", "repository", "status") err = cmd.Run() if err == nil { // Already connected log.Printf("Already connected to a repository, disconnecting first") disconnectCmd := exec.Command("kopia", "repository", "disconnect") if err := disconnectCmd.Run(); err != nil { return fmt.Errorf("failed to disconnect from current repository: %w", err) } } // Connect to the repository log.Printf("Connecting to B2 repository for %s", serviceName) connectCmd := exec.Command( "kopia", "repository", "connect", "b2", "--bucket", bucketName, "--key-id", keyID, "--key", applicationKey, "--password", password, ) connectOutput, err := connectCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to connect to repository: %w\nOutput: %s", err, connectOutput) } log.Printf("Successfully connected to repository for %s", serviceName) return nil } // ListSnapshots lists all snapshots in the repository for a given service func (r *RestoreManager) ListSnapshots(ctx context.Context, serviceName string) error { // Connect to the repository if err := r.connectToRepository(ctx, serviceName); err != nil { return err } // List all snapshots log.Printf("Listing snapshots for %s", serviceName) listCmd := exec.Command("kopia", "snapshot", "list") listOutput, err := listCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to list snapshots: %w\nOutput: %s", err, listOutput) } // Print the output fmt.Println("Available snapshots:") fmt.Println(string(listOutput)) return nil } // RestoreFile restores a file or directory from a specific snapshot to a target location func (r *RestoreManager) RestoreFile(ctx context.Context, serviceName, snapshotID, sourcePath, targetPath string) error { // Connect to the repository if err := r.connectToRepository(ctx, serviceName); err != nil { return err } // Create a subdirectory with the snapshot ID name snapshotDirPath := filepath.Join(targetPath, snapshotID) if err := os.MkdirAll(snapshotDirPath, 0755); err != nil { return fmt.Errorf("failed to create target directory: %w", err) } // Construct the kopia restore command log.Printf("Restoring from snapshot %s to %s", snapshotID, snapshotDirPath) // Build the command with all required parameters restoreCmd := exec.Command( "kopia", "snapshot", "restore", snapshotID, // Just the snapshot ID snapshotDirPath, // Target location where files will be restored ) // Execute the command and capture output restoreOutput, err := restoreCmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to restore files: %w\nOutput: %s", err, restoreOutput) } log.Printf("Successfully restored files to %s", snapshotDirPath) log.Printf("Restore output: %s", string(restoreOutput)) return nil }