Skip to content

Latest commit

 

History

History
918 lines (675 loc) · 37.4 KB

File metadata and controls

918 lines (675 loc) · 37.4 KB

DBack — Go Desktop App Agent Guide

This document is the AI roadmap for the DBack Go desktop application (dback module). Use it before exploring the whole repository. For the WordPress plugin REST agent, see wordpress/dback-db-tools/wordpress_agent.md.

Agent rule — version on every change: After any code or build change to the Go app, you must bump the app version in main.go and build.sh, update Current app version in Versioning below, and tell the user which git tag to push for release (see Build and embed). Never finish a change set without syncing version + tag instructions.


Purpose

DBack is a Gio desktop app (not a web server) for MySQL/MariaDB backup, restore, and SQL queries against remote hosts. Data (profiles, templates, history, logs, sync settings) lives in a local encrypted vault. Backup files (.sql.gz) are stored on disk under each host’s destination folder.

Stack: Go 1.22 · Gio UI · SSH/shell transport · WordPress REST plugin transport · MinIO S3 sync · Argon2id + AES-GCM vault.

Supported desktop targets: Linux and Windows (primary release platforms). All new and changed Go/UI code must work correctly on both — not Linux-only.


Repository map

dback/
├── main.go                         # Entry: embed logo, ui.New().Run()
├── agent.md                        # This file (Go app roadmap)
├── models/models.go                # Domain types: Profile, bundles, vault payload
├── ui/                             # Gio UI (screens, widgets, theme, state)
├── internal/
│   ├── app/                        # Business orchestration (Backup, Restore, sync, vault API)
│   ├── store/                      # Persistence, vault, import/export bundles
│   ├── sync/s3.go                  # S3-compatible push/pull
│   └── secrets/                    # Argon2id + AES-GCM
├── backend/
│   ├── ssh/                        # SSH, JumpHost, Localhost executor
│   ├── db/                         # Shell command builders, validation, query parsing
│   ├── transfer/                   # Backup/restore strategies
│   ├── verify/                     # SHA256 quick check, fingerprint capture, deep-verify report
│   ├── preflight/                  # Remote preflight (SSH path)
│   └── wordpress/                  # REST client, plugin zip generation
└── wordpress/dback-db-tools/       # Embedded PHP plugin (see wordpress_agent.md)

Runtime data directory: ~/.config/dback (via ui.DesktopPlatform.AppDataDir()).

