Skip to content

Commit 2d235c1

Browse files
committed
Implement cloud storage upload functionality for database backups: Added support for S3, GCS, and Azure in backup commands, enhanced command-line flags for cloud configuration, and integrated cloud upload handling in scheduled backups. Updated version to v0.1.0.
1 parent 875adb7 commit 2d235c1

File tree

14 files changed

+473
-21
lines changed

14 files changed

+473
-21
lines changed

cmd/backup.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package cmd
22

33
import (
4+
"dbx/internal/cloud"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
410
"github.com/spf13/cobra"
511
)
612

@@ -9,6 +15,12 @@ var (
915
dbType, host, user, password, database, out, port, uri string
1016
backupType, sqlitePath string
1117
outDir string // Alias for out, used in some commands
18+
// Cloud upload flags
19+
uploadCloud bool
20+
cloudProvider string // s3, gcs, azure
21+
s3Bucket, s3Prefix string
22+
gcsBucket, gcsPrefix string
23+
azureAccount, azureContainer, azureBlob string
1224
)
1325

1426
var backupCmd = &cobra.Command{
@@ -21,3 +33,47 @@ func init() {
2133
rootCmd.AddCommand(backupCmd)
2234
// Subcommands are added in their respective files
2335
}
36+
37+
// handleCloudUpload handles cloud upload for backup files
38+
func handleCloudUpload(dbName, outDir, dbType string) error {
39+
// Find the most recent backup file
40+
pattern := filepath.Join(outDir, dbName+"*")
41+
matches, _ := filepath.Glob(pattern)
42+
if len(matches) == 0 {
43+
return fmt.Errorf("no backup file found in %s", outDir)
44+
}
45+
46+
// Use the most recent file
47+
backupFile := matches[len(matches)-1]
48+
49+
switch strings.ToLower(cloudProvider) {
50+
case "s3":
51+
bucket := s3Bucket
52+
if bucket == "" {
53+
bucket = os.Getenv("DBX_S3_BUCKET")
54+
}
55+
if bucket == "" {
56+
return fmt.Errorf("S3 bucket name required (use --s3-bucket or set DBX_S3_BUCKET env var)")
57+
}
58+
prefix := s3Prefix
59+
if prefix == "" {
60+
prefix = os.Getenv("DBX_S3_PREFIX")
61+
if prefix == "" {
62+
prefix = "dbx/"
63+
}
64+
}
65+
return cloud.UploadToS3(backupFile, bucket, prefix)
66+
case "gcs":
67+
if gcsBucket == "" {
68+
return fmt.Errorf("GCS bucket name required (use --gcs-bucket)")
69+
}
70+
return cloud.UploadToGCS(backupFile, gcsBucket, gcsPrefix)
71+
case "azure":
72+
if azureAccount == "" || azureContainer == "" {
73+
return fmt.Errorf("Azure account and container required (use --azure-account and --azure-container)")
74+
}
75+
return cloud.UploadToAzure(backupFile, azureAccount, azureContainer, azureBlob)
76+
default:
77+
return fmt.Errorf("unsupported cloud provider: %s (use s3, gcs, or azure)", cloudProvider)
78+
}
79+
}

cmd/mongodb.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package cmd
22

33
import (
4+
"dbx/internal/cloud"
45
"dbx/internal/db"
56
"fmt"
67
"os"
8+
"path/filepath"
9+
"strings"
710

811
"github.com/spf13/cobra"
912
)
@@ -18,6 +21,14 @@ var mongodbCmd = &cobra.Command{
1821
os.Exit(1)
1922
}
2023
fmt.Println("✅ Backup complete")
24+
25+
// Handle cloud upload if requested
26+
if uploadCloud {
27+
if err := handleCloudUpload(database, out, "mongodb"); err != nil {
28+
fmt.Printf("⚠️ Cloud upload failed: %v\n", err)
29+
}
30+
}
31+
2132
return nil
2233
},
2334
}
@@ -28,6 +39,17 @@ func init() {
2839
mongodbCmd.Flags().StringVar(&uri, "uri", "mongodb://localhost:27017", "MongoDB URI")
2940
mongodbCmd.Flags().StringVar(&database, "database", "", "Database name")
3041
mongodbCmd.Flags().StringVar(&out, "out", "./backups", "Output directory")
42+
43+
// Cloud upload flags
44+
mongodbCmd.Flags().BoolVar(&uploadCloud, "upload", false, "Upload backup to cloud storage")
45+
mongodbCmd.Flags().StringVar(&cloudProvider, "cloud", "s3", "Cloud provider: s3, gcs, or azure")
46+
mongodbCmd.Flags().StringVar(&s3Bucket, "s3-bucket", "", "S3 bucket name (or set DBX_S3_BUCKET env var)")
47+
mongodbCmd.Flags().StringVar(&s3Prefix, "s3-prefix", "dbx/", "S3 prefix/folder path")
48+
mongodbCmd.Flags().StringVar(&gcsBucket, "gcs-bucket", "", "GCS bucket name")
49+
mongodbCmd.Flags().StringVar(&gcsPrefix, "gcs-prefix", "dbx/", "GCS prefix/folder path")
50+
mongodbCmd.Flags().StringVar(&azureAccount, "azure-account", "", "Azure storage account name")
51+
mongodbCmd.Flags().StringVar(&azureContainer, "azure-container", "", "Azure container name")
52+
mongodbCmd.Flags().StringVar(&azureBlob, "azure-blob", "", "Azure blob name (optional)")
3153

3254
mongodbCmd.MarkFlagRequired("database")
3355
}

