@@ -3,74 +3,120 @@ package db
33import (
44 "dback/models"
55 "fmt"
6+ "strings"
67)
78
9+ func shellEscape (s string ) string {
10+ // Replace ' with '\''
11+ return "'" + strings .ReplaceAll (s , "'" , "'\\ ''" ) + "'"
12+ }
13+
814// BuildExportCommand constructs the shell command to dump the database.
9- // The output of this command will be the gzipped SQL dump.
1015func BuildExportCommand (p models.Profile ) string {
1116 var cmd string
1217
13- if p .DBType == models .DBTypePostgreSQL {
18+ if p .DBType == models .DBTypeCouchDB {
19+ // CouchDB Logic
20+ // Note: CouchDB logic uses complex sh -c string, we assume fixed structure safe.
21+ // But p.ContainerID might need escaping?
22+ // For simplicity, we keep CouchDB logic as is, assuming alphanumeric IDs.
23+ if p .IsDocker {
24+ cmd = fmt .Sprintf (`sh -c 'DATA_DIR=$(docker inspect %s --format "{{ range .Mounts }}{{ if eq .Destination \"/opt/couchdb/data\" }}{{ .Destination }}{{ end }}{{ end }}"); if [ -z "$DATA_DIR" ]; then DATA_DIR="/opt/couchdb/data"; fi; docker exec %s tar cf - $DATA_DIR'` , p .ContainerID , p .ContainerID )
25+ } else {
26+ cmd = `sh -c 'DATA_DIR=$(grep -r "database_dir" /opt/couchdb/etc/local.ini 2>/dev/null | awk "{print $3}"); if [ -z "$DATA_DIR" ]; then DATA_DIR="/var/lib/couchdb"; fi; sudo systemctl stop couchdb >&2; tar cf - $DATA_DIR; sudo systemctl start couchdb >&2'`
27+ }
28+ } else if p .DBType == models .DBTypePostgreSQL {
1429 // PostgreSQL Logic
15- // Format: PGPASSWORD='pass' pg_dump -h host -p port -U user dbname
30+ // Escape inputs
31+ pwd := shellEscape (p .DBPassword )
32+ user := shellEscape (p .DBUser )
33+ dbName := shellEscape (p .TargetDBName )
1634
17- authEnv := fmt .Sprintf ("PGPASSWORD='%s'" , p .DBPassword )
18- args := fmt .Sprintf ("-U %s %s" , p .DBUser , p .TargetDBName )
35+ // authEnv includes PGPASSWORD='...' which is quoted by shellEscape
36+ // Wait, shellEscape returns '...'.
37+ // So PGPASSWORD='...' is PGPASSWORD='pass'.
38+ // If pass is pass'word, it becomes PGPASSWORD='pass'\''word'. Correct.
39+ authEnv := fmt .Sprintf ("PGPASSWORD=%s" , pwd )
40+ args := fmt .Sprintf ("-U %s %s" , user , dbName )
1941
2042 if p .IsDocker {
21- // Docker: docker exec -e PGPASSWORD=... container pg_dump -U user dbname
22- // Note: pg_dump connects to localhost inside container by default if -h not specified,
23- // or socket. Usually safe to omit -h inside container or use localhost.
2443 cmd = fmt .Sprintf ("docker exec -e %s %s pg_dump %s" ,
2544 authEnv , p .ContainerID , args )
2645 } else {
27- // Native
2846 hostArgs := fmt .Sprintf ("-h %s -p %s" , p .DBHost , p .DBPort )
2947 cmd = fmt .Sprintf ("%s pg_dump %s %s" , authEnv , hostArgs , args )
3048 }
3149
3250 } else {
3351 // MySQL/MariaDB Logic
34- authArgs := fmt .Sprintf ("-u %s -p'%s'" , p .DBUser , p .DBPassword )
52+ // mysql -p'pass'
53+ // If pass is 'pass', -p'pass'.
54+ // If pass is pass'word, -p'pass'\''word'.
55+ // But we need to be careful about -pFLAG format.
56+ // -p%s.
57+ pwd := shellEscape (p .DBPassword )
58+ // shellEscape adds outer quotes.
59+ // So -p'pass' becomes -p'pass'.
60+ // Wait, shellEscape returns 'pass'.
61+ // fmt.Sprintf("-p%s", pwd) -> -p'pass'. Correct.
62+
63+ authArgs := fmt .Sprintf ("-u %s -p%s" , p .DBUser , pwd )
3564 hostArgs := ""
3665 if ! p .IsDocker {
3766 hostArgs = fmt .Sprintf ("-h %s -P %s" , p .DBHost , p .DBPort )
3867 }
3968
4069 if p .IsDocker {
41- // Docker: docker exec -i container mysqldump ...
4270 cmd = fmt .Sprintf ("docker exec -i %s mysqldump %s %s" ,
4371 p .ContainerID , authArgs , p .TargetDBName )
4472 } else {
45- // Native: mysqldump -h ... ...
4673 cmd = fmt .Sprintf ("mysqldump %s %s %s" ,
4774 hostArgs , authArgs , p .TargetDBName )
4875 }
4976 }
5077
51- // Pipe through gzip
52- return fmt .Sprintf ("%s | gzip" , cmd )
78+ // Compression Logic
79+ compressCmd := "if command -v zstd >/dev/null 2>&1; then zstd; else gzip; fi"
80+
81+ // Use set -o pipefail to catch errors.
82+ // We wrap in bash to ensure pipefail support, but NOT using -c '...' because of quoting hell.
83+ // We try to run: bash -c "set -o pipefail; CMD | COMPRESS"
84+ // But CMD has single quotes.
85+ // Double quotes "..." allow $ expansion.
86+ // We must escape $ and " and \ inside CMD.
87+ // This is hard.
88+ // Alternative: Use { set -o pipefail; cmd; } | compress?
89+ // No, pipefail must be set in the shell executing the pipeline.
90+ // If we just send the string `set -o pipefail; cmd | compress` to SSH, it runs in user shell.
91+ // If user shell is bash, it works.
92+ // If user shell is sh, it might fail.
93+ // But wrapping in `bash -c` caused the error.
94+ // So we simply send it raw and hope for bash/zsh.
95+ return fmt .Sprintf ("set -o pipefail; %s | { %s; }" , cmd , compressCmd )
5396}
5497
5598// BuildImportCommand constructs the shell command to restore the database.
56- // It expects the input (stdin) to be a gzipped SQL stream.
5799func BuildImportCommand (p models.Profile ) string {
58100 var cmd string
59101
60- if p .DBType == models .DBTypePostgreSQL {
102+ if p .DBType == models .DBTypeCouchDB {
103+ // CouchDB Logic
104+ if p .IsDocker {
105+ // Docker: Untar then restart
106+ cmd = fmt .Sprintf (`sh -c 'docker exec -i %s tar xf - -C /; docker restart %s >&2'` , p .ContainerID , p .ContainerID )
107+ } else {
108+ // Native: Stop, Untar, Start
109+ cmd = `sh -c 'sudo systemctl stop couchdb >&2; tar xf - -C /; sudo systemctl start couchdb >&2'`
110+ }
111+ } else if p .DBType == models .DBTypePostgreSQL {
61112 // PostgreSQL Logic
62- // psql usually takes SQL on stdin.
63- // Format: PGPASSWORD='pass' psql -h host -p port -U user dbname
64-
65113 authEnv := fmt .Sprintf ("PGPASSWORD='%s'" , p .DBPassword )
66114 args := fmt .Sprintf ("-U %s %s" , p .DBUser , p .TargetDBName )
67115
68116 if p .IsDocker {
69- // Docker: docker exec -i -e PGPASSWORD=... container psql ...
70117 cmd = fmt .Sprintf ("docker exec -i -e %s %s psql %s" ,
71118 authEnv , p .ContainerID , args )
72119 } else {
73- // Native
74120 hostArgs := fmt .Sprintf ("-h %s -p %s" , p .DBHost , p .DBPort )
75121 cmd = fmt .Sprintf ("%s psql %s %s" , authEnv , hostArgs , args )
76122 }
@@ -84,16 +130,15 @@ func BuildImportCommand(p models.Profile) string {
84130 }
85131
86132 if p .IsDocker {
87- // Docker: docker exec -i container mysql ...
88133 cmd = fmt .Sprintf ("docker exec -i %s mysql %s %s" ,
89134 p .ContainerID , authArgs , p .TargetDBName )
90135 } else {
91- // Native: mysql -h ... ...
92136 cmd = fmt .Sprintf ("mysql %s %s %s" ,
93137 hostArgs , authArgs , p .TargetDBName )
94138 }
95139 }
96140
97- // We expect compressed input, so we unzip before piping to db command
98- return fmt .Sprintf ("gunzip -c | %s" , cmd )
141+ // Decompression Logic: Try zstd, fallback to gzip
142+ decompressCmd := "if command -v zstd >/dev/null 2>&1; then zstd -d 2>/dev/null || gunzip -c; else gunzip -c; fi"
143+ return fmt .Sprintf ("{ %s; } | %s" , decompressCmd , cmd )
99144}
0 commit comments