File Role
app_data.vault.json Encrypted vault (profiles, templates, history, logs, sync)
ssh_known_hosts SSH host key store
{Destination}/{HostName}/*.sql.gz Backup files (not in vault)

Architecture

flowchart TB
    subgraph ui [ui/]
        Hosts[Hosts screen]
        Backups[Backups screen]
        Profile[Profile editor]
        Settings[Settings + Sync]
    end

    subgraph app [internal/app]
        BackupFn[App.Backup]
        RestoreFn[App.Restore]
        QueryFn[App.RunImportQuery]
        SyncFn[App.SyncPush / SyncPull]
    end

    subgraph transport [Transport layer]
        SSH[backend/ssh Executor]
        Transfer[backend/transfer]
        WP[backend/wordpress Client]
    end

    subgraph remote [Remote]
        Shell[mysqldump / mysql CLI]
        Plugin[WP plugin dback/v1 REST]
    end

    ui --> app
    BackupFn --> Transfer
    RestoreFn --> Transfer
    QueryFn --> SSH
    QueryFn --> WP
    Transfer --> SSH
    Transfer --> WP
    SSH --> Shell
    WP --> Plugin
Loading

Layering rule: UI never calls backend/* directly. Always go through internal/app.App.


Connection types

Defined in models/models.go:

ConnectionTypeSSH       = "SSH"
ConnectionTypeJumpHost  = "JumpHost"
ConnectionTypeLocalhost = "Localhost"
ConnectionTypeWordPress = "WordPress"
Type Transport Backup Import Query Preflight
SSH ssh.Client → shell transfer.BackupSSH transfer.RestoreSSH db.BuildQueryCommand + executor preflight.Run (remote bash)
Jump Host SSH via bastion (newJumpClient) Same; tmp-file strategy first Same Same Same
Localhost LocalClient → local/WSL bash Same Same Same Same
WordPress HTTP REST dback/v1 transfer.BackupWordPress transfer.RestoreWordPress wordpress.Client.Query GET /preflight

Branching in app layer (internal/app/app.go):

if profile.UsesWordPress() {
    // wordpress path
} else {
    // ssh.NewExecutor + transfer.BackupSSH / RestoreSSH
}

Helpers on models.Profile:

Method Meaning
UsesSSH() SSH or JumpHost
UsesWordPress() WordPress REST
IsLocalhost() Local shell
SupportsSQLQuery() MySQL/MariaDB, or WordPress
AllowsImport() !ImportProtected

Profile fields by connection type

Shared (all types)

Field Purpose
ID, Name, Group Identity
ConnectionType SSH / JumpHost / Localhost / WordPress
TargetDBName DB name for filenames, query placeholders, WordPress import target
Destination Local folder for backup files
PreImportQuery, RunQueryBeforeImport SQL before restore
PostImportQuery, RunQueryAfterImport SQL after restore
ImportProtected Block restore to this host
DBType MySQL or MariaDB (WordPress defaults to MySQL in UI)

SSH / Jump Host / Localhost

Field Notes
Host, Port SSH target (hidden for Localhost)
SSHUser, SSHPassword, AuthType, AuthKeyPath, AuthKeyPEM SSH auth
JumpHost, JumpPort, JumpUser, JumpPassword, JumpAuthType, JumpAuthKeyPath, JumpAuthKeyPEM Jump Host only
DBHost, DBPort, DBUser, DBPassword Remote DB (unless Docker)
IsDocker, ContainerID docker exec path

WordPress

Field Notes
WPUrl Site URL (https://example.com)
WPKey API key → header X-DBACK-KEY
Host Synced from WPUrl on save
DB credential fields Not used — plugin uses wp-config
TargetDBName Empty → WordPress default DB (DB_NAME); set → X-DBACK-DATABASE on import/query

Legacy (migration only, never written on save): ExportSettings, ImportSettings — flattened by store.flattenProfile.


Backup flow

Entry points

Layer Symbol File
UI UI.runBackup ui/hosts.go
App App.Backup internal/app/app.go

SSH / Jump / Localhost path

App.Backup
  → transfer.BackupSSH
    → db.ValidateProfileForRemoteOps
    → ssh.NewExecutor(profile)
    → preflight.Run(client, profile, 0, operationID)
    → estimateBackupTotal (optional progress)
    → mkdir {Destination}/{safeName(Name)}/
    → file: {TargetDBName}_{DD_MM_YYYY_HH_MM_SS}.sql.gz
    → strategies: streaming → tmp-file (JumpHost: tmp-file first)
    → validateBackupIntegrity, checksum
  → ExportRecord → vault history

Strategies (backend/transfer/transfer.go):

Strategy Behavior
StrategyStreaming RunCommandStream(BuildExportCommand) → write local file
StrategyTmpFile Remote dump to tmp → download with resume (.meta metadata)

Preflight (backend/preflight/): OS, dump tools, gzip/zstd, disk space, Docker status, writable tmp dirs.

WordPress path

App.Backup → transfer.BackupWordPress
  → client.Preflight (GET /preflight)
  → client.Export (GET /export) → stream gzip to local file

No tmp-file fallback. Plugin details: wordpress_agent.md.

Validation

  • Minimum backup size: 128 bytes (app + transfer).
  • Jump Host: prefer tmp-file — double SSH tunnel streaming is unreliable for large dumps.

Dry-Run Verify (backup verification)

Two-layer verification compares each backup against a fingerprint captured at backup time — never against the live source database.

flowchart TB
    subgraph backupTime [At backup time]
        File[".sql.gz on disk"]
        SHA[SHA256 checksum]
        FP["table row counts from source DB"]
        Vault[ExportRecord in vault]
        File --> SHA --> Vault
        File --> FP --> Vault
    end
    subgraph quick [Quick verify — automatic]
        QCheck["QuickCheck: SHA256 vs stored checksum"]
        QCheck --> QuickVerified
    end
    subgraph deep [Deep verify — optional]
        TempDB["temp DB dback_verify_*"]
        Restore["RestoreSSH / RestoreWordPress to temp DB"]
        Count["COUNT(*) per table"]
        Report["BuildTableReport vs fingerprint"]
        TempDB --> Restore --> Count --> Report
    end
    Vault --> QCheck
    Vault --> Restore
Loading

Layer 1 — Fingerprint at backup time (automatic)

After a successful backup in App.Backup (internal/app/app.go):

  1. verify.ChecksumFileExportRecord.Sha256
  2. verify.CaptureFingerprint (default ModeFast) → ExportRecord.Fingerprint
  3. applyAutoQuickVerifyExportRecord.QuickVerified

Fingerprint query (fast mode): information_schema.tablestable_name, table_rows for the backup schema.

Transport Schema in query
SSH / Jump / Localhost table_schema = '{TargetDBName}'
WordPress, empty TargetDBName table_schema = DATABASE()
WordPress, named DB table_schema = '{TargetDBName}'

Fingerprint capture uses App.RunImportQuery via appQueryRunner (internal/app/verify.go).

Layer 2 — Quick verify (SHA256 only)

Symbol Location
verify.QuickCheck backend/verify/quick.go
App.QuickVerify internal/app/verify.go
App.BackupVerifyStatus display helper for list/detail

Recomputes SHA256 of the local .sql.gz and compares to ExportRecord.Sha256. No database connection. Runs automatically after every new backup; can be re-run via App.QuickVerify.

Layer 3 — Deep verify (optional)

Symbol Location
App.DeepVerify internal/app/verify.go
verify.CountTablesExact, verify.BuildTableReport backend/verify/fingerprint.go, report.go
verify.TempDBName dback_verify_{unix}

Prerequisites: Fingerprint present, quick check passes, destination AllowsImport() (!ImportProtected).

Flow:

App.DeepVerify
  → verify.QuickCheck (abort if SHA256 mismatch)
  → tempDB = verify.TempDBName()
  → WordPress only: prepareVerifyDatabase (DROP+CREATE via RunImportQuery, connectDB=false)
  → transfer.RestoreSSH or RestoreWordPress with TargetDBOverride=tempDB
  → verify.CountTablesExact on temp DB (exact COUNT(*) per table)
  → verify.BuildTableReport vs record.Fingerprint
  → persist ExportRecord.DeepVerified
  → dropVerifyDatabase (defer cleanup on failure)

SSH / Jump / Localhost: RestoreRequest.TargetDBOverride switches restore to a temp database only — restoreProfile overrides TargetDBName; uses BuildImportPrepareTempCommand and BuildImportStreamCommandForVerify so production DB is not dropped.

WordPress: prepareVerifyDatabase creates temp DB without X-DBACK-DATABASE; restore/import uses X-DBACK-DATABASE: tempDB.

Result semantics (App.BackupDeepVerifyStatus):

State Meaning
matched SHA256 OK and all table row counts match fingerprint
row_diff SHA256 OK but row counts differ (fast fingerprint is approximate; file may still be valid)
none Deep verify not run

Deep verify failure with mismatches returns error from App.DeepVerify but still persists DeepVerified with Passed: false and per-table Report.

ExportRecord verify fields

Field Role
Sha256 SHA256 of backup file at creation
Fingerprint BackupFingerprintMode, Tables, TotalRows, CapturedAt
QuickVerified Last quick (SHA256) verify result
DeepVerified Last deep verify result + Report
LastVerified Legacy; prefer QuickVerified / DeepVerified

Import destination memory (vault)

AppVaultPayload.ImportDestByProfile maps source host profile ID → last chosen destination profile ID for import and deep verify.

API File
Store.ImportDestForProfile, SetImportDestForProfile internal/store/store.go
App.ImportDestForProfile, SetImportDestForProfile internal/app/import_prefs.go

Parser note

db.parseTSVBlock accepts single-column MySQL CLI output without tabs (e.g. COUNT(*)\n0) — required for exact row counts during deep verify.

Risks and constraints

  • Fast fingerprint uses information_schema.table_rows (approximate on InnoDB). Deep verify compares restored data against that snapshot, not a live re-read of production.
  • Old backups without Fingerprint cannot deep verify — user must create a new backup.
  • Deep verify cost: full restore to a writable host (CPU, disk, time); temp DB name must match dback_verify_* prefix (safety check).
  • Quick verify requires the backup file on local disk (backup file not found if only metadata synced).

Tests

Area Path
Checksum / quick check backend/verify/quick_test.go
Fingerprint parse / report backend/verify/fingerprint_test.go, report_test.go
Import dest prefs internal/store/vault_test.goTestVaultPersistsImportDestByProfile

Import / restore flow

Entry points

Layer Symbol File
UI UI.runRestore ui/backups.go
App App.Restore internal/app/app.go

Order of operations

1. Check destination.AllowsImport()  (ImportProtected)
2. Pre-import query (optional)       → RunImportQuery, connectDB=false
3. transfer.RestoreSSH or RestoreWordPress
4. Post-import query (optional)      → RunImportQuery, connectDB=true

Important: Pre-import query failure aborts restore before any import upload starts. Pre-import runs when PreImportQuery is non-empty (WordPress and SSH). Post-import query failure returns an error after the database import completes.

WordPress before-import: DBack sends no X-DBACK-DATABASE header so DROP/CREATE DATABASE can run against the server default connection. After-import and manual queries with connectDB use TargetDBName when set.

SSH path

transfer.RestoreSSH
  → preflight.Run(client, profile, fileSize, operationID)
  → detectCompression (gzip / zstd magic)
  → BuildImportPrepareCommand: DROP + CREATE DATABASE (when applicable)
  → strategies: streaming (pipe stdin) → tmp-file upload + import from file

Key builders: backend/db/commands.goBuildImportStreamCommand, BuildImportFromFileCommand, BuildImportPrepareCommand.

WordPress path

transfer.RestoreWordPress
  → client.Preflight
  → client.Import(body, db.WordPressImportDatabase(profile))

Import body from $request->get_body() (not php://input). Target DB via X-DBACK-DATABASE.


Query execution

Entry points

Context Call chain
Host editor → Queries tab ui/query.goApp.RunImportQuery
Pre/post import App.Restore
Connection test (DB step) App.TestDatabaseConnection

Paths

WordPress:

wordpress.NewClient(profile)
  → client.Query(ctx, sql, db.WordPressImportDatabase(profile))
  → POST /query + optional X-DBACK-DATABASE
  → JSON → db.QueryResult

SSH / Jump / Localhost:

db.BuildQueryCommand(profile, query, connectDB)  // base64 pipe, anti-injection
  → ssh.NewExecutor → RunCommand
  → db.ParseMySQLBatchOutput

Placeholders (models.SubstituteQuery): {databasename}, {host}, {profile}, {dbuser}.

UI limits: 100 rows, 20 columns (ui/query.go).


Connection test

Two-step timeline (ui/host_connection.go):

Step SSH/Jump/Local WordPress
Server echo dback-server-ok via executor GET /ping
Database SELECT 1 via mysql CLI SELECT 1 via REST query

Labels: serverConnectionStepLabel() — Local shell / WordPress REST API / Server connection.


S3 sync

Item Location
S3 client internal/sync/s3.go
App API internal/app/sync.goSyncPush, SyncDownload, PreviewSyncImport, ImportAppDataFromBundle
UI ui/settings_sync.go
Model models.SyncSettings, models.SyncActivity

Remote object: {bucket}/dback/app-data.json (sync.ObjectKey).

SyncSettings fields: Endpoint, Region, Bucket, AccessKeyID, SecretKey, UseSSL.

Push

App.SyncPush
  → store.MarshalAppDataBundleForSync (encrypt with vault master key)
  → sync.Push → PutObject
  → store.RecordSyncPush (local LastPushAt only)

Pull

App.SyncDownload → sync.Pull
  → PreviewSyncImport / ImportAppDataFromBundle
  → merge profiles/templates + Reload
  → RecordSyncPull

Included in sync bundle: profiles, templates, history metadata, logs, sync credentials.
Excluded: backup .sql.gz files, SyncActivity timestamps.

Encryption: Sync uses vault master key (unlock passphrase). Local file export uses a separate export password (App.ExportAppData).


Vault and persistence

Concern File / symbol
Vault file {baseDir}/app_data.vault.jsonstore.VaultPath()
Payload models.AppVaultPayload — profiles, templates, history, logs, sync
Crypto internal/secrets/ — Argon2id + AES-256-GCM
Lifecycle CreateVault, Unlock, Lock, Reloadinternal/app/app.go
Legacy migration Plaintext JSON files → vault on first unlock
Secret stripping store.stripSecrets — passwords, keys, WPKey on export without secrets

All store writes require unlocked vault (ErrVaultLocked otherwise).


WordPress plugin integration

Item Location
Plugin source wordpress/dback-db-tools/
Embed wordpress/dback-db-tools/embed.go//go:embed
Zip build backend/wordpress/pluginzip.goBuildPluginZip(siteURL, apiKey)
App facade internal/app/pluginzip.goBuildWordPressPluginZip
UI Host form → Generate Token, Download Plugin — ui/settings_form.go

Output filename: dback-{hostname}-{pluginVersion}.zip. Token hardcoded in DBACK_HARDCODED_API_KEY placeholder.

REST contract: namespace dback/v1, auth X-DBACK-KEY, optional X-DBACK-DATABASE. Full API: wordpress_agent.md.


Adding a new host type — checklist

  1. Modelmodels/models.go

    • Add ConnectionType constant
    • Add Profile fields if needed
    • Add UsesX(), update SupportsSQLQuery() if applicable
  2. Validationbackend/db/validate*.go

    • ValidateProfileForX, wire into ValidateProfileForOps
  3. Transport

    • Shell-based: extend ssh.NewExecutor or reuse it
    • HTTP/API-based: new package like backend/wordpress with a client
  4. Transferbackend/transfer/

    • BackupX, RestoreX using same BackupRequest / RestoreRequest types
  5. App orchestrationinternal/app/app.go

    • Branch in: Backup, Restore, RunImportQuery, TestServerConnection, TestDatabaseConnection
  6. Preflight

    • SSH: extend db.BuildPreflightScript + preflight.Run
    • REST: new endpoint + client parser (WordPress pattern)
  7. Storeinternal/store/store.go

    • normalizeProfile defaults; do not filter out new type in flattenProfiles
  8. UIui/settings_form.go

    • Add to connTypeValues
    • Conditional fields in SettingsForm.layout
    • Update hostConnectionSubtitle in ui/helpers.go
    • Update serverConnectionStepLabel in ui/host_connection.go
  9. Tests — model helpers, store, client httptest, app integration

Pattern to copy for HTTP hosts: WordPress (backend/wordpress/ + plugin embed).

Pattern to copy for shell hosts: Localhost (special case in ssh.NewExecutor).


UI guidelines (Gio)

Theme tokens — ui/theme.go (AppTheme)

Token Default Use
Padding 24dp Main content inset (layoutContent)
CardPadding 20dp Inside card()
Gap 16dp Vertical spacing — vgap(theme)
SectionGap 28dp Between major sections
Radius 12dp Cards
RadiusSm 8dp Inputs, small buttons

Colors: GitHub dark palette — Bg, Surface, Accent (green), Link (blue), Danger, Text, TextMuted.

Layout patterns

Pattern Where How
Desktop shell layoutDesktop 248dp sidebar + Flexed(1) content
Content padding layoutContent Inset with theme.Padding on all sides
Full-width content Inside content gtx.Constraints.Min.X = gtx.Constraints.Max.X
Centered modal/card Login, dialogs layout.Center.Layout + max width (e.g. 440dp)
Right-aligned actions Profile footer Flexed(1) empty spacer + buttons
Horizontal button groups Forms layout.Flex{Axis: Horizontal} + hgap(theme)
Scrollable forms Profile editor scrollArea + widget.List

Spacing helpers — ui/widgets.go

vgap(theme)              // vertical Gap (16dp)
hgap(theme)              // horizontal Gap (16dp)
spacer(theme, unit.Dp(n)) // fixed vertical space

Label → field gap: 6dp inset in labeledField.

Components — use existing, do not reinvent

Component Function Notes
Card card(gtx, theme, widget) Surface + 1dp border, CardPadding
Primary button primaryButton(gtx, th, theme, &btn, label, onClick) Green accent
Secondary button secondaryButton(...) Outlined
Danger button dangerButton(...) Delete actions
Tab tabButton(...) Profile Connection / Queries
Text input editorField inside borderedInput Focus ring = Link color
Password passwordField Toggle visibility
Enum / dropdown enumField Connection type, DB type
Checkbox checkboxField Import protected, Docker
Muted help mutedLabel Secondary instructions
Section title sectionTitle (H4) / material.Subtitle1 in cards
Page header pageHeader(gtx, th, theme, title, action) Title left, action right
Dialog ui/dialog.go Overlay stack on main layout
Loading DialogLoading Long operations

Async UI pattern

  • Long ops run in goroutines (ui/jobs.go, hosts.go, backups.go).
  • Update UI via u.invalidate() after state changes.
  • Use operationJob for cancel + progress.
  • Never block the Gio event loop.

Platform

  • ui/platform.goPlatform interface (AppDataDir, OpenFolder, IsMobile); Windows uses explorer/start, Linux uses xdg-open/explorer fallbacks.
  • File picks: ui/explorer.gopickOpenFile, pickSaveFile, pickSaveBytes, pickFolder (Gio explorer on both desktop OSes).
  • See Cross-platform requirement — mandatory for all file I/O and download flows.

UI do's and don'ts

Do:

  • Reuse AppTheme tokens for all spacing — no magic numbers unless one-off (e.g. login 20dp spacer).
  • Wrap related fields in card().
  • Hide irrelevant fields by connection type (see SettingsForm.layout pattern with isWordPress, isLocal, isJump).
  • Call u.core.* app methods from UI — never import backend/* in UI for operations.
  • Use secondaryButton for secondary actions, primaryButton for main CTA.

Don't:

  • Add raw material.Button without theme colors.
  • Hardcode colors outside AppTheme.
  • Run SSH/HTTP on the UI thread.
  • Duplicate vault or transfer logic in UI.

Cross-platform requirement (Linux + Windows) — mandatory

DBack ships on Linux and Windows. Every feature you add or change must work correctly on both platforms before merge. Treat Linux-only or Windows-only shortcuts as bugs unless explicitly documented and approved.

Scope

Layer Must be cross-platform
Go app (internal/, backend/, models/) Yes — paths, filenames, I/O, zip, crypto
Gio UI (ui/) Yes — file dialogs, folders, errors, async saves
WordPress plugin zip download Yes — Windows save dialog + filename rules
Remote SSH targets Linux servers only (expected); the desktop app still runs on Win/Linux

Path and filesystem rules

  • Use filepath.Join, filepath.ToSlash, os.UserHomeDir, os.UserConfigDir for OS paths — never hardcode / for local disk paths.
  • Use path.Join only inside zip archive entry names (always forward slashes).
  • Do not assume ~/.config/dback literally — use ui.DesktopPlatform.AppDataDir() / store paths.
  • 0644 / 0755 permissions on os.WriteFile / MkdirAll are fine (Windows ignores Unix mode bits).
  • Avoid shelling out without runtime.GOOS branching — see backend/ssh/local.go (Windows → WSL when needed).

Filenames and downloads

  • Any user-facing download name (plugin zip, exports) must pass through sanitizers — see sanitizeDownloadFilename / sanitizeFilename in backend/wordpress/pluginzip.go.
  • Strip or replace Windows-forbidden characters: \ / : * ? " < > |
  • Avoid Windows reserved basenames: CON, PRN, AUX, NUL, COM1COM9, LPT1LPT9
  • Trim trailing . and spaces; cap length (~200 chars for suggested save names).
  • Plugin zip internal folder name must match the zip basename (without .zip).

UI / file dialogs (ui/explorer.go)

  • Use gioui.org/x/explorerCreateFile / ChooseFile — not hand-rolled paths per OS except where already wrapped (chooseFolderDialog).
  • pickSaveBytes pattern: write with wc.Write(data) then wc.Close(); surface errors via showError; treat explorer.ErrUserDecline as cancel (no error toast).
  • Never fail silently when save/write fails — user must see showError.
  • Folder open: ui/platform.goexplorer on Linux, start on Windows.

Shell / SSH (remote vs local)

  • Remote backup commands assume bash on Linux servers — unchanged.
  • Localhost on Windows uses WSL when available (local.go) — do not assume /bin/bash on the desktop OS.
  • Do not use Unix-only tools in code paths that run on the Windows desktop without a GOOS guard.

WordPress / HTTP

  • REST client is OS-agnostic; keep JSON, streaming, and zip building in memory (bytes.Buffer + archive/zip).
  • Hardcoded plugin token uses base64url — safe in PHP single-quoted strings.

Testing checklist (required for UI / I/O changes)

Before finishing a task that touches paths, files, dialogs, or zip:

  1. Run go test ./... (CI runs on Linux; catches logic regressions).
  2. Mentally verify Windows paths: save dialog, filename with dots, cancel vs error.
  3. For plugin zip: assert all entries live under stable dback-db-tools/ root (TestBuildPluginZipUsesStableRootFolder).
  4. For explorer changes: verify both code paths — *os.File from Gio and error handling.

If you cannot run Windows locally, document assumptions and add unit tests for sanitizers and pure logic.

Cross-platform do's

  • Branch with runtime.GOOS == "windows" when OS behavior differs.
  • Return clear errors to the UI — never swallow I/O failures in goroutines.
  • Keep business logic free of OS tests when possible (sanitize in backend/, call from UI).
  • Match existing patterns in explorer.go, platform.go, pluginzip.go.

Cross-platform don'ts

  • Don't hardcode /tmp, /home, or ~ in desktop code.
  • Don't use path.Join for local filesystem paths.
  • Don't assume Gio CreateFile always succeeds without handling write/close errors.
  • Don't ship download filenames with : or \ from raw URLs.
  • Don't add zenity/kdialog requirements for core features — Windows has no equivalent; use Gio explorer first.
  • Don't merge file-save or plugin-zip changes without filename safety tests.

Build and embed

Item Location
Logo main.go//go:embed logo.png
Version main.go default + build.sh APP_VERSION; must match release git tag
Linux .deb packaging/nfpm.yaml + build.shdist/dback_${VERSION}_amd64.deb
Launchpad PPA debian/ + packaging/build-ppa.sh; CI matrix (noble + jammy): .github/workflows/ppa.yml; see ppa.md
Desktop icon (deb) packaging/dback.desktop + packaging/icons/hicolor/*/apps/dback.png
In-app updater internal/update/ + internal/app/update.go; About → Check for updates
Plugin embed wordpress/dback-db-tools/embed.go
Linux build build.sh — CGO + Gio/EGL + nfpm deb
Windows build GitHub Actions + go build — see README.md
CI release .github/workflows/go.yml — Go 1.22, GOTOOLCHAIN=local; runs on tag push v*

