Skip to content

Commit 06b178f

Browse files
committed
feat(backend): enhance database import commands and WordPress plugin version
- Update PostgreSQL import logic to drop and recreate the database before import for better data integrity. - Improve MySQL/MariaDB import commands to connect without specifying the database name, allowing for easier management. - Introduce a new SQL escape function in the WordPress plugin to handle special characters more effectively during data operations. - Bump WordPress plugin version to 1.6, reflecting the latest enhancements and fixes. - Refactor import progress handling in the UI to provide clearer feedback during the upload and processing phases.
1 parent 95a68d3 commit 06b178f

3 files changed

Lines changed: 134 additions & 41 deletions

File tree

backend/db/commands.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,46 +108,51 @@ func BuildImportCommand(p models.Profile) string {
108108
cmd = `sh -c 'sudo systemctl stop couchdb >&2; tar xf - -C /; sudo systemctl start couchdb >&2'`
109109
}
110110
} else if p.DBType == models.DBTypePostgreSQL {
111-
// PostgreSQL Logic
111+
// PostgreSQL Logic - drop and recreate database before import
112112
authEnv := fmt.Sprintf("PGPASSWORD='%s'", p.DBPassword)
113-
args := fmt.Sprintf("-U %s %s", p.DBUser, p.TargetDBName)
114-
113+
115114
if p.IsDocker {
116-
cmd = fmt.Sprintf("docker exec -i -e %s %s psql %s",
117-
authEnv, p.ContainerID, args)
115+
// First drop/create, then pipe stdin to psql for import
116+
cmd = fmt.Sprintf("sh -c '%s docker exec %s psql -U %s postgres -c \"DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;\" && docker exec -i -e %s %s psql -U %s %s'",
117+
authEnv, p.ContainerID, p.DBUser, p.TargetDBName, p.TargetDBName,
118+
authEnv, p.ContainerID, p.DBUser, p.TargetDBName)
118119
} else {
119120
hostArgs := fmt.Sprintf("-h %s -p %s", p.DBHost, p.DBPort)
120-
cmd = fmt.Sprintf("%s psql %s %s", authEnv, hostArgs, args)
121+
cmd = fmt.Sprintf("sh -c '%s psql %s -U %s postgres -c \"DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;\" && %s psql %s -U %s %s'",
122+
authEnv, hostArgs, p.DBUser, p.TargetDBName, p.TargetDBName,
123+
authEnv, hostArgs, p.DBUser, p.TargetDBName)
121124
}
122125

123126
} else {
124127
// MySQL/MariaDB Logic - try mariadb first (for MariaDB 10.5+), fallback to mysql
128+
// Connect WITHOUT database name so we can DROP and CREATE it
125129
authArgs := fmt.Sprintf("-u %s -p'%s'", p.DBUser, p.DBPassword)
126130
hostArgs := ""
127131
if !p.IsDocker {
128132
hostArgs = fmt.Sprintf("-h %s -P %s", p.DBHost, p.DBPort)
129133
}
130134

131135
if p.IsDocker {
132-
// Inside container, check which command exists
133-
cmd = fmt.Sprintf("docker exec -i %s sh -c 'if command -v mariadb >/dev/null 2>&1; then mariadb %s %s; else mysql %s %s; fi'",
134-
p.ContainerID, authArgs, p.TargetDBName, authArgs, p.TargetDBName)
136+
// Inside container - connect without database name
137+
cmd = fmt.Sprintf("docker exec -i %s sh -c 'if command -v mariadb >/dev/null 2>&1; then mariadb %s; else mysql %s; fi'",
138+
p.ContainerID, authArgs, authArgs)
135139
} else {
136-
// On host, check which command exists
137-
cmd = fmt.Sprintf("sh -c 'if command -v mariadb >/dev/null 2>&1; then mariadb %s %s %s; else mysql %s %s %s; fi'",
138-
hostArgs, authArgs, p.TargetDBName, hostArgs, authArgs, p.TargetDBName)
140+
// On host - connect without database name
141+
cmd = fmt.Sprintf("sh -c 'if command -v mariadb >/dev/null 2>&1; then mariadb %s %s; else mysql %s %s; fi'",
142+
hostArgs, authArgs, hostArgs, authArgs)
139143
}
140144
}
141145