cmd/mysql.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package cmd
22

33
import (
4+
"dbx/internal/cloud"
45
"dbx/internal/db"
56
"fmt"
67
"os"
8+
"path/filepath"
9+
"strings"
710

811
"github.com/spf13/cobra"
912
)
@@ -27,6 +30,15 @@ var mysqlCmd = &cobra.Command{
2730
os.Exit(1)
2831
}
2932
fmt.Println("✅ Backup complete")
33+
34+
// Handle cloud upload if requested
35+
if uploadCloud {
36+
if err := handleCloudUpload(database, out, "mysql"); err != nil {
37+
fmt.Printf("⚠️ Cloud upload failed: %v\n", err)
38+
// Don't fail the backup if upload fails
39+
}
40+
}
41+
3042
return nil
3143
},
3244
}
@@ -40,6 +52,17 @@ func init() {
4052
mysqlCmd.Flags().StringVar(&database, "database", "", "MySQL database name")
4153
mysqlCmd.Flags().StringVar(&out, "out", "./backups", "Output directory")
4254
mysqlCmd.Flags().StringVar(&backupType, "type", "full", "Backup type: full, incremental, or differential")
55+
56+
// Cloud upload flags
57+
mysqlCmd.Flags().BoolVar(&uploadCloud, "upload", false, "Upload backup to cloud storage")
58+
mysqlCmd.Flags().StringVar(&cloudProvider, "cloud", "s3", "Cloud provider: s3, gcs, or azure")
59+
mysqlCmd.Flags().StringVar(&s3Bucket, "s3-bucket", "", "S3 bucket name (or set DBX_S3_BUCKET env var)")
60+
mysqlCmd.Flags().StringVar(&s3Prefix, "s3-prefix", "dbx/", "S3 prefix/folder path")
61+
mysqlCmd.Flags().StringVar(&gcsBucket, "gcs-bucket", "", "GCS bucket name")
62+
mysqlCmd.Flags().StringVar(&gcsPrefix, "gcs-prefix", "dbx/", "GCS prefix/folder path")
63+
mysqlCmd.Flags().StringVar(&azureAccount, "azure-account", "", "Azure storage account name")
64+
mysqlCmd.Flags().StringVar(&azureContainer, "azure-container", "", "Azure container name")
65+
mysqlCmd.Flags().StringVar(&azureBlob, "azure-blob", "", "Azure blob name (optional)")
4366

4467
mysqlCmd.MarkFlagRequired("database")
4568
}

cmd/postgres.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package cmd
22