Go toolchain: go.mod declares go 1.22. CI and Launchpad use GOTOOLCHAIN=local (no auto-download). PPA builds vendor deps at package time (vendor/ is gitignored). Launchpad jammy may install golang-1.22-go without /usr/bin/go, so debian/rules must export /usr/lib/go-1.22/bin in PATH; debian/prepare-go.sh verifies and logs the actual go binary. Do not run go mod tidy with a newer local Go without verifying CI/PPA still pass.

Current app version in repo: 3.8.3 → About screen and local ./build.sh use this until you bump again.

Local build

./build.sh linux
# or explicitly:
APP_VERSION=3.8.3 ./build.sh linux

Outputs: dist/dback-linux, dist/dback, dist/dback_3.8.3_amd64.deb.
build.sh prints the release git tag to push when the build succeeds.

GitHub Release (after merging to master)

Tag must match main.go / build.sh version (3.8.3 → tag v3.8.3). CI strips the v and embeds the version in binaries and the .deb name.

git tag v3.8.3
git push origin v3.8.3

GitHub Actions then publishes:

Asset Name
Linux binary dback-linux
Windows binary dback-windows.exe
Debian package dback_3.8.3_amd64.deb

PPA: ppa.yml uploads two source packages per tag — PPA_DIST=noble and PPA_DIST=jammy — via packaging/sync-debian-changelog.sh. See ppa.md.