142146
// Decompression Logic: Detect format and decompress if needed
143-
// Then prepend SET commands to disable strict mode and foreign key checks
144147
decompressCmd := `F=/tmp/dback_import_$$.dat; cat > $F;
145148
if file "$F" 2>/dev/null | grep -q "gzip"; then gunzip -c "$F";
146149
elif file "$F" 2>/dev/null | grep -q "Zstandard"; then zstd -d -c "$F";
147150
else cat "$F"; fi; rm -f "$F"`
148151

149-
// Prepend SQL settings before the actual data
150-
prependSQL := `echo "SET SESSION sql_mode=''; SET FOREIGN_KEY_CHECKS=0;"`
152+
// Prepend SQL: Drop and recreate database, then set session options
153+
// Using escaped quotes to avoid bash interpreting backticks
154+
dropAndCreate := fmt.Sprintf(`printf 'DROP DATABASE IF EXISTS %s; CREATE DATABASE %s CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE %s; SET SESSION sql_mode='"'"''"'"'; SET FOREIGN_KEY_CHECKS=0;\n'`,
155+
p.TargetDBName, p.TargetDBName, p.TargetDBName)
151156

152-
return fmt.Sprintf("{ %s; { %s; }; } | %s", prependSQL, decompressCmd, cmd)
157+
return fmt.Sprintf("{ %s; { %s; }; } | %s", dropAndCreate, decompressCmd, cmd)
153158
}

