Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions cmd/backup_domain.go
Original file line number Diff line number Diff line change
@@ -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")
}
88 changes: 88 additions & 0 deletions cmd/restore_domain.go
Original file line number Diff line number Diff line change
@@ -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")
}