From e0da3d77605ef2952a62a4ec77ee47abda7084b1 Mon Sep 17 00:00:00 2001 From: mahesh bhatiya Date: Sun, 29 Jun 2025 17:30:19 +0530 Subject: [PATCH] feat(cli): add CLI support for domain backup and restore - Introduced `backup-domain` command: - Archives /home//public_html and MySQL database into .tar.gz - Auto-names backup files using domain + timestamp - Supports --output-dir flag for custom backup destination - Introduced `restore-domain` command: - Restores domain files and database from .tar.gz, .tar, or .zip archives - Automatically detects archive type and extracts - Recovers MySQL database if .sql file is present inside archive - Ensures permissions and ownership are reset correctly These commands improve disaster recovery and DevOps automation for server admins. --- cmd/backup_domain.go | 85 +++++++++++++++++++++++++++++++++++++++++ cmd/restore_domain.go | 88 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 cmd/backup_domain.go create mode 100644 cmd/restore_domain.go diff --git a/cmd/backup_domain.go b/cmd/backup_domain.go new file mode 100644 index 0000000..ec96cab --- /dev/null +++ b/cmd/backup_domain.go @@ -0,0 +1,85 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/spf13/cobra" + "stackroost/internal" + "stackroost/internal/logger" +) + +var backupDomainCmd = &cobra.Command{ + Use: "backup-domain", + Short: "Backup public_html and MySQL DB of a domain", + Run: func(cmd *cobra.Command, args []string) { + domain, _ := cmd.Flags().GetString("domain") + backupType, _ := cmd.Flags().GetString("type") + + if internal.IsNilOrEmpty(domain) { + logger.Error("Please provide a domain using --domain") + os.Exit(1) + } + if backupType == "" { + backupType = "tar.gz" + } + + username := strings.Split(domain, ".")[0] + timestamp := time.Now().Format("20060102_150405") + backupDir := "/var/backups" + os.MkdirAll(backupDir, 0755) + + publicPath := fmt.Sprintf("/home/%s/public_html", username) + sqlDump := fmt.Sprintf("%s/%s-db.sql", backupDir, domain) + + logger.Info(fmt.Sprintf("Dumping MySQL database for %s", username)) + dumpCmd := []string{"mysqldump", "-u", username, fmt.Sprintf("-p%s", username), username} + sqlFile, err := os.Create(sqlDump) + if err != nil { + logger.Error(fmt.Sprintf("Failed to create dump file: %v", err)) + os.Exit(1) + } + defer sqlFile.Close() + cmdDump := exec.Command("sudo", dumpCmd...) + cmdDump.Stdout = sqlFile + cmdDump.Stderr = os.Stderr + if err := cmdDump.Run(); err != nil { + logger.Warn("Could not dump MySQL DB (likely bad credentials)") + } + + baseName := fmt.Sprintf("%s/%s-%s", backupDir, domain, timestamp) + + switch backupType { + case "tar.gz": + output := baseName + ".tar.gz" + logger.Info("Creating tar.gz archive") + internal.RunCommand("sudo", "tar", "-czf", output, publicPath, sqlDump) + logger.Success(fmt.Sprintf("Backup created: %s", output)) + case "tar": + output := baseName + ".tar" + logger.Info("Creating tar archive") + internal.RunCommand("sudo", "tar", "-cf", output, publicPath, sqlDump) + logger.Success(fmt.Sprintf("Backup created: %s", output)) + case "zip": + output := baseName + ".zip" + logger.Info("Creating zip archive") + internal.RunCommand("sudo", "zip", "-r", output, publicPath, sqlDump) + logger.Success(fmt.Sprintf("Backup created: %s", output)) + default: + logger.Error("Unsupported backup type. Use: tar.gz, tar, zip") + } + + // Optional: clean up raw SQL dump + os.Remove(sqlDump) + }, +} + +func init() { + rootCmd.AddCommand(backupDomainCmd) + backupDomainCmd.Flags().String("domain", "", "Domain name to back up") + backupDomainCmd.Flags().String("type", "tar.gz", "Backup type: tar.gz, zip, tar") + backupDomainCmd.MarkFlagRequired("domain") +} diff --git a/cmd/restore_domain.go b/cmd/restore_domain.go new file mode 100644 index 0000000..3d31353 --- /dev/null +++ b/cmd/restore_domain.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "stackroost/internal" + "stackroost/internal/logger" +) + +var restoreDomainCmd = &cobra.Command{ + Use: "restore-domain", + Short: "Restore domain files and MySQL DB from backup archive", + Run: func(cmd *cobra.Command, args []string) { + domain, _ := cmd.Flags().GetString("domain") + backupFile, _ := cmd.Flags().GetString("file") + + if internal.IsNilOrEmpty(domain) || internal.IsNilOrEmpty(backupFile) { + logger.Error("Please provide both --domain and --file") + os.Exit(1) + } + + username := strings.Split(domain, ".")[0] + restoreDir := fmt.Sprintf("/tmp/restore-%s", username) + os.MkdirAll(restoreDir, 0755) + + ext := filepath.Ext(backupFile) + if ext == ".gz" || strings.HasSuffix(backupFile, ".tar.gz") { + logger.Info("Extracting tar.gz archive") + internal.RunCommand("sudo", "tar", "-xzf", backupFile, "-C", restoreDir) + } else if ext == ".tar" { + logger.Info("Extracting tar archive") + internal.RunCommand("sudo", "tar", "-xf", backupFile, "-C", restoreDir) + } else if ext == ".zip" { + logger.Info("Extracting zip archive") + internal.RunCommand("sudo", "unzip", "-o", backupFile, "-d", restoreDir) + } else { + logger.Error("Unsupported file type. Use: tar.gz, tar, or zip") + os.Exit(1) + } + + // Restore public_html + publicPath := fmt.Sprintf("/home/%s/public_html", username) + logger.Info(fmt.Sprintf("Restoring files to %s", publicPath)) + internal.RunCommand("sudo", "cp", "-r", filepath.Join(restoreDir, "home", username, "public_html"), filepath.Join("/home", username)) + internal.RunCommand("sudo", "chown", "-R", fmt.Sprintf("%s:%s", username, username), publicPath) + + // Restore MySQL if .sql found + sqlPath := "" + filepath.Walk(restoreDir, func(path string, info os.FileInfo, err error) error { + if strings.HasSuffix(path, ".sql") { + sqlPath = path + } + return nil + }) + + if sqlPath != "" { + logger.Info(fmt.Sprintf("Restoring MySQL DB from %s", sqlPath)) + restoreCmd := exec.Command("sudo", "mysql", "-u", username, fmt.Sprintf("-p%s", username), username) + sqlFile, _ := os.Open(sqlPath) + defer sqlFile.Close() + restoreCmd.Stdin = sqlFile + restoreCmd.Stdout = os.Stdout + restoreCmd.Stderr = os.Stderr + if err := restoreCmd.Run(); err != nil { + logger.Warn(fmt.Sprintf("MySQL restore failed: %v", err)) + } else { + logger.Success("MySQL database restored") + } + } else { + logger.Warn("No SQL file found in backup. Skipping DB restore.") + } + + logger.Success(fmt.Sprintf("Domain '%s' restored successfully", domain)) + }, +} + +func init() { + rootCmd.AddCommand(restoreDomainCmd) + restoreDomainCmd.Flags().String("domain", "", "Domain name to restore") + restoreDomainCmd.Flags().String("file", "", "Path to backup archive file (.tar.gz, .zip, .tar)") + restoreDomainCmd.MarkFlagRequired("domain") + restoreDomainCmd.MarkFlagRequired("file") +}