backend/wordpress/generator.go

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const pluginTemplate = `<?php
1414
/**
1515
* Plugin Name: DB Sync Connector
1616
* Description: generated by DB Sync Manager App. Handles database export/import securely.
17-
* Version: 1.5
17+
* Version: 1.6
1818
* Author: DB Sync Manager
1919
*/
2020
@@ -68,7 +68,7 @@ class DBack_Sync_API {
6868
$info = array(
6969
'success' => true,
7070
'message' => 'DBack Sync plugin is active and ready',
71-
'version' => '1.5',
71+
'version' => '1.6',
7272
'php_version' => PHP_VERSION,
7373
'wp_version' => get_bloginfo('version'),
7474
'can_use_shell' => $this->can_use_shell(),
@@ -199,6 +199,28 @@ class DBack_Sync_API {
199199
exit;
200200
}
201201
202+
/**
203+
* Escape a value for SQL - uses mysqli_real_escape_string directly
204+
* to avoid WordPress esc_sql() issues with special characters like %
205+
*/
206+
private function sql_escape($val) {
207+
global $wpdb;
208+
if ($val === null) {
209+
return 'NULL';
210+
}
211+
// Use mysqli_real_escape_string directly to avoid esc_sql() issues with %
212+
if (isset($wpdb->dbh) && ($wpdb->dbh instanceof mysqli)) {
213+
return "'" . mysqli_real_escape_string($wpdb->dbh, $val) . "'";
214+
}
215+
// Fallback: manual escaping
216+
$escaped = str_replace(
217+
array("\\", "\x00", "\n", "\r", "'", '"', "\x1a"),
218+
array("\\\\", "\\0", "\\n", "\\r", "\\'", '\\"', "\\Z"),
219+
$val
220+
);
221+
return "'" . $escaped . "'";
222+
}
223+
202224
/**
203225
* PHP-based fallback for generating SQL dump
204226
*/
@@ -214,33 +236,40 @@ class DBack_Sync_API {
214236
215237
foreach ($tables as $table) {
216238
// Get Create Table
217-
$create_table = $wpdb->get_row("SHOW CREATE TABLE $table", ARRAY_N);
218-
gzwrite($fp, "DROP TABLE IF EXISTS $table;\n");
239+
$create_table = $wpdb->get_row("SHOW CREATE TABLE ` + "`" + `$table` + "`" + `", ARRAY_N);
240+
gzwrite($fp, "DROP TABLE IF EXISTS ` + "`" + `$table` + "`" + `;\n");
219241
gzwrite($fp, $create_table[1] . ";\n\n");
220242
221-
// Get Data
222-
// Using unbuffered query via mysqli to save memory if available
243+
// Get column names for proper INSERT syntax
244+
$columns = $wpdb->get_results("SHOW COLUMNS FROM ` + "`" + `$table` + "`" + `", ARRAY_N);
245+
$column_names = array();
246+
foreach ($columns as $col) {
247+
$column_names[] = "` + "`" + `" . $col[0] . "` + "`" + `";
248+
}
249+
$columns_str = implode(', ', $column_names);
250+
251+
// Get Data using unbuffered query via mysqli
223252
if (isset($wpdb->dbh) && ($wpdb->dbh instanceof mysqli)) {
224-
$result = mysqli_query($wpdb->dbh, "SELECT * FROM $table", MYSQLI_USE_RESULT);
253+
$result = mysqli_query($wpdb->dbh, "SELECT * FROM ` + "`" + `$table` + "`" + `", MYSQLI_USE_RESULT);
225254
if ($result) {
226255
while ($row = mysqli_fetch_row($result)) {
227-
$values = array_map(function($val) {
228-
if ($val === null) return 'NULL';
229-
return "'" . esc_sql($val) . "'";
230-
}, $row);
231-
gzwrite($fp, "INSERT INTO $table VALUES (" . implode(',', $values) . ");\n");
256+
$values = array();
257+
foreach ($row as $val) {
258+
$values[] = $this->sql_escape($val);
259+
}
260+
gzwrite($fp, "INSERT INTO ` + "`" + `$table` + "`" + ` ($columns_str) VALUES (" . implode(',', $values) . ");\n");
232261
}
233262
mysqli_free_result($result);
234263
}
235264
} else {
236265
// Fallback for older systems (high memory usage)
237-
$rows = $wpdb->get_results("SELECT * FROM $table", ARRAY_N);
266+
$rows = $wpdb->get_results("SELECT * FROM ` + "`" + `$table` + "`" + `", ARRAY_N);
238267
foreach ($rows as $row) {
239-
$values = array_map(function($val) {
240-
if ($val === null) return 'NULL';
241-
return "'" . esc_sql($val) . "'";
242-
}, $row);
243-
gzwrite($fp, "INSERT INTO $table VALUES (" . implode(',', $values) . ");\n");
268+
$values = array();
269+
foreach ($row as $val) {
270+
$values[] = $this->sql_escape($val);
271+
}
272+
gzwrite($fp, "INSERT INTO ` + "`" + `$table` + "`" + ` ($columns_str) VALUES (" . implode(',', $values) . ");\n");
244273
}
245274
}
246275
gzwrite($fp, "\n");

ui/import.go

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"io"
1010
"os"
1111
"os/exec"
12+
"path/filepath"
1213
"strings"
14+
"time"
1315

1416
"fyne.io/fyne/v2"
1517
"fyne.io/fyne/v2/container"
@@ -516,21 +518,47 @@ func (u *UI) createImportTab(w fyne.Window) fyne.CanvasObject {
516518
return
517519
}
518520

519-
// Copy with progress
521+
// Copy with progress (upload phase: 0-50%)
520522
progressR := &ssh.ProgressReader{
521523
Reader: inFile,
522524
Total: totalSize,
523525
Callback: func(current int64, total int64) {
524-
pct := float64(current) / float64(total)
526+
pct := float64(current) / float64(total) * 0.5 // 0-50%
525527
progressBar.SetValue(pct)
526-
statusLabel.SetText(fmt.Sprintf("Restoring: %.1f%%", pct*100))
528+
statusLabel.SetText(fmt.Sprintf("Uploading: %.1f%%", pct*100*2))
527529
},
528530
}
529531

530532
io.Copy(stdin, progressR)
531533
stdin.Close()
532534

533-
if err := cmd.Wait(); err != nil {
535+
// Processing phase: show indeterminate progress
536+
statusLabel.SetText("⏳ Processing... Please wait (DROP + CREATE + IMPORT)")
537+
progressBar.SetValue(0.5)
538+
539+
// Animate progress bar while waiting
540+
done := make(chan bool)
541+
go func() {
542+
val := 0.5
543+
for {
544+
select {
545+
case <-done:
546+
return
547+
default:
548+
val += 0.05
549+
if val > 0.95 {
550+
val = 0.5
551+
}
552+
progressBar.SetValue(val)
553+
time.Sleep(200 * time.Millisecond)
554+
}
555+
}
556+
}()
557+
558+
err = cmd.Wait()
559+
done <- true
560+
561+
if err != nil {
534562
stderrStr := stderrBuf.String()
535563
errMsg := fmt.Sprintf("Error: %v", err)
536564
if stderrStr != "" {
@@ -570,13 +598,14 @@ func (u *UI) createImportTab(w fyne.Window) fyne.CanvasObject {
570598
io.Copy(&stderrBuf, stderr)
571599
}()
572600

601+
// Upload phase: 0-50%
573602
progressR := &ssh.ProgressReader{
574603
Reader: inFile,
575604
Total: totalSize,
576605
Callback: func(current int64, total int64) {
577-
pct := float64(current) / float64(total)
606+
pct := float64(current) / float64(total) * 0.5
578607
progressBar.SetValue(pct)
579-
statusLabel.SetText(fmt.Sprintf("Uploading: %.1f%%", pct*100))
608+
statusLabel.SetText(fmt.Sprintf("Uploading: %.1f%%", pct*100*2))
580609
},
581610
}
582611

@@ -590,8 +619,33 @@ func (u *UI) createImportTab(w fyne.Window) fyne.CanvasObject {
590619
// Close stdin to signal EOF to remote process
591620
stdin.Close()
592621

622+
// Processing phase: animate progress bar
623+
statusLabel.SetText("⏳ Processing... Please wait (DROP + CREATE + IMPORT)")
624+
progressBar.SetValue(0.5)
625+
626+
done := make(chan bool)
627+
go func() {
628+
val := 0.5
629+
for {
630+
select {
631+
case <-done:
632+
return
633+
default:
634+
val += 0.05
635+
if val > 0.95 {
636+
val = 0.5
637+
}
638+
progressBar.SetValue(val)
639+
time.Sleep(200 * time.Millisecond)
640+
}
641+
}
642+
}()
643+
593644
// Wait for remote command to finish
594-
if err := session.Wait(); err != nil {
645+
err = session.Wait()
646+
done <- true
647+
648+
if err != nil {
595649
errMsg := fmt.Sprintf("Process exited with error: %v. Stderr: %s", err, stderrBuf.String())
596650
statusLabel.SetText("Restore Process Failed")
597651
u.log(&p, "Import", "Remote restore process failed", "", "", "Failed", errMsg)
@@ -604,6 +658,11 @@ func (u *UI) createImportTab(w fyne.Window) fyne.CanvasObject {
604658
progressBar.SetValue(1.0)
605659
sizeStr := fmt.Sprintf("%.2f MB", float64(totalSize)/1024/1024)
606660
u.log(&p, "Import", "Import completed successfully", sourcePath, sizeStr, "Success", "")
661+
662+
// Show success dialog
663+
dialog.ShowInformation("Import Complete",
664+
fmt.Sprintf("Database restored successfully!\n\nDatabase: %s\nFile: %s\nSize: %s",
665+
p.TargetDBName, filepath.Base(sourcePath), sizeStr), w)
607666
}()
608667
})
609668
startBtn.Importance = widget.HighImportance

0 commit comments

Comments
 (0)