Skip to content

Commit a140533

Browse files
committed
feat(database): add PostgreSQL support
Add PostgreSQL database type and implement command building for pg_dump and psql. Refactor export and import commands to handle both MySQL/MariaDB and PostgreSQL databases. Add string trimming to form inputs and enhance profile management with duplicate and rename functionality.
1 parent 5613b39 commit a140533

File tree

5 files changed

+159
-88
lines changed

5 files changed

+159
-88
lines changed

backend/db/commands.go

Lines changed: 73 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,51 @@
11
package db
22

33
import (
4-
"fmt"
54
"dback/models"
5+
"fmt"
66
)
77

88
// BuildExportCommand constructs the shell command to dump the database.
99
// The output of this command will be the gzipped SQL dump.
1010
func BuildExportCommand(p models.Profile) string {
1111
var cmd string
12-
13-
// Basic mysqldump arguments
14-
// Note: passing password directly is insecure in process list, but standard for simple tools.
15-
// Ideally using a config file, but following prompt examples.
16-
authArgs := fmt.Sprintf("-u %s -p'%s'", p.DBUser, p.DBPassword)
17-
hostArgs := ""
18-
if !p.IsDocker {
19-
hostArgs = fmt.Sprintf("-h %s -P %s", p.DBHost, p.DBPort)
20-
}
2112

22-
if p.IsDocker {
23-
// Docker: docker exec -i container mysqldump ...
24-
// We don't typically use -h 127.0.0.1 inside the container unless specified,
25-
// usually it connects to localhost inside container.
26-
cmd = fmt.Sprintf("docker exec -i %s mysqldump %s %s",
27-
p.ContainerID, authArgs, p.TargetDBName)
13+
if p.DBType == models.DBTypePostgreSQL {
14+
// PostgreSQL Logic
15+
// Format: PGPASSWORD='pass' pg_dump -h host -p port -U user dbname
16+
17+
authEnv := fmt.Sprintf("PGPASSWORD='%s'", p.DBPassword)
18+
args := fmt.Sprintf("-U %s %s", p.DBUser, p.TargetDBName)
19+
20+
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.
24+
cmd = fmt.Sprintf("docker exec -e %s %s pg_dump %s",
25+
authEnv, p.ContainerID, args)
26+
} else {
27+
// Native
28+
hostArgs := fmt.Sprintf("-h %s -p %s", p.DBHost, p.DBPort)
29+
cmd = fmt.Sprintf("%s pg_dump %s %s", authEnv, hostArgs, args)
30+
}
31+
2832
} else {
29-
// Native: mysqldump -h ... ...
30-
cmd = fmt.Sprintf("mysqldump %s %s %s",
31-
hostArgs, authArgs, p.TargetDBName)
33+
// MySQL/MariaDB Logic
34+
authArgs := fmt.Sprintf("-u %s -p'%s'", p.DBUser, p.DBPassword)
35+
hostArgs := ""
36+
if !p.IsDocker {
37+
hostArgs = fmt.Sprintf("-h %s -P %s", p.DBHost, p.DBPort)
38+
}
39+
40+
if p.IsDocker {
41+
// Docker: docker exec -i container mysqldump ...
42+
cmd = fmt.Sprintf("docker exec -i %s mysqldump %s %s",
43+
p.ContainerID, authArgs, p.TargetDBName)
44+
} else {
45+
// Native: mysqldump -h ... ...
46+
cmd = fmt.Sprintf("mysqldump %s %s %s",
47+
hostArgs, authArgs, p.TargetDBName)
48+
}
3249
}
3350

3451
// Pipe through gzip
@@ -37,31 +54,46 @@ func BuildExportCommand(p models.Profile) string {
3754

3855
// BuildImportCommand constructs the shell command to restore the database.
3956
// It expects the input (stdin) to be a gzipped SQL stream.
40-
// To handle non-gzipped input, we'd need to know the source format,
41-
// but for this helper we'll assume the transfer sends a gzip stream or we gzip it on the fly.
42-
// Actually, the UI requirement says "Select a local .sql or .sql.gz".
43-
// If we standardize on sending a compressed stream to the server to save bandwidth,
44-
// then the server command should always expect gzip input.
4557
func BuildImportCommand(p models.Profile) string {
46-
// Basic mysql arguments
47-
authArgs := fmt.Sprintf("-u %s -p'%s'", p.DBUser, p.DBPassword)
48-
hostArgs := ""
49-
if !p.IsDocker {
50-
hostArgs = fmt.Sprintf("-h %s -P %s", p.DBHost, p.DBPort)
51-
}
58+
var cmd string
59+
60+
if p.DBType == models.DBTypePostgreSQL {
61+
// PostgreSQL Logic
62+
// psql usually takes SQL on stdin.
63+
// Format: PGPASSWORD='pass' psql -h host -p port -U user dbname
64+
65+
authEnv := fmt.Sprintf("PGPASSWORD='%s'", p.DBPassword)
66+
args := fmt.Sprintf("-U %s %s", p.DBUser, p.TargetDBName)
67+
68+
if p.IsDocker {
69+
// Docker: docker exec -i -e PGPASSWORD=... container psql ...
70+
cmd = fmt.Sprintf("docker exec -i -e %s %s psql %s",
71+
authEnv, p.ContainerID, args)
72+
} else {
73+
// Native
74+
hostArgs := fmt.Sprintf("-h %s -p %s", p.DBHost, p.DBPort)
75+
cmd = fmt.Sprintf("%s psql %s %s", authEnv, hostArgs, args)
76+
}
5277

53-
// The base command that accepts SQL on stdin
54-
var mysqlCmd string
55-
if p.IsDocker {
56-
// Docker: docker exec -i container mysql ...
57-
mysqlCmd = fmt.Sprintf("docker exec -i %s mysql %s %s",
58-
p.ContainerID, authArgs, p.TargetDBName)
5978
} else {
60-
// Native: mysql -h ... ...
61-
mysqlCmd = fmt.Sprintf("mysql %s %s %s",
62-
hostArgs, authArgs, p.TargetDBName)
79+
// MySQL/MariaDB Logic
80+
authArgs := fmt.Sprintf("-u %s -p'%s'", p.DBUser, p.DBPassword)
81+
hostArgs := ""
82+
if !p.IsDocker {
83+
hostArgs = fmt.Sprintf("-h %s -P %s", p.DBHost, p.DBPort)
84+
}
85+
86+
if p.IsDocker {
87+
// Docker: docker exec -i container mysql ...
88+
cmd = fmt.Sprintf("docker exec -i %s mysql %s %s",
89+
p.ContainerID, authArgs, p.TargetDBName)
90+
} else {
91+
// Native: mysql -h ... ...
92+
cmd = fmt.Sprintf("mysql %s %s %s",
93+
hostArgs, authArgs, p.TargetDBName)
94+
}
6395
}
6496

65-
// We expect compressed input, so we unzip before piping to mysql
66-
return fmt.Sprintf("gunzip -c | %s", mysqlCmd)
97+
// We expect compressed input, so we unzip before piping to db command
98+
return fmt.Sprintf("gunzip -c | %s", cmd)
6799
}

models/models.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ const (
2424
type DBType string
2525

2626
const (
27-
DBTypeMySQL DBType = "MySQL"
28-
DBTypeMariaDB DBType = "MariaDB"
27+
DBTypeMySQL DBType = "MySQL"
28+
DBTypeMariaDB DBType = "MariaDB"
29+
DBTypePostgreSQL DBType = "PostgreSQL"
2930
)
3031

3132
// Profile represents a saved connection profile

ui/export.go

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"os"
77
"path/filepath"
8+
"strings"
89
"time"
910

1011
"dback/backend/db"
@@ -177,7 +178,7 @@ func (u *UI) createExportTab(w fyne.Window) fyne.CanvasObject {
177178
}
178179
}
179180

180-
u.expDBTypeSelect = widget.NewSelect([]string{string(models.DBTypeMySQL), string(models.DBTypeMariaDB)}, nil)
181+
u.expDBTypeSelect = widget.NewSelect([]string{string(models.DBTypeMySQL), string(models.DBTypeMariaDB), string(models.DBTypePostgreSQL)}, nil)
181182
u.expDBTypeSelect.SetSelected(string(models.DBTypeMySQL))
182183

183184
u.expDBHostEntry = widget.NewEntry()
@@ -206,6 +207,7 @@ func (u *UI) createExportTab(w fyne.Window) fyne.CanvasObject {
206207
DBPort: u.expDBPortEntry.Text,
207208
DBUser: u.expDBUserEntry.Text,
208209
DBPassword: u.expDBPassEntry.Text,
210+
DBType: models.DBType(u.expDBTypeSelect.Selected),
209211
IsDocker: u.expIsDockerCheck.Checked,
210212
ContainerID: u.expContainerIDEntry.Text,
211213
}
@@ -218,14 +220,29 @@ func (u *UI) createExportTab(w fyne.Window) fyne.CanvasObject {
218220
}
219221
defer client.Close()
220222

221-
// Construct a ping command
222-
authArgs := fmt.Sprintf("-u %s -p'%s'", p.DBUser, p.DBPassword)
223+
// Construct a check command
223224
var cmd string
224-
if p.IsDocker {
225-
cmd = fmt.Sprintf("docker exec -i %s mysqladmin %s ping", p.ContainerID, authArgs)
225+
if p.DBType == models.DBTypePostgreSQL {
226+
// Postgres Check
227+
// Use pg_isready
228+
// PGPASSWORD='pass' pg_isready -h host -p port -U user
229+
authEnv := fmt.Sprintf("PGPASSWORD='%s'", p.DBPassword)
230+
if p.IsDocker {
231+
// docker exec -e PGPASSWORD=... container pg_isready -U user
232+
cmd = fmt.Sprintf("docker exec -e %s %s pg_isready -U %s", authEnv, p.ContainerID, p.DBUser)
233+
} else {
234+
hostArgs := fmt.Sprintf("-h %s -p %s", p.DBHost, p.DBPort)
235+
cmd = fmt.Sprintf("%s pg_isready %s -U %s", authEnv, hostArgs, p.DBUser)
236+
}
226237
} else {
227-
hostArgs := fmt.Sprintf("-h %s -P %s", p.DBHost, p.DBPort)
228-
cmd = fmt.Sprintf("mysqladmin %s %s ping", hostArgs, authArgs)
238+
// MySQL/MariaDB Check
239+
authArgs := fmt.Sprintf("-u %s -p'%s'", p.DBUser, p.DBPassword)
240+
if p.IsDocker {
241+
cmd = fmt.Sprintf("docker exec -i %s mysqladmin %s ping", p.ContainerID, authArgs)
242+
} else {
243+
hostArgs := fmt.Sprintf("-h %s -P %s", p.DBHost, p.DBPort)
244+
cmd = fmt.Sprintf("mysqladmin %s %s ping", hostArgs, authArgs)
245+
}
229246
}
230247

231248
_, session, err := client.RunCommandStream(cmd)
@@ -340,19 +357,20 @@ func (u *UI) createExportTab(w fyne.Window) fyne.CanvasObject {
340357

341358
// SSH Flow
342359
p := models.Profile{
343-
Host: u.expHostEntry.Text,
344-
Port: u.expPortEntry.Text,
345-
SSHUser: u.expSSHUserEntry.Text,
346-
SSHPassword: u.expSSHPassEntry.Text,
360+
Host: strings.TrimSpace(u.expHostEntry.Text),
361+
Port: strings.TrimSpace(u.expPortEntry.Text),
362+
SSHUser: strings.TrimSpace(u.expSSHUserEntry.Text),
363+
SSHPassword: strings.TrimSpace(u.expSSHPassEntry.Text),
347364
AuthType: models.AuthType(u.expAuthTypeSelect.Selected),
348-
AuthKeyPath: u.expKeyPathEntry.Text,
349-
DBHost: u.expDBHostEntry.Text,
350-
DBPort: u.expDBPortEntry.Text,
351-
DBUser: u.expDBUserEntry.Text,
352-
DBPassword: u.expDBPassEntry.Text,
365+
AuthKeyPath: strings.TrimSpace(u.expKeyPathEntry.Text),
366+
DBHost: strings.TrimSpace(u.expDBHostEntry.Text),
367+
DBPort: strings.TrimSpace(u.expDBPortEntry.Text),
368+
DBUser: strings.TrimSpace(u.expDBUserEntry.Text),
369+
DBPassword: strings.TrimSpace(u.expDBPassEntry.Text),
370+
DBType: models.DBType(u.expDBTypeSelect.Selected),
353371
IsDocker: u.expIsDockerCheck.Checked,
354-
ContainerID: u.expContainerIDEntry.Text,
355-
TargetDBName: u.expTargetDBEntry.Text,
372+
ContainerID: strings.TrimSpace(u.expContainerIDEntry.Text),
373+
TargetDBName: strings.TrimSpace(u.expTargetDBEntry.Text),
356374
}
357375

358376
go func() {

ui/import.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io"
1010
"os"
1111
"os/exec"
12+
"strings"
1213

1314
"fyne.io/fyne/v2"
1415
"fyne.io/fyne/v2/container"
@@ -148,6 +149,9 @@ func (u *UI) createImportTab(w fyne.Window) fyne.CanvasObject {
148149
}
149150
}
150151

152+
dbTypeSelect := widget.NewSelect([]string{string(models.DBTypeMySQL), string(models.DBTypeMariaDB), string(models.DBTypePostgreSQL)}, nil)
153+
dbTypeSelect.SetSelected(string(models.DBTypeMySQL))
154+
151155
dbHostEntry := widget.NewEntry()
152156
dbHostEntry.SetText("127.0.0.1")
153157
dbPortEntry := widget.NewEntry()
@@ -161,6 +165,7 @@ func (u *UI) createImportTab(w fyne.Window) fyne.CanvasObject {
161165
dbGroup := widget.NewCard("Destination Database", "", container.NewVBox(
162166
isDockerCheck,
163167
widget.NewForm(
168+
widget.NewFormItem("DB Type", dbTypeSelect),
164169
widget.NewFormItem("Container Name/ID", containerIDEntry),
165170
widget.NewFormItem("DB Host", dbHostEntry),
166171
widget.NewFormItem("DB Port", dbPortEntry),
@@ -224,19 +229,20 @@ func (u *UI) createImportTab(w fyne.Window) fyne.CanvasObject {
224229
}
225230

226231
p := models.Profile{
227-
Host: hostEntry.Text,
228-
Port: portEntry.Text,
229-
SSHUser: sshUserEntry.Text,
230-
SSHPassword: sshPasswordEntry.Text,
232+
Host: strings.TrimSpace(hostEntry.Text),
233+
Port: strings.TrimSpace(portEntry.Text),
234+
SSHUser: strings.TrimSpace(sshUserEntry.Text),
235+
SSHPassword: strings.TrimSpace(sshPasswordEntry.Text),
231236
AuthType: models.AuthType(authTypeSelect.Selected),
232-
AuthKeyPath: keyPathEntry.Text,
233-
DBHost: dbHostEntry.Text,
234-
DBPort: dbPortEntry.Text,
235-
DBUser: dbUserEntry.Text,
236-
DBPassword: dbPasswordEntry.Text,
237+
AuthKeyPath: strings.TrimSpace(keyPathEntry.Text),
238+
DBHost: strings.TrimSpace(dbHostEntry.Text),
239+
DBPort: strings.TrimSpace(dbPortEntry.Text),
240+
DBUser: strings.TrimSpace(dbUserEntry.Text),
241+
DBPassword: strings.TrimSpace(dbPasswordEntry.Text),
242+
DBType: models.DBType(dbTypeSelect.Selected),
237243
IsDocker: isDockerCheck.Checked,
238-
ContainerID: containerIDEntry.Text,
239-
TargetDBName: targetDBEntry.Text,
244+
ContainerID: strings.TrimSpace(containerIDEntry.Text),
245+
TargetDBName: strings.TrimSpace(targetDBEntry.Text),
240246
}
241247

242248
go func() {

ui/ui.go

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,26 +83,14 @@ func (u *UI) Run() {
8383
func() fyne.CanvasObject {
8484
label := widget.NewLabel("Profile Name")
8585
saveBtn := widget.NewButtonWithIcon("", theme.DocumentSaveIcon(), nil)
86+
duplicateBtn := widget.NewButtonWithIcon("", theme.ContentCopyIcon(), nil)
87+
renameBtn := widget.NewButtonWithIcon("", theme.DocumentCreateIcon(), nil) // Pencil/Edit icon
8688
deleteBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), nil)
87-
buttons := container.NewHBox(saveBtn, deleteBtn)
89+
buttons := container.NewHBox(saveBtn, duplicateBtn, renameBtn, deleteBtn)
8890
return container.NewBorder(nil, nil, nil, buttons, label)
8991
},
9092
func(i int, o fyne.CanvasObject) {
91-
// The object is the container created above
92-
// Border container objects are usually: [center, top, bottom, left, right] order in Objects slice?
93-
// Actually, it's better not to rely on index if possible, but Fyne implementation details vary.
94-
// Let's look at NewBorder implementation or behavior.
95-
// Usually Objects[0] is the main content (label) if it's Center.
96-
// Wait, container.NewBorder returns a container with objects.
9793
c := o.(*fyne.Container)
98-
99-
// We know the structure: Border(nil, nil, nil, HBox(Save, Delete), Label)
100-
// Label is likely Center. HBox is Right.
101-
// Accessing objects by type/position is risky if we don't know internal order.
102-
// However, we constructed it.
103-
// Let's iterate to find them or assume order.
104-
// Typically Center is first or last.
105-
// Safe way:
10694
var label *widget.Label
10795
var btnContainer *fyne.Container
10896

@@ -115,7 +103,9 @@ func (u *UI) Run() {
115103
}
116104

117105
saveBtn := btnContainer.Objects[0].(*widget.Button)
118-
deleteBtn := btnContainer.Objects[1].(*widget.Button)
106+
duplicateBtn := btnContainer.Objects[1].(*widget.Button)
107+
renameBtn := btnContainer.Objects[2].(*widget.Button)
108+
deleteBtn := btnContainer.Objects[3].(*widget.Button)
119109

120110
p := u.profiles[i]
121111
label.SetText(p.Name)
@@ -152,6 +142,30 @@ func (u *UI) Run() {
152142
dialog.ShowInformation("Saved", fmt.Sprintf("Profile '%s' updated", p.Name), u.window)
153143
}
154144

145+
duplicateBtn.OnTapped = func() {
146+
// Clone profile
147+
newProfile := p
148+
newProfile.ID = fmt.Sprintf("%d", time.Now().Unix())
149+
newProfile.Name = p.Name + " (Copy)"
150+
151+
u.profiles = append(u.profiles, newProfile)
152+
u.saveProfiles()
153+
sidebar.Refresh()
154+
sidebar.Select(len(u.profiles) - 1)
155+
}
156+
157+
renameBtn.OnTapped = func() {
158+
entry := widget.NewEntry()
159+
entry.SetText(p.Name)
160+
dialog.ShowCustomConfirm("Rename Profile", "Rename", "Cancel", entry, func(b bool) {
161+
if b && entry.Text != "" {
162+
u.profiles[i].Name = entry.Text
163+
u.saveProfiles()
164+
sidebar.Refresh()
165+
}
166+
}, u.window)
167+
}
168+
155169
deleteBtn.OnTapped = func() {
156170
dialog.ShowConfirm("Delete Profile", fmt.Sprintf("Are you sure you want to delete '%s'?", p.Name), func(b bool) {
157171
if b {

0 commit comments

Comments
 (0)