When you bump the app version, update this table’s examples and the tag commands to the new v{x.y.z}.

Cross-platform: all embedded assets and zip/download logic must work on both targets — see Cross-platform requirement above.


Key symbols quick reference

Concern Primary symbols File
Backup App.Backup, transfer.BackupSSH, transfer.BackupWordPress internal/app/app.go, backend/transfer/
Verify App.QuickVerify, App.DeepVerify, verify.QuickCheck, verify.CaptureFingerprint internal/app/verify.go, backend/verify/
Restore App.Restore, transfer.RestoreSSH, transfer.RestoreWordPress same
Query App.RunImportQuery, db.BuildQueryCommand, wordpress.Client.Query internal/app/app.go, backend/db/, backend/wordpress/
Executor ssh.NewExecutor, ssh.NewClient, LocalClient backend/ssh/executor.go
Commands BuildExportCommand, BuildImportStreamCommand, BuildPreflightScript backend/db/commands.go
Vault Store.Unlock, Store.SaveProfiles internal/store/
Sync App.SyncPush, sync.Push internal/app/sync.go, internal/sync/s3.go
UI shell UI.layout, Section, View ui/app.go, ui/state.go
About / updates layoutAbout, runAboutUpdateCheck ui/about.go, ui/about_update.go
Profile model models.Profile, ConnectionType models/models.go

Hard constraints (do's and don'ts)

Do

  • Branch WordPress vs non-WordPress at internal/app using profile.UsesWordPress().
  • Use ssh.NewExecutor(profile) as the single shell entry for SSH/Jump/Localhost.
  • Run preflight before backup/restore on SSH path.
  • Mask commands in logs: db.MaskCommand.
  • Substitute query placeholders before execution.
  • Respect context.Context — close SSH on cancel.
  • Clear legacy ExportSettings / ImportSettings on save.
  • Validate minimum backup size (128 bytes).
  • Capture SHA256 + fingerprint after every successful backup (captureBackupMetadata).
  • Use TargetDBOverride only for deep verify temp DB restores — never point it at production.
  • Use tmp-file-first for Jump Host backups.
  • Keep vault locked checks on all store access.
  • Pass X-DBACK-DATABASE for WordPress import and query when TargetDBName is set.
  • Sanitize download filenames and handle save dialog errors on Linux and Windows.
  • Run go test ./...; add tests for path/filename/zip logic when touching I/O.
  • Bump app version in main.go and build.sh on every change set (see Versioning).

Don't

  • Don't call ssh.NewClient directly from app code — use NewExecutor.
  • Don't write ExportSettings/ImportSettings on save (migration only).
  • Don't continue restore if pre-import query fails — abort and show error.
  • Don't ship Linux-only desktop code without GOOS handling or documented exception.
  • Don't swallow file save errors in pickSaveBytes / export flows.
  • Don't include backup files in app bundles or S3 sync.
  • Don't include SyncActivity in remote sync payload.
  • Don't skip ImportProtected check before restore.
  • Don't interpolate SQL into shell — use base64 pipe in BuildQueryCommand.
  • Don't assume streaming works through double SSH tunnel (Jump Host).
  • Don't add connection-type logic only in UI — mirror in app + transfer/backend.
  • Don't read php://input in WordPress plugin for REST import — use $request->get_body().
  • Don't trust wpdb->select() return value on modern WordPress — it returns null on success; verify with $wpdb->ready and SELECT DATABASE().

Testing

go test ./...
# CI uses Go 1.22 with GOTOOLCHAIN=local — match locally when debugging CI failures:
GOTOOLCHAIN=go1.22.12 go test ./...

Key test locations:

Area Path
WordPress client backend/wordpress/client_test.go
App updater internal/update/*_test.go
Transfer / validate backend/transfer/*_test.go
Dry-Run Verify backend/verify/*_test.go
Store / vault internal/store/store_test.go
UI helpers ui/helpers_test.go, ui/filters_test.go

Related documentation

Document Scope
wordpress/dback-db-tools/wordpress_agent.md PHP plugin REST API, export/import/query, auth, constraints
README.md User-facing features and setup

Versioning

Current app version: 3.8.3

The WordPress plugin has its own version in wordpress/dback-db-tools/ (DBACK_DB_TOOLS_VERSION) — see wordpress_agent.md. Do not confuse the two.

Required on every DBack app change (mandatory)

When you modify Go app code, UI, build/CI, updater, or user-visible behavior:

  1. Bump the app version (patch by default: 3.8.03.8.1; minor: 3.7.x3.8.0):
    • main.govar appVersion = "…" (local/go run default)
    • build.shAPP_VERSION="${APP_VERSION:-…}"
  2. Update examples in README.md if they show a pinned version.
  3. Update Current app version here and in Build and embed.
  4. Tell the user the release git tag to push: v{same version} (e.g. v3.8.3 for app version 3.8.3).
git tag v3.8.3
git push origin v3.8.3

CI reads the tag (v3.8.3APP_VERSION=3.8.3); tag and main.go/build.sh must always match. PPA changelog per Ubuntu series is synced in CI — do not commit jammy/noble-specific changelog entries unless doing a manual PPA upload.

Agent checklist before finishing

  • main.go appVersion bumped
  • build.sh default APP_VERSION bumped
  • agent.md Current app version + Build section tag examples updated
  • User told: git tag vX.Y.Z and git push origin vX.Y.Z

Bump guide

Change type Example bump
Bug fix, CI/packaging, docs tied to app behavior 3.8.03.8.1
New user-facing feature (backward compatible) 3.7.x3.8.0
Breaking profile/vault/sync contract 3.x4.0.0

Do not

  • Leave main.go / build.sh defaults on an old version after shipping a newer tag.
  • Commit dist/ binaries or .deb files (see .gitignore).
  • Bump only the Git tag without updating main.go and build.sh defaults.

Version note

When this doc and code diverge, trust the code and update this file. Last aligned with v3.8.3 — Go 1.22 toolchain, jammy/noble PPA matrix, offline vendor builds, in-app updater, Dry-Run Verify (SHA256 + fingerprint + deep verify), and mandatory app version bumps on changes.