2026-08-13 14:32:54 -05:00
|
|
|
import { dialog, ipcMain } from 'electron'
|
|
|
|
|
import { createReadStream, createWriteStream } from 'fs'
|
|
|
|
|
import { mkdir, readdir, rm, stat } from 'fs/promises'
|
|
|
|
|
import { basename, join } from 'path'
|
|
|
|
|
import { spawn } from 'child_process'
|
|
|
|
|
import { createGunzip, createGzip } from 'zlib'
|
|
|
|
|
import { AURORA_ENV, getProjectConfigByRoot } from '../auroraEngine'
|
2026-08-22 04:17:22 -05:00
|
|
|
import { startNativeProject } from '../native/nativeProject'
|
|
|
|
|
import { runtimeRoot } from '../nativeRuntime'
|
2026-08-13 14:32:54 -05:00
|
|
|
import type { AuroraSnapshot } from '../../shared/types'
|
|
|
|
|
|
|
|
|
|
const snapshotsDir = (root: string): string => join(root, '.aurora', 'snapshots')
|
|
|
|
|
const composeFile = (root: string): string => join(root, '.aurora', 'compose.yaml')
|
|
|
|
|
|
|
|
|
|
function safeSnapshotName(value?: string): string {
|
|
|
|
|
const fallback = new Date().toISOString().replace(/[:.]/g, '-')
|
|
|
|
|
const safe = (value?.trim() || fallback).replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '')
|
|
|
|
|
return safe || fallback
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:17:22 -05:00
|
|
|
function dbCommand(
|
|
|
|
|
type: 'mariadb' | 'mysql' | 'postgres',
|
|
|
|
|
mode: 'dump' | 'restore'
|
|
|
|
|
): { command: string; args: string[] } {
|
2026-08-13 14:32:54 -05:00
|
|
|
if (type === 'postgres') {
|
|
|
|
|
return mode === 'dump'
|
|
|
|
|
? { command: 'pg_dump', args: ['-U', 'db', '-d', 'db', '--clean', '--if-exists'] }
|
|
|
|
|
: { command: 'psql', args: ['-U', 'db', '-d', 'db', '-v', 'ON_ERROR_STOP=1'] }
|
|
|
|
|
}
|
2026-08-14 13:47:25 -05:00
|
|
|
if (type === 'mysql') {
|
|
|
|
|
return mode === 'dump'
|
2026-08-22 04:17:22 -05:00
|
|
|
? {
|
|
|
|
|
command: 'mysqldump',
|
|
|
|
|
args: ['-udb', '-pdb', '--single-transaction', '--routines', '--triggers', 'db']
|
|
|
|
|
}
|
2026-08-14 13:47:25 -05:00
|
|
|
: { command: 'mysql', args: ['-udb', '-pdb', 'db'] }
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
return mode === 'dump'
|
2026-08-22 04:17:22 -05:00
|
|
|
? {
|
|
|
|
|
command: 'mariadb-dump',
|
|
|
|
|
args: ['-udb', '-pdb', '--single-transaction', '--routines', '--triggers', 'db']
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
: { command: 'mariadb', args: ['-udb', '-pdb', 'db'] }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:17:22 -05:00
|
|
|
async function runDatabasePipe(
|
|
|
|
|
root: string,
|
|
|
|
|
mode: 'dump' | 'restore',
|
|
|
|
|
filePath: string
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const config = await getProjectConfigByRoot(root)
|
|
|
|
|
let command: string
|
|
|
|
|
let args: string[]
|
|
|
|
|
if (config.runtimeEngine === 'native') {
|
|
|
|
|
if (!config.nativePorts) throw new Error('Native project has no allocated database port.')
|
|
|
|
|
if (config.database !== 'mariadb')
|
|
|
|
|
throw new Error('Aurora Native database portability currently supports MariaDB.')
|
|
|
|
|
await startNativeProject({
|
|
|
|
|
name: config.name,
|
|
|
|
|
root,
|
|
|
|
|
docroot: config.docroot,
|
|
|
|
|
ports: config.nativePorts
|
|
|
|
|
})
|
|
|
|
|
command = join(runtimeRoot(), 'bin', mode === 'dump' ? 'mariadb-dump' : 'mariadb')
|
|
|
|
|
args = [
|
|
|
|
|
'--host=127.0.0.1',
|
|
|
|
|
`--port=${config.nativePorts.database}`,
|
|
|
|
|
'--user=db',
|
|
|
|
|
'--password=db',
|
|
|
|
|
...(mode === 'dump' ? ['--single-transaction', '--routines', '--triggers'] : []),
|
|
|
|
|
'db'
|
|
|
|
|
]
|
|
|
|
|
} else {
|
|
|
|
|
const db = dbCommand(config.database, mode)
|
|
|
|
|
command = 'docker'
|
|
|
|
|
args = ['compose', '-f', composeFile(root), 'exec', '-T', 'db', db.command, ...db.args]
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
return new Promise((resolve, reject) => {
|
2026-08-22 04:17:22 -05:00
|
|
|
const child = spawn(command, args, {
|
|
|
|
|
cwd: root,
|
|
|
|
|
env: AURORA_ENV,
|
|
|
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
2026-08-13 14:32:54 -05:00
|
|
|
})
|
|
|
|
|
let stderr = ''
|
2026-08-22 04:17:22 -05:00
|
|
|
child.stderr.on('data', (chunk) => {
|
|
|
|
|
stderr += chunk.toString()
|
|
|
|
|
})
|
2026-08-13 14:32:54 -05:00
|
|
|
const gz = filePath.endsWith('.gz')
|
|
|
|
|
let outputFinished = Promise.resolve()
|
|
|
|
|
if (mode === 'dump') {
|
|
|
|
|
const output = createWriteStream(filePath)
|
|
|
|
|
const source = gz ? child.stdout.pipe(createGzip()) : child.stdout
|
|
|
|
|
source.pipe(output)
|
2026-08-22 04:17:22 -05:00
|
|
|
outputFinished = new Promise<void>((done, fail) => {
|
|
|
|
|
output.on('finish', done)
|
|
|
|
|
output.on('error', fail)
|
|
|
|
|
})
|
2026-08-13 14:32:54 -05:00
|
|
|
} else {
|
|
|
|
|
const input = createReadStream(filePath)
|
|
|
|
|
const source = gz ? input.pipe(createGunzip()) : input
|
|
|
|
|
source.pipe(child.stdin)
|
|
|
|
|
input.on('error', reject)
|
|
|
|
|
}
|
|
|
|
|
child.on('error', reject)
|
|
|
|
|
child.on('close', async (code) => {
|
2026-08-22 04:17:22 -05:00
|
|
|
if (code !== 0)
|
|
|
|
|
return reject(new Error(stderr.trim() || `Database command exited with code ${code}`))
|
|
|
|
|
try {
|
|
|
|
|
await outputFinished
|
|
|
|
|
resolve()
|
|
|
|
|
} catch (error) {
|
|
|
|
|
reject(error)
|
|
|
|
|
}
|
2026-08-13 14:32:54 -05:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function listSnapshots(root: string): Promise<AuroraSnapshot[]> {
|
|
|
|
|
const dir = snapshotsDir(root)
|
|
|
|
|
await mkdir(dir, { recursive: true })
|
|
|
|
|
const files = (await readdir(dir)).filter((name) => name.endsWith('.sql.gz'))
|
2026-08-22 04:17:22 -05:00
|
|
|
const rows = await Promise.all(
|
|
|
|
|
files.map(async (name) => ({
|
|
|
|
|
Name: name.replace(/\.sql\.gz$/, ''),
|
|
|
|
|
Created: (await stat(join(dir, name))).mtime.toISOString()
|
|
|
|
|
}))
|
|
|
|
|
)
|
2026-08-13 14:32:54 -05:00
|
|
|
return rows.sort((a, b) => b.Created.localeCompare(a.Created))
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-22 04:17:22 -05:00
|
|
|
export function registerDatabaseIpc(): void {
|
|
|
|
|
ipcMain.handle('database:listSnapshots', (_event, _name: string, root: string) =>
|
|
|
|
|
listSnapshots(root)
|
|
|
|
|
)
|
|
|
|
|
ipcMain.handle(
|
|
|
|
|
'database:createSnapshot',
|
|
|
|
|
async (_event, _operationId: string, root: string, name?: string) => {
|
|
|
|
|
await mkdir(snapshotsDir(root), { recursive: true })
|
|
|
|
|
await runDatabasePipe(
|
|
|
|
|
root,
|
|
|
|
|
'dump',
|
|
|
|
|
join(snapshotsDir(root), `${safeSnapshotName(name)}.sql.gz`)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
ipcMain.handle(
|
|
|
|
|
'database:restoreSnapshot',
|
|
|
|
|
async (_event, _operationId: string, root: string, name: string) => {
|
|
|
|
|
await runDatabasePipe(
|
|
|
|
|
root,
|
|
|
|
|
'restore',
|
|
|
|
|
join(snapshotsDir(root), `${safeSnapshotName(name)}.sql.gz`)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
ipcMain.handle(
|
|
|
|
|
'database:deleteSnapshot',
|
|
|
|
|
async (_event, _operationId: string, root: string, name: string) =>
|
|
|
|
|
rm(join(snapshotsDir(root), `${safeSnapshotName(name)}.sql.gz`), { force: true })
|
|
|
|
|
)
|
|
|
|
|
ipcMain.handle(
|
|
|
|
|
'database:importFile',
|
|
|
|
|
async (_event, _operationId: string, root: string, filePath: string) => {
|
|
|
|
|
await runDatabasePipe(root, 'restore', filePath)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
ipcMain.handle(
|
|
|
|
|
'database:exportFile',
|
|
|
|
|
async (_event, _operationId: string, root: string, filePath: string) => {
|
|
|
|
|
await runDatabasePipe(root, 'dump', filePath)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
ipcMain.handle('database:pickImportFile', async () => {
|
|
|
|
|
const r = await dialog.showOpenDialog({
|
|
|
|
|
properties: ['openFile'],
|
|
|
|
|
filters: [{ name: 'SQL dumps', extensions: ['sql', 'gz'] }]
|
|
|
|
|
})
|
|
|
|
|
return r.canceled ? null : r.filePaths[0]
|
2026-08-13 14:32:54 -05:00
|
|
|
})
|
2026-08-22 04:17:22 -05:00
|
|
|
ipcMain.handle('database:pickExportPath', async (_e, name: string) => {
|
|
|
|
|
const r = await dialog.showSaveDialog({
|
|
|
|
|
defaultPath: basename(name).endsWith('.gz') ? name : `${name}.sql.gz`,
|
|
|
|
|
filters: [
|
|
|
|
|
{ name: 'Compressed SQL dump', extensions: ['gz'] },
|
|
|
|
|
{ name: 'SQL dump', extensions: ['sql'] }
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
return r.canceled ? null : r.filePath
|
2026-08-13 14:32:54 -05:00
|
|
|
})
|
|
|
|
|
}
|