33
import (
4+
"dbx/internal/cloud"
45
"dbx/internal/db"
56
"fmt"
67
"os"
8+
"path/filepath"
9+
"strings"
710

811
"github.com/spf13/cobra"
912
)
@@ -25,6 +28,14 @@ var postgresCmd = &cobra.Command{
2528
os.Exit(1)
2629
}
2730
fmt.Println("✅ Backup complete")
31+
32+
// Handle cloud upload if requested
33+
if uploadCloud {
34+
if err := handleCloudUpload(database, out, "postgres"); err != nil {
35+
fmt.Printf("⚠️ Cloud upload failed: %v\n", err)
36+
}
37+
}
38+
2839
return nil
2940
},
3041
}
@@ -39,6 +50,17 @@ func init() {
3950
postgresCmd.Flags().StringVar(&database, "database", "", "Database name")
4051
postgresCmd.Flags().StringVar(&out, "out", "./backups", "Output directory")
4152
postgresCmd.Flags().StringVar(&backupType, "type", "full", "Backup type: full, incremental, or differential")
53+
54+
// Cloud upload flags
55+
postgresCmd.Flags().BoolVar(&uploadCloud, "upload", false, "Upload backup to cloud storage")
56+
postgresCmd.Flags().StringVar(&cloudProvider, "cloud", "s3", "Cloud provider: s3, gcs, or azure")
57+
postgresCmd.Flags().StringVar(&s3Bucket, "s3-bucket", "", "S3 bucket name (or set DBX_S3_BUCKET env var)")
58+
postgresCmd.Flags().StringVar(&s3Prefix, "s3-prefix", "dbx/", "S3 prefix/folder path")
59+
postgresCmd.Flags().StringVar(&gcsBucket, "gcs-bucket", "", "GCS bucket name")
60+
postgresCmd.Flags().StringVar(&gcsPrefix, "gcs-prefix", "dbx/", "GCS prefix/folder path")
61+
postgresCmd.Flags().StringVar(&azureAccount, "azure-account", "", "Azure storage account name")
62+
postgresCmd.Flags().StringVar(&azureContainer, "azure-container", "", "Azure container name")
63+
postgresCmd.Flags().StringVar(&azureBlob, "azure-blob", "", "Azure blob name (optional)")
4264

4365
postgresCmd.MarkFlagRequired("database")
4466
}

cmd/schedule.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,41 @@ var scheduleAddCmd = &cobra.Command{
4949
return fmt.Errorf("unsupported database type: %s", dbType)
5050
}
5151

52+
// Add cloud upload parameters if requested
53+
if uploadCloud {
54+
params["upload_cloud"] = "true"
55+
params["cloud_provider"] = cloudProvider
56+
if s3Bucket != "" {
57+
params["s3_bucket"] = s3Bucket
58+
}
59+
if s3Prefix != "" {
60+
params["s3_prefix"] = s3Prefix
61+
}
62+
if gcsBucket != "" {
63+
params["gcs_bucket"] = gcsBucket
64+
}
65+
if gcsPrefix != "" {
66+
params["gcs_prefix"] = gcsPrefix
67+
}
68+
if azureAccount != "" {
69+
params["azure_account"] = azureAccount
70+
}
71+
if azureContainer != "" {
72+
params["azure_container"] = azureContainer
73+
}
74+
if azureBlob != "" {
75+
params["azure_blob"] = azureBlob
76+
}
77+
}
78+
5279
if err := scheduler.AddJob(dbType, scheduleCron, params); err != nil {
5380
fmt.Println("Failed to schedule backup:", err)
5481
os.Exit(1)
5582
}
5683
fmt.Println("✅ Backup scheduled successfully")
84+
if uploadCloud {
85+
fmt.Println("☁️ Cloud upload enabled for this schedule")
86+
}
5787
return nil
5888
},
5989
}
@@ -97,8 +127,18 @@ func init() {
97127
scheduleAddCmd.Flags().StringVar(&sqlitePath, "path", "", "SQLite database path")
98128
scheduleAddCmd.Flags().StringVar(&out, "out", "./backups", "Output directory")
99129
scheduleAddCmd.Flags().StringVar(&scheduleCron, "cron", "", "Cron schedule (e.g., '0 2 * * *' for daily at 2 AM)")
130+
131+
// Cloud upload flags for scheduled backups
132+
scheduleAddCmd.Flags().BoolVar(&uploadCloud, "upload", false, "Upload backups to cloud storage automatically")
133+
scheduleAddCmd.Flags().StringVar(&cloudProvider, "cloud", "s3", "Cloud provider: s3, gcs, or azure")
134+
scheduleAddCmd.Flags().StringVar(&s3Bucket, "s3-bucket", "", "S3 bucket name (or set DBX_S3_BUCKET env var)")
135+
scheduleAddCmd.Flags().StringVar(&s3Prefix, "s3-prefix", "dbx/", "S3 prefix/folder path")
136+
scheduleAddCmd.Flags().StringVar(&gcsBucket, "gcs-bucket", "", "GCS bucket name")
137+
scheduleAddCmd.Flags().StringVar(&gcsPrefix, "gcs-prefix", "dbx/", "GCS prefix/folder path")
138+
scheduleAddCmd.Flags().StringVar(&azureAccount, "azure-account", "", "Azure storage account name")
139+
scheduleAddCmd.Flags().StringVar(&azureContainer, "azure-container", "", "Azure container name")
140+
scheduleAddCmd.Flags().StringVar(&azureBlob, "azure-blob", "", "Azure blob name (optional)")
100141

101142
scheduleAddCmd.MarkFlagRequired("db")
102143
scheduleAddCmd.MarkFlagRequired("cron")
103-
}
104144

cmd/sqlite.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package cmd
22

33
import (
4+
"dbx/internal/cloud"
45
"dbx/internal/db"
56
"fmt"
67
"os"
8+
"path/filepath"
9+
"strings"
710

811
"github.com/spf13/cobra"
912
)
@@ -20,6 +23,15 @@ var sqliteCmd = &cobra.Command{
2023
os.Exit(1)
2124
}
2225
fmt.Println("✅ Backup complete")
26+
27+
// Handle cloud upload if requested
28+
if uploadCloud {
29+
dbName := filepath.Base(sqlitePath)
30+
if err := handleCloudUpload(dbName, out, "sqlite"); err != nil {
31+
fmt.Printf("⚠️ Cloud upload failed: %v\n", err)
32+
}
33+
}
34+
2335
return nil
2436
},
2537
}
@@ -29,6 +41,17 @@ func init() {
2941

3042
sqliteCmd.Flags().StringVar(&sqlitePath, "path", "", "Path to SQLite database file")
3143
sqliteCmd.Flags().StringVar(&out, "out", "./backups", "Output directory")
44+
45+
// Cloud upload flags
46+
sqliteCmd.Flags().BoolVar(&uploadCloud, "upload", false, "Upload backup to cloud storage")
47+
sqliteCmd.Flags().StringVar(&cloudProvider, "cloud", "s3", "Cloud provider: s3, gcs, or azure")
48+
sqliteCmd.Flags().StringVar(&s3Bucket, "s3-bucket", "", "S3 bucket name (or set DBX_S3_BUCKET env var)")
49+
sqliteCmd.Flags().StringVar(&s3Prefix, "s3-prefix", "dbx/", "S3 prefix/folder path")
50+
sqliteCmd.Flags().StringVar(&gcsBucket, "gcs-bucket", "", "GCS bucket name")
51+
sqliteCmd.Flags().StringVar(&gcsPrefix, "gcs-prefix", "dbx/", "GCS prefix/folder path")
52+
sqliteCmd.Flags().StringVar(&azureAccount, "azure-account", "", "Azure storage account name")
53+
sqliteCmd.Flags().StringVar(&azureContainer, "azure-container", "", "Azure container name")
54+
sqliteCmd.Flags().StringVar(&azureBlob, "azure-blob", "", "Azure blob name (optional)")
3255

3356
sqliteCmd.MarkFlagRequired("path")
3457
}

internal/db/mysql.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ func BackupMySQLWithType(host, user, password, database, outDir string, backupTy
3434
}
3535

3636
args := []string{"-h", host, "-u", user}
37+
// Use MYSQL_PWD environment variable for security (password not visible in process list)
3738
if password != "" {
38-
args = append(args, "-p"+password)
39+
// Set environment variable before command execution
40+
// Note: This is set per-command, not globally
3941
}
4042

4143
// For incremental backups, use --master-data and --flush-logs
@@ -49,7 +51,12 @@ func BackupMySQLWithType(host, user, password, database, outDir string, backupTy
4951
args = append(args, database)
5052

5153
cmd := exec.Command("mysqldump", args...)
52-
cmd.Env = os.Environ()
54+
env := os.Environ()
55+
// Set MYSQL_PWD environment variable for secure password passing
56+
if password != "" {
57+
env = append(env, "MYSQL_PWD="+password)
58+
}
59+
cmd.Env = env
5360

5461
stdout, err := cmd.StdoutPipe()
5562
if err != nil {
@@ -71,6 +78,8 @@ func BackupMySQLWithType(host, user, password, database, outDir string, backupTy
7178
return fmt.Errorf("mysqldump failed to start: %v", err)
7279
}
7380

81+
fmt.Println("🔄 Running MySQL backup...")
82+
7483
// Buffer the dump output in memory
7584
var outputBuf bytes.Buffer
7685
if _, err := io.Copy(&outputBuf, stdout); err != nil {
@@ -118,5 +127,15 @@ func BackupMySQLWithType(host, user, password, database, outDir string, backupTy
118127
return err
119128
}
120129

130+
// Verify backup file was created and is not empty
131+
info, statErr := os.Stat(outFile)
132+
if statErr != nil {
133+
return fmt.Errorf("backup file verification failed: %w", statErr)
134+
}
135+
if info.Size() == 0 {
136+
return fmt.Errorf("backup file is empty")
137+
}
138+
139+
fmt.Printf("✅ Backup verified: %s (%.2f MB)\n", outFile, float64(info.Size())/1024/1024)
121140
return nil
122141
}

0 commit comments

Comments
 (0)