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") +}