spindb 0.30.6 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/cli/commands/export.ts +183 -1
- package/cli/commands/menu/container-handlers.ts +103 -9
- package/cli/commands/menu/index.ts +16 -5
- package/cli/commands/menu/settings-handlers.ts +5 -1
- package/core/docker-exporter.ts +362 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -386,6 +386,36 @@ Generated files:
|
|
|
386
386
|
- `entrypoint.sh` - Startup script
|
|
387
387
|
- `README.md` - Instructions
|
|
388
388
|
|
|
389
|
+
### Deploying Your Container
|
|
390
|
+
|
|
391
|
+
**SpinDB doesn't require Docker for local development**, but it can repackage your database as a Docker image for deployment to cloud servers, EC2 instances, Kubernetes clusters, or any Docker-compatible environment.
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
# Export your local database to Docker
|
|
395
|
+
spindb export docker mydb -o ./mydb-deploy
|
|
396
|
+
|
|
397
|
+
# Build and run
|
|
398
|
+
cd ./mydb-deploy
|
|
399
|
+
docker compose build --no-cache
|
|
400
|
+
docker compose up -d
|
|
401
|
+
|
|
402
|
+
# Connect from host (credentials in .env)
|
|
403
|
+
source .env
|
|
404
|
+
psql "postgresql://$SPINDB_USER:$SPINDB_PASSWORD@localhost:$PORT/$DATABASE"
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Schema-only vs Full Data:**
|
|
408
|
+
```bash
|
|
409
|
+
spindb export docker mydb # Include all data (default)
|
|
410
|
+
spindb export docker mydb --no-data # Schema only (empty tables)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
> **Development Tool Notice:** SpinDB is currently a development tool. While Docker exports include TLS encryption and authentication, they are intended for staging and testing—not production workloads. For production databases, consider managed services.
|
|
414
|
+
|
|
415
|
+
**Future Export Options:** Additional export targets are planned for future releases, including direct deployment to managed database services like Neon, Supabase, and PlanetScale.
|
|
416
|
+
|
|
417
|
+
See [DEPLOY.md](DEPLOY.md) for comprehensive deployment documentation.
|
|
418
|
+
|
|
389
419
|
### Container Management
|
|
390
420
|
|
|
391
421
|
```bash
|
|
@@ -1047,6 +1077,8 @@ See [CLAUDE.md](CLAUDE.md) for AI-assisted development context.
|
|
|
1047
1077
|
|
|
1048
1078
|
See [ENGINE_CHECKLIST.md](ENGINE_CHECKLIST.md) for adding new database engines.
|
|
1049
1079
|
|
|
1080
|
+
See [USE_CASES.md](USE_CASES.md) for detailed use cases and infrastructure opportunities.
|
|
1081
|
+
|
|
1050
1082
|
---
|
|
1051
1083
|
|
|
1052
1084
|
## Acknowledgments
|
package/cli/commands/export.ts
CHANGED
|
@@ -5,7 +5,14 @@ import { containerManager } from '../../core/container-manager'
|
|
|
5
5
|
import { processManager } from '../../core/process-manager'
|
|
6
6
|
import { getEngine } from '../../engines'
|
|
7
7
|
import { platformService } from '../../core/platform-service'
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
exportToDocker,
|
|
10
|
+
getExportBackupPath,
|
|
11
|
+
dockerExportExists,
|
|
12
|
+
getDockerConnectionString,
|
|
13
|
+
getDockerCredentials,
|
|
14
|
+
getDefaultDockerExportPath,
|
|
15
|
+
} from '../../core/docker-exporter'
|
|
9
16
|
import { promptContainerSelect, promptConfirm } from '../ui/prompts'
|
|
10
17
|
import { createSpinner } from '../ui/spinner'
|
|
11
18
|
import { uiSuccess, uiError, uiWarning, box, formatBytes } from '../ui/theme'
|
|
@@ -360,3 +367,178 @@ export const exportCommand = new Command('export')
|
|
|
360
367
|
},
|
|
361
368
|
),
|
|
362
369
|
)
|
|
370
|
+
.addCommand(
|
|
371
|
+
new Command('docker-url')
|
|
372
|
+
.description('Get connection string for an existing Docker export')
|
|
373
|
+
.argument('[container]', 'Container name')
|
|
374
|
+
.option('-c, --copy', 'Copy connection string to clipboard')
|
|
375
|
+
.option('-j, --json', 'Output result as JSON')
|
|
376
|
+
.option(
|
|
377
|
+
'--host <hostname>',
|
|
378
|
+
'Override hostname in connection string',
|
|
379
|
+
'localhost',
|
|
380
|
+
)
|
|
381
|
+
.action(
|
|
382
|
+
async (
|
|
383
|
+
containerArg: string | undefined,
|
|
384
|
+
options: {
|
|
385
|
+
copy?: boolean
|
|
386
|
+
json?: boolean
|
|
387
|
+
host?: string
|
|
388
|
+
},
|
|
389
|
+
) => {
|
|
390
|
+
try {
|
|
391
|
+
let containerName = containerArg
|
|
392
|
+
|
|
393
|
+
// Select container if not provided
|
|
394
|
+
if (!containerName) {
|
|
395
|
+
if (options.json) {
|
|
396
|
+
console.log(
|
|
397
|
+
JSON.stringify({ error: 'Container name is required' }),
|
|
398
|
+
)
|
|
399
|
+
process.exit(1)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const containers = await containerManager.list()
|
|
403
|
+
|
|
404
|
+
if (containers.length === 0) {
|
|
405
|
+
console.log(
|
|
406
|
+
uiWarning(
|
|
407
|
+
'No containers found. Create one with: spindb create',
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Filter to containers with Docker exports
|
|
414
|
+
const containersWithExports = containers.filter((c) =>
|
|
415
|
+
dockerExportExists(c.name, c.engine),
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if (containersWithExports.length === 0) {
|
|
419
|
+
console.log(
|
|
420
|
+
uiWarning(
|
|
421
|
+
'No Docker exports found. Export a container first with: spindb export docker <container>',
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const selected = await promptContainerSelect(
|
|
428
|
+
containersWithExports,
|
|
429
|
+
'Select container:',
|
|
430
|
+
)
|
|
431
|
+
if (!selected) return
|
|
432
|
+
containerName = selected
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Get container config
|
|
436
|
+
const config = await containerManager.getConfig(containerName)
|
|
437
|
+
if (!config) {
|
|
438
|
+
if (options.json) {
|
|
439
|
+
console.log(
|
|
440
|
+
JSON.stringify({
|
|
441
|
+
error: `Container "${containerName}" not found`,
|
|
442
|
+
}),
|
|
443
|
+
)
|
|
444
|
+
} else {
|
|
445
|
+
console.error(uiError(`Container "${containerName}" not found`))
|
|
446
|
+
}
|
|
447
|
+
process.exit(1)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check if Docker export exists
|
|
451
|
+
if (!dockerExportExists(containerName, config.engine)) {
|
|
452
|
+
const exportPath = getDefaultDockerExportPath(
|
|
453
|
+
containerName,
|
|
454
|
+
config.engine,
|
|
455
|
+
)
|
|
456
|
+
if (options.json) {
|
|
457
|
+
console.log(
|
|
458
|
+
JSON.stringify({
|
|
459
|
+
error: `No Docker export found for "${containerName}". Export first with: spindb export docker ${containerName}`,
|
|
460
|
+
exportPath,
|
|
461
|
+
}),
|
|
462
|
+
)
|
|
463
|
+
} else {
|
|
464
|
+
console.error(
|
|
465
|
+
uiError(
|
|
466
|
+
`No Docker export found for "${containerName}".\nExport first with: spindb export docker ${containerName}`,
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
process.exit(1)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Get credentials and connection string
|
|
474
|
+
const credentials = await getDockerCredentials(
|
|
475
|
+
containerName,
|
|
476
|
+
config.engine,
|
|
477
|
+
)
|
|
478
|
+
const connectionString = await getDockerConnectionString(
|
|
479
|
+
containerName,
|
|
480
|
+
config.engine,
|
|
481
|
+
{ host: options.host },
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if (!connectionString || !credentials) {
|
|
485
|
+
if (options.json) {
|
|
486
|
+
console.log(
|
|
487
|
+
JSON.stringify({
|
|
488
|
+
error: 'Could not read Docker export credentials',
|
|
489
|
+
}),
|
|
490
|
+
)
|
|
491
|
+
} else {
|
|
492
|
+
console.error(
|
|
493
|
+
uiError('Could not read Docker export credentials'),
|
|
494
|
+
)
|
|
495
|
+
}
|
|
496
|
+
process.exit(1)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Copy to clipboard if requested
|
|
500
|
+
if (options.copy) {
|
|
501
|
+
const copied =
|
|
502
|
+
await platformService.copyToClipboard(connectionString)
|
|
503
|
+
if (!options.json) {
|
|
504
|
+
if (copied) {
|
|
505
|
+
console.log(
|
|
506
|
+
uiSuccess('Connection string copied to clipboard'),
|
|
507
|
+
)
|
|
508
|
+
} else {
|
|
509
|
+
console.log(uiWarning('Could not copy to clipboard'))
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Output
|
|
515
|
+
if (options.json) {
|
|
516
|
+
console.log(
|
|
517
|
+
JSON.stringify({
|
|
518
|
+
connectionString,
|
|
519
|
+
username: credentials.username,
|
|
520
|
+
password: credentials.password,
|
|
521
|
+
host: options.host || 'localhost',
|
|
522
|
+
port: credentials.port,
|
|
523
|
+
database: credentials.database,
|
|
524
|
+
engine: credentials.engine,
|
|
525
|
+
version: credentials.version,
|
|
526
|
+
}),
|
|
527
|
+
)
|
|
528
|
+
} else if (!options.copy) {
|
|
529
|
+
// Only print connection string if not copying (to allow piping)
|
|
530
|
+
console.log(connectionString)
|
|
531
|
+
}
|
|
532
|
+
} catch (error) {
|
|
533
|
+
const e = error as Error
|
|
534
|
+
|
|
535
|
+
if (options.json) {
|
|
536
|
+
console.log(JSON.stringify({ error: e.message }))
|
|
537
|
+
} else {
|
|
538
|
+
console.error(uiError(e.message))
|
|
539
|
+
}
|
|
540
|
+
process.exit(1)
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
),
|
|
544
|
+
)
|
|
@@ -60,6 +60,8 @@ import {
|
|
|
60
60
|
import {
|
|
61
61
|
exportToDocker,
|
|
62
62
|
getExportBackupPath,
|
|
63
|
+
dockerExportExists,
|
|
64
|
+
getDockerConnectionString,
|
|
63
65
|
} from '../../../core/docker-exporter'
|
|
64
66
|
import { getDefaultFormat } from '../../../config/backup-formats'
|
|
65
67
|
import { Engine, isFileBasedEngine } from '../../../types'
|
|
@@ -573,7 +575,11 @@ export async function handleList(
|
|
|
573
575
|
const allChoices: (FilterableChoice | inquirer.Separator)[] = [
|
|
574
576
|
// Show toggle hint at top when server-based containers exist
|
|
575
577
|
...(hasServerContainers
|
|
576
|
-
? [
|
|
578
|
+
? [
|
|
579
|
+
new inquirer.Separator(
|
|
580
|
+
chalk.cyan('── [Shift+Tab] toggle start/stop ──'),
|
|
581
|
+
),
|
|
582
|
+
]
|
|
577
583
|
: []),
|
|
578
584
|
...containerChoices,
|
|
579
585
|
new inquirer.Separator(),
|
|
@@ -820,8 +826,8 @@ export async function showContainerSubmenu(
|
|
|
820
826
|
// Copy connection string - requires database selection for multi-db containers
|
|
821
827
|
actionChoices.push(
|
|
822
828
|
canDoDbAction
|
|
823
|
-
? { name: `${chalk.
|
|
824
|
-
: disabledItem('
|
|
829
|
+
? { name: `${chalk.green('⎘')} Copy connection string`, value: 'copy' }
|
|
830
|
+
: disabledItem('⎘', 'Copy connection string'),
|
|
825
831
|
)
|
|
826
832
|
|
|
827
833
|
// Backup - requires database selection for multi-db containers
|
|
@@ -1746,30 +1752,85 @@ async function handleDelete(containerName: string): Promise<void> {
|
|
|
1746
1752
|
deleteSpinner.succeed(`Container "${containerName}" deleted`)
|
|
1747
1753
|
}
|
|
1748
1754
|
|
|
1755
|
+
async function isDockerContainerRunning(
|
|
1756
|
+
containerName: string,
|
|
1757
|
+
): Promise<boolean> {
|
|
1758
|
+
try {
|
|
1759
|
+
const { execSync } = await import('child_process')
|
|
1760
|
+
const result = execSync(
|
|
1761
|
+
`docker ps --filter "name=spindb-${containerName}" --format "{{.Names}}"`,
|
|
1762
|
+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
|
|
1763
|
+
)
|
|
1764
|
+
return result.trim().includes(`spindb-${containerName}`)
|
|
1765
|
+
} catch {
|
|
1766
|
+
return false
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1749
1770
|
async function handleExportSubmenu(
|
|
1750
1771
|
containerName: string,
|
|
1751
1772
|
databases: string[],
|
|
1752
1773
|
showMainMenu: () => Promise<void>,
|
|
1753
1774
|
): Promise<void> {
|
|
1775
|
+
const config = await containerManager.getConfig(containerName)
|
|
1776
|
+
if (!config) {
|
|
1777
|
+
console.log(uiError(`Container "${containerName}" not found`))
|
|
1778
|
+
await pressEnterToContinue()
|
|
1779
|
+
return
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// Check if Docker export already exists
|
|
1783
|
+
const hasDockerExport = dockerExportExists(containerName, config.engine)
|
|
1784
|
+
|
|
1785
|
+
// Check if Docker container is running (only if export exists)
|
|
1786
|
+
let dockerRunning = false
|
|
1787
|
+
if (hasDockerExport) {
|
|
1788
|
+
dockerRunning = await isDockerContainerRunning(containerName)
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1754
1791
|
console.log()
|
|
1755
1792
|
console.log(header('Export'))
|
|
1756
1793
|
console.log()
|
|
1757
1794
|
|
|
1795
|
+
// Build choices based on whether export exists
|
|
1796
|
+
const choices: MenuChoice[] = []
|
|
1797
|
+
|
|
1798
|
+
if (hasDockerExport) {
|
|
1799
|
+
// Export exists: show option to get connection string with running status
|
|
1800
|
+
const runningStatus = dockerRunning
|
|
1801
|
+
? chalk.green('running')
|
|
1802
|
+
: chalk.gray('not running')
|
|
1803
|
+
choices.push({
|
|
1804
|
+
name: `${chalk.green('⎘')} Get Docker connection string ${chalk.gray(`(${runningStatus})`)}`,
|
|
1805
|
+
value: 'docker-url',
|
|
1806
|
+
})
|
|
1807
|
+
choices.push({
|
|
1808
|
+
name: `${chalk.cyan('▣')} Docker ${chalk.gray('(Re-export - invalidates original credentials)')}`,
|
|
1809
|
+
value: 'docker',
|
|
1810
|
+
})
|
|
1811
|
+
} else {
|
|
1812
|
+
// No export: just show Docker option
|
|
1813
|
+
choices.push({ name: `${chalk.cyan('▣')} Docker`, value: 'docker' })
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
choices.push(new inquirer.Separator())
|
|
1817
|
+
choices.push({ name: `${chalk.blue('←')} Back`, value: 'back' })
|
|
1818
|
+
choices.push({ name: `${chalk.blue('⌂')} Back to main menu`, value: 'home' })
|
|
1819
|
+
|
|
1758
1820
|
const { action } = await escapeablePrompt<{ action: string }>([
|
|
1759
1821
|
{
|
|
1760
1822
|
type: 'list',
|
|
1761
1823
|
name: 'action',
|
|
1762
1824
|
message: 'Export format:',
|
|
1763
|
-
choices
|
|
1764
|
-
{ name: `${chalk.cyan('▣')} Docker`, value: 'docker' },
|
|
1765
|
-
new inquirer.Separator(),
|
|
1766
|
-
{ name: `${chalk.blue('←')} Back`, value: 'back' },
|
|
1767
|
-
{ name: `${chalk.blue('⌂')} Back to main menu`, value: 'home' },
|
|
1768
|
-
],
|
|
1825
|
+
choices,
|
|
1769
1826
|
},
|
|
1770
1827
|
])
|
|
1771
1828
|
|
|
1772
1829
|
switch (action) {
|
|
1830
|
+
case 'docker-url':
|
|
1831
|
+
await handleGetDockerConnectionString(containerName, config.engine)
|
|
1832
|
+
await handleExportSubmenu(containerName, databases, showMainMenu)
|
|
1833
|
+
return
|
|
1773
1834
|
case 'docker':
|
|
1774
1835
|
await handleExportDocker(containerName, databases, showMainMenu)
|
|
1775
1836
|
return
|
|
@@ -1782,6 +1843,39 @@ async function handleExportSubmenu(
|
|
|
1782
1843
|
}
|
|
1783
1844
|
}
|
|
1784
1845
|
|
|
1846
|
+
async function handleGetDockerConnectionString(
|
|
1847
|
+
containerName: string,
|
|
1848
|
+
engine: Engine,
|
|
1849
|
+
): Promise<void> {
|
|
1850
|
+
const connectionString = await getDockerConnectionString(
|
|
1851
|
+
containerName,
|
|
1852
|
+
engine,
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
if (!connectionString) {
|
|
1856
|
+
console.log()
|
|
1857
|
+
console.log(uiError('Could not read Docker export credentials'))
|
|
1858
|
+
await pressEnterToContinue()
|
|
1859
|
+
return
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Copy to clipboard
|
|
1863
|
+
const copied = await platformService.copyToClipboard(connectionString)
|
|
1864
|
+
|
|
1865
|
+
console.log()
|
|
1866
|
+
if (copied) {
|
|
1867
|
+
console.log(uiSuccess('Connection string copied to clipboard'))
|
|
1868
|
+
} else {
|
|
1869
|
+
console.log(uiWarning('Could not copy to clipboard'))
|
|
1870
|
+
}
|
|
1871
|
+
console.log()
|
|
1872
|
+
console.log(chalk.gray(' Connection string:'))
|
|
1873
|
+
console.log(chalk.cyan(` ${connectionString}`))
|
|
1874
|
+
console.log()
|
|
1875
|
+
|
|
1876
|
+
await pressEnterToContinue()
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1785
1879
|
async function handleExportDocker(
|
|
1786
1880
|
containerName: string,
|
|
1787
1881
|
databases: string[],
|
|
@@ -2,7 +2,10 @@ import { Command } from 'commander'
|
|
|
2
2
|
import chalk from 'chalk'
|
|
3
3
|
import inquirer from 'inquirer'
|
|
4
4
|
import { containerManager } from '../../../core/container-manager'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
updateManager,
|
|
7
|
+
type UpdateCheckResult,
|
|
8
|
+
} from '../../../core/update-manager'
|
|
6
9
|
import {
|
|
7
10
|
promptInstallDependencies,
|
|
8
11
|
enableGlobalEscape,
|
|
@@ -158,9 +161,13 @@ async function handleUpdate(): Promise<void> {
|
|
|
158
161
|
return
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
console.log(chalk.gray(` Current version: ${cachedUpdateResult.currentVersion}`))
|
|
162
164
|
console.log(
|
|
163
|
-
chalk.gray(`
|
|
165
|
+
chalk.gray(` Current version: ${cachedUpdateResult.currentVersion}`),
|
|
166
|
+
)
|
|
167
|
+
console.log(
|
|
168
|
+
chalk.gray(
|
|
169
|
+
` Latest version: ${chalk.green(cachedUpdateResult.latestVersion)}`,
|
|
170
|
+
),
|
|
164
171
|
)
|
|
165
172
|
console.log()
|
|
166
173
|
|
|
@@ -173,7 +180,9 @@ async function handleUpdate(): Promise<void> {
|
|
|
173
180
|
spinner.succeed('Update complete')
|
|
174
181
|
console.log()
|
|
175
182
|
console.log(
|
|
176
|
-
uiSuccess(
|
|
183
|
+
uiSuccess(
|
|
184
|
+
`Updated from ${result.previousVersion} to ${result.newVersion}`,
|
|
185
|
+
),
|
|
177
186
|
)
|
|
178
187
|
console.log()
|
|
179
188
|
if (result.previousVersion !== result.newVersion) {
|
|
@@ -189,7 +198,9 @@ async function handleUpdate(): Promise<void> {
|
|
|
189
198
|
console.log(uiError(result.error || 'Unknown error'))
|
|
190
199
|
console.log()
|
|
191
200
|
const pm = await updateManager.detectPackageManager()
|
|
192
|
-
console.log(
|
|
201
|
+
console.log(
|
|
202
|
+
chalk.gray(` Manual update: ${updateManager.getInstallCommand(pm)}`),
|
|
203
|
+
)
|
|
193
204
|
}
|
|
194
205
|
|
|
195
206
|
await pressEnterToContinue()
|
|
@@ -4,7 +4,11 @@ import { configManager } from '../../../core/config-manager'
|
|
|
4
4
|
import { updateManager } from '../../../core/update-manager'
|
|
5
5
|
import { escapeablePrompt } from '../../ui/prompts'
|
|
6
6
|
import { header, uiSuccess, uiInfo } from '../../ui/theme'
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
setCachedIconMode,
|
|
9
|
+
ENGINE_BRAND_COLORS,
|
|
10
|
+
getPageSize,
|
|
11
|
+
} from '../../constants'
|
|
8
12
|
import { hasAnyInstalledEngines } from '../../helpers'
|
|
9
13
|
import { Engine, type IconMode } from '../../../types'
|
|
10
14
|
import { type MenuChoice, pressEnterToContinue } from './shared'
|
package/core/docker-exporter.ts
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* using the same hostdb binaries as local development.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { mkdir, writeFile, copyFile, rm, readdir } from 'fs/promises'
|
|
11
|
+
import { mkdir, writeFile, copyFile, rm, readdir, readFile } from 'fs/promises'
|
|
12
12
|
import { join, basename } from 'path'
|
|
13
13
|
import { existsSync } from 'fs'
|
|
14
|
+
import { homedir } from 'os'
|
|
14
15
|
import {
|
|
15
16
|
type ContainerConfig,
|
|
16
17
|
Engine,
|
|
@@ -73,6 +74,71 @@ function getEngineDisplayName(engine: Engine): string {
|
|
|
73
74
|
return displayNames[engine] || engine
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Engine binary configuration for Docker exports
|
|
79
|
+
*
|
|
80
|
+
* Defines primary binaries per engine for PATH setup and documentation.
|
|
81
|
+
* This structure supports future enhancements:
|
|
82
|
+
* - excludedBinaries: binaries to omit from PATH (e.g., internal tools)
|
|
83
|
+
* - renamedBinaries: map of original -> renamed (e.g., for collision avoidance)
|
|
84
|
+
* - priority: for multi-engine containers, which engine's binary wins
|
|
85
|
+
*/
|
|
86
|
+
const _ENGINE_BINARY_CONFIG: Record<
|
|
87
|
+
Engine,
|
|
88
|
+
{
|
|
89
|
+
primaryBinaries: string[]
|
|
90
|
+
}
|
|
91
|
+
> = {
|
|
92
|
+
[Engine.PostgreSQL]: {
|
|
93
|
+
primaryBinaries: ['psql', 'pg_dump', 'pg_restore', 'createdb', 'dropdb'],
|
|
94
|
+
},
|
|
95
|
+
[Engine.MySQL]: {
|
|
96
|
+
primaryBinaries: ['mysql', 'mysqldump', 'mysqladmin'],
|
|
97
|
+
},
|
|
98
|
+
[Engine.MariaDB]: {
|
|
99
|
+
primaryBinaries: ['mariadb', 'mariadb-dump', 'mariadb-admin'],
|
|
100
|
+
},
|
|
101
|
+
[Engine.SQLite]: {
|
|
102
|
+
primaryBinaries: ['sqlite3'],
|
|
103
|
+
},
|
|
104
|
+
[Engine.DuckDB]: {
|
|
105
|
+
primaryBinaries: ['duckdb'],
|
|
106
|
+
},
|
|
107
|
+
[Engine.MongoDB]: {
|
|
108
|
+
primaryBinaries: ['mongosh', 'mongodump', 'mongorestore'],
|
|
109
|
+
},
|
|
110
|
+
[Engine.FerretDB]: {
|
|
111
|
+
primaryBinaries: ['mongosh', 'psql'], // FerretDB uses mongosh + PostgreSQL backend
|
|
112
|
+
},
|
|
113
|
+
[Engine.Redis]: {
|
|
114
|
+
primaryBinaries: ['redis-cli', 'redis-server'],
|
|
115
|
+
},
|
|
116
|
+
[Engine.Valkey]: {
|
|
117
|
+
primaryBinaries: ['valkey-cli', 'valkey-server'],
|
|
118
|
+
},
|
|
119
|
+
[Engine.ClickHouse]: {
|
|
120
|
+
primaryBinaries: ['clickhouse', 'clickhouse-client'],
|
|
121
|
+
},
|
|
122
|
+
[Engine.Qdrant]: {
|
|
123
|
+
primaryBinaries: [], // REST API only, no CLI tools
|
|
124
|
+
},
|
|
125
|
+
[Engine.Meilisearch]: {
|
|
126
|
+
primaryBinaries: [], // REST API only, no CLI tools
|
|
127
|
+
},
|
|
128
|
+
[Engine.CouchDB]: {
|
|
129
|
+
primaryBinaries: [], // REST API only, no CLI tools
|
|
130
|
+
},
|
|
131
|
+
[Engine.CockroachDB]: {
|
|
132
|
+
primaryBinaries: ['cockroach'],
|
|
133
|
+
},
|
|
134
|
+
[Engine.SurrealDB]: {
|
|
135
|
+
primaryBinaries: ['surreal'],
|
|
136
|
+
},
|
|
137
|
+
[Engine.QuestDB]: {
|
|
138
|
+
primaryBinaries: [], // Uses psql from PostgreSQL for connections
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
|
|
76
142
|
/**
|
|
77
143
|
* Get the connection string template for an engine
|
|
78
144
|
* Includes placeholders for credentials and optionally TLS
|
|
@@ -181,6 +247,12 @@ ENV DEBIAN_FRONTEND=noninteractive
|
|
|
181
247
|
|
|
182
248
|
# Install base dependencies
|
|
183
249
|
# libnuma1: Required by PostgreSQL binaries
|
|
250
|
+
# libxml2: XML library required by PostgreSQL
|
|
251
|
+
# libicu70: ICU library required by PostgreSQL (Ubuntu 22.04 ships ICU 70)
|
|
252
|
+
# libaio1: Async I/O library required by MySQL
|
|
253
|
+
# libncurses6: Terminal library required by MariaDB
|
|
254
|
+
# locales: Needed for PostgreSQL locale configuration
|
|
255
|
+
# lsof: Needed by SpinDB's findProcessByPort()
|
|
184
256
|
# gosu: For running commands as non-root user
|
|
185
257
|
RUN apt-get update && apt-get install -y \\
|
|
186
258
|
curl \\
|
|
@@ -188,9 +260,20 @@ RUN apt-get update && apt-get install -y \\
|
|
|
188
260
|
ca-certificates \\
|
|
189
261
|
gnupg \\
|
|
190
262
|
libnuma1 \\
|
|
263
|
+
libxml2 \\
|
|
264
|
+
libicu70 \\
|
|
265
|
+
libaio1 \\
|
|
266
|
+
libncurses6 \\
|
|
267
|
+
locales \\
|
|
268
|
+
lsof \\
|
|
191
269
|
gosu \\
|
|
270
|
+
&& locale-gen en_US.UTF-8 \\
|
|
192
271
|
&& rm -rf /var/lib/apt/lists/*
|
|
193
272
|
|
|
273
|
+
# Set locale environment variables
|
|
274
|
+
ENV LANG=en_US.UTF-8
|
|
275
|
+
ENV LC_ALL=en_US.UTF-8
|
|
276
|
+
|
|
194
277
|
# Install Node.js 22 LTS (matches SpinDB's engine requirements)
|
|
195
278
|
RUN mkdir -p /etc/apt/keyrings \\
|
|
196
279
|
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \\
|
|
@@ -286,7 +369,7 @@ fi
|
|
|
286
369
|
case Engine.PostgreSQL:
|
|
287
370
|
userCreationCommands = `
|
|
288
371
|
# Create user with password
|
|
289
|
-
echo "Creating database user..."
|
|
372
|
+
echo "[$(date '+%H:%M:%S')] Creating database user '$SPINDB_USER'..."
|
|
290
373
|
cat > /tmp/create-user.sql <<EOSQL
|
|
291
374
|
DO \\$\\$
|
|
292
375
|
BEGIN
|
|
@@ -299,8 +382,13 @@ END
|
|
|
299
382
|
\\$\\$;
|
|
300
383
|
GRANT ALL PRIVILEGES ON DATABASE "$DATABASE" TO "$SPINDB_USER";
|
|
301
384
|
EOSQL
|
|
302
|
-
run_as_spindb spindb run "$CONTAINER_NAME" /tmp/create-user.sql --database postgres
|
|
385
|
+
if ! run_as_spindb spindb run "$CONTAINER_NAME" /tmp/create-user.sql --database postgres; then
|
|
386
|
+
echo "[$(date '+%H:%M:%S')] ERROR: Failed to create database user"
|
|
387
|
+
rm -f /tmp/create-user.sql
|
|
388
|
+
exit 1
|
|
389
|
+
fi
|
|
303
390
|
rm -f /tmp/create-user.sql
|
|
391
|
+
echo "[$(date '+%H:%M:%S')] User '$SPINDB_USER' created successfully"
|
|
304
392
|
`
|
|
305
393
|
break
|
|
306
394
|
|
|
@@ -431,27 +519,83 @@ fi
|
|
|
431
519
|
: databases.length > 1
|
|
432
520
|
? `
|
|
433
521
|
# Restore data for all databases
|
|
522
|
+
echo "[$(date '+%H:%M:%S')] Checking for backup files in ${initDir}..."
|
|
523
|
+
ls -la ${initDir}/ 2>/dev/null || echo " (directory empty or not found)"
|
|
434
524
|
DATABASES="${databases.join(' ')}"
|
|
435
525
|
for DB in $DATABASES; do
|
|
436
526
|
# Find backup file for this database (pattern: containerName-dbName.*)
|
|
437
527
|
BACKUP_FILE=$(ls ${initDir}/${containerName}-$DB.* 2>/dev/null | head -1)
|
|
438
528
|
if [ -n "$BACKUP_FILE" ]; then
|
|
439
|
-
echo "
|
|
529
|
+
echo "[$(date '+%H:%M:%S')] Found backup for database '$DB': $BACKUP_FILE"
|
|
440
530
|
# Add database to tracking if not already tracked
|
|
441
531
|
run_as_spindb spindb databases add "$CONTAINER_NAME" "$DB" 2>/dev/null || true
|
|
442
|
-
run_as_spindb spindb restore "$CONTAINER_NAME" "$BACKUP_FILE" --database "$DB" --force
|
|
532
|
+
if ! run_as_spindb spindb restore "$CONTAINER_NAME" "$BACKUP_FILE" --database "$DB" --force; then
|
|
533
|
+
echo "[$(date '+%H:%M:%S')] ERROR: Restore of '$DB' failed"
|
|
534
|
+
exit 1
|
|
535
|
+
fi
|
|
536
|
+
echo "[$(date '+%H:%M:%S')] Restore of '$DB' completed successfully"
|
|
537
|
+
else
|
|
538
|
+
echo "[$(date '+%H:%M:%S')] WARNING: No backup file found for database '$DB'"
|
|
443
539
|
fi
|
|
444
540
|
done
|
|
445
541
|
`
|
|
446
542
|
: `
|
|
447
543
|
# Restore data if backup exists
|
|
544
|
+
echo "[$(date '+%H:%M:%S')] Checking for backup files in ${initDir}..."
|
|
545
|
+
ls -la ${initDir}/ 2>/dev/null || echo " (directory empty or not found)"
|
|
448
546
|
if ls ${initDir}/* 1> /dev/null 2>&1; then
|
|
449
|
-
echo "Restoring data from backup..."
|
|
450
547
|
BACKUP_FILE=$(ls ${initDir}/* | head -1)
|
|
451
|
-
|
|
548
|
+
echo "[$(date '+%H:%M:%S')] Found backup file: $BACKUP_FILE"
|
|
549
|
+
echo "[$(date '+%H:%M:%S')] Restoring to database '$DATABASE'..."
|
|
550
|
+
if ! run_as_spindb spindb restore "$CONTAINER_NAME" "$BACKUP_FILE" --database "$DATABASE" --force; then
|
|
551
|
+
echo "[$(date '+%H:%M:%S')] ERROR: Restore failed with exit code $?"
|
|
552
|
+
exit 1
|
|
553
|
+
fi
|
|
554
|
+
echo "[$(date '+%H:%M:%S')] Restore completed successfully"
|
|
555
|
+
else
|
|
556
|
+
echo "[$(date '+%H:%M:%S')] WARNING: No backup files found in ${initDir}"
|
|
452
557
|
fi
|
|
453
558
|
`
|
|
454
559
|
|
|
560
|
+
// Post-restore commands - grant table/sequence permissions to the spindb user
|
|
561
|
+
// Tables created during restore are owned by postgres, so spindb user needs grants
|
|
562
|
+
let postRestoreCommands = ''
|
|
563
|
+
|
|
564
|
+
switch (engine) {
|
|
565
|
+
case Engine.PostgreSQL:
|
|
566
|
+
case Engine.CockroachDB:
|
|
567
|
+
postRestoreCommands = `
|
|
568
|
+
# Grant table and sequence permissions to spindb user
|
|
569
|
+
# (Tables from restore are owned by postgres, spindb user needs access)
|
|
570
|
+
echo "[$(date '+%H:%M:%S')] Granting table permissions to '$SPINDB_USER'..."
|
|
571
|
+
cat > /tmp/grant-permissions.sql <<EOSQL
|
|
572
|
+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "$SPINDB_USER";
|
|
573
|
+
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO "$SPINDB_USER";
|
|
574
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO "$SPINDB_USER";
|
|
575
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO "$SPINDB_USER";
|
|
576
|
+
EOSQL
|
|
577
|
+
if ! run_as_spindb spindb run "$CONTAINER_NAME" /tmp/grant-permissions.sql --database "$DATABASE"; then
|
|
578
|
+
echo "[$(date '+%H:%M:%S')] ERROR: Failed to grant permissions"
|
|
579
|
+
rm -f /tmp/grant-permissions.sql
|
|
580
|
+
exit 1
|
|
581
|
+
fi
|
|
582
|
+
rm -f /tmp/grant-permissions.sql
|
|
583
|
+
echo "[$(date '+%H:%M:%S')] Permissions granted successfully"
|
|
584
|
+
`
|
|
585
|
+
break
|
|
586
|
+
|
|
587
|
+
case Engine.MySQL:
|
|
588
|
+
case Engine.MariaDB:
|
|
589
|
+
// MySQL grants are already handled by GRANT ALL ON database.* in user creation
|
|
590
|
+
postRestoreCommands = ''
|
|
591
|
+
break
|
|
592
|
+
|
|
593
|
+
default:
|
|
594
|
+
// Other engines don't need post-restore permission grants
|
|
595
|
+
postRestoreCommands = ''
|
|
596
|
+
break
|
|
597
|
+
}
|
|
598
|
+
|
|
455
599
|
return `#!/bin/bash
|
|
456
600
|
set -e
|
|
457
601
|
|
|
@@ -483,6 +627,8 @@ FILE_DB_PATH="/home/spindb/.spindb/containers/${engine}/\${CONTAINER_NAME}/\${CO
|
|
|
483
627
|
# Export environment variables for the spindb user
|
|
484
628
|
export SPINDB_CONTAINER SPINDB_DATABASE SPINDB_ENGINE SPINDB_VERSION SPINDB_PORT SPINDB_USER SPINDB_PASSWORD
|
|
485
629
|
|
|
630
|
+
# PATH will be updated after spindb downloads engine binaries
|
|
631
|
+
|
|
486
632
|
# Fix permissions on mounted volume (may have been created with root ownership)
|
|
487
633
|
echo "Setting up directories..."
|
|
488
634
|
chown -R spindb:spindb /home/spindb/.spindb 2>/dev/null || true
|
|
@@ -502,16 +648,52 @@ run_as_spindb() {
|
|
|
502
648
|
}
|
|
503
649
|
|
|
504
650
|
# Check if container already exists
|
|
505
|
-
if run_as_spindb spindb list --json 2>/dev/null | grep -q '"name":"'"$CONTAINER_NAME"'"'; then
|
|
506
|
-
echo "Container '$CONTAINER_NAME' already exists"
|
|
651
|
+
if run_as_spindb spindb list --json 2>/dev/null | grep -q '"name": "'"$CONTAINER_NAME"'"'; then
|
|
652
|
+
echo "[$(date '+%H:%M:%S')] Container '$CONTAINER_NAME' already exists"
|
|
653
|
+
${
|
|
654
|
+
isFileBased
|
|
655
|
+
? `# File-based database: no server to start`
|
|
656
|
+
: `# Check if database is running, start if not (handles Docker restart)
|
|
657
|
+
if ! run_as_spindb spindb list --json 2>/dev/null | grep -q '"status":.*"running"'; then
|
|
658
|
+
echo "[$(date '+%H:%M:%S')] Database not running, starting..."
|
|
659
|
+
if ! run_as_spindb spindb start "$CONTAINER_NAME"; then
|
|
660
|
+
echo "[$(date '+%H:%M:%S')] ERROR: Failed to start database"
|
|
661
|
+
exit 1
|
|
662
|
+
fi
|
|
663
|
+
fi`
|
|
664
|
+
}
|
|
507
665
|
else
|
|
508
|
-
echo "Creating container '$CONTAINER_NAME'..."
|
|
666
|
+
echo "[$(date '+%H:%M:%S')] Creating container '$CONTAINER_NAME'..."
|
|
509
667
|
${
|
|
510
668
|
isFileBased
|
|
511
669
|
? `# File-based database: use deterministic path for database file
|
|
512
|
-
run_as_spindb spindb create "$CONTAINER_NAME" --engine "$ENGINE" --db-version "$VERSION" --path "$FILE_DB_PATH" --force
|
|
513
|
-
|
|
670
|
+
if ! run_as_spindb spindb create "$CONTAINER_NAME" --engine "$ENGINE" --db-version "$VERSION" --path "$FILE_DB_PATH" --force; then
|
|
671
|
+
echo "[$(date '+%H:%M:%S')] ERROR: Failed to create container"
|
|
672
|
+
exit 1
|
|
673
|
+
fi`
|
|
674
|
+
: `# Use --start to ensure database is created (non-TTY defaults to no-start)
|
|
675
|
+
if ! run_as_spindb spindb create "$CONTAINER_NAME" --engine "$ENGINE" --db-version "$VERSION" --port "$PORT" --database "$DATABASE" --force --start; then
|
|
676
|
+
echo "[$(date '+%H:%M:%S')] ERROR: Failed to create container"
|
|
677
|
+
exit 1
|
|
678
|
+
fi`
|
|
514
679
|
}
|
|
680
|
+
echo "[$(date '+%H:%M:%S')] Container created successfully"
|
|
681
|
+
fi
|
|
682
|
+
|
|
683
|
+
# Add engine binary directory to PATH (idempotent - only adds if not present)
|
|
684
|
+
# This allows users to run psql, mysql, etc. directly in the container
|
|
685
|
+
# Note: We add the actual bin directory to PATH instead of creating symlinks
|
|
686
|
+
# because some binaries (like psql) are wrapper scripts that use relative paths
|
|
687
|
+
echo "Setting up database binaries in PATH..."
|
|
688
|
+
BIN_DIR=$(ls -d /home/spindb/.spindb/bin/\${ENGINE}-*/bin 2>/dev/null | head -1)
|
|
689
|
+
if [ -d "$BIN_DIR" ]; then
|
|
690
|
+
export PATH="$BIN_DIR:$PATH"
|
|
691
|
+
# Create/overwrite script in /etc/profile.d for system-wide access (idempotent)
|
|
692
|
+
# This ensures PATH is set for login shells without duplication
|
|
693
|
+
echo "export PATH=\\"$BIN_DIR:\\$PATH\\"" > /etc/profile.d/spindb-bins.sh
|
|
694
|
+
echo "Binaries available in PATH: $(ls "$BIN_DIR" | tr '\\n' ' ')"
|
|
695
|
+
else
|
|
696
|
+
echo "Warning: No engine binaries found"
|
|
515
697
|
fi
|
|
516
698
|
${networkConfig}${
|
|
517
699
|
isFileBased
|
|
@@ -519,29 +701,41 @@ ${networkConfig}${
|
|
|
519
701
|
# File-based database: no server to start, just verify file exists after restore
|
|
520
702
|
`
|
|
521
703
|
: `
|
|
522
|
-
#
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
# Wait for database to be ready
|
|
527
|
-
echo "Waiting for database to be ready..."
|
|
704
|
+
# Database was started by 'spindb create --start' above
|
|
705
|
+
# Wait for database to be fully ready for connections
|
|
706
|
+
echo "[$(date '+%H:%M:%S')] Waiting for database to be ready..."
|
|
528
707
|
RETRIES=30
|
|
529
708
|
until run_as_spindb spindb list --json 2>/dev/null | grep -q '"status":.*"running"' || [ $RETRIES -eq 0 ]; do
|
|
530
|
-
echo "Waiting for database... ($RETRIES attempts remaining)"
|
|
709
|
+
echo "[$(date '+%H:%M:%S')] Waiting for database... ($RETRIES attempts remaining)"
|
|
531
710
|
sleep 2
|
|
532
711
|
RETRIES=$((RETRIES-1))
|
|
533
712
|
done
|
|
534
713
|
|
|
535
714
|
if [ $RETRIES -eq 0 ]; then
|
|
536
|
-
echo "
|
|
715
|
+
echo "[$(date '+%H:%M:%S')] ERROR: Database failed to start"
|
|
537
716
|
exit 1
|
|
538
717
|
fi`
|
|
539
718
|
}
|
|
540
719
|
|
|
541
|
-
echo "Database is running!"
|
|
720
|
+
echo "[$(date '+%H:%M:%S')] Database is running!"
|
|
721
|
+
|
|
722
|
+
# Initialization marker file - ensures user creation and data restore only run once
|
|
723
|
+
INIT_MARKER="/home/spindb/.spindb/.initialized-$CONTAINER_NAME"
|
|
724
|
+
if [ ! -f "$INIT_MARKER" ]; then
|
|
725
|
+
echo "[$(date '+%H:%M:%S')] ======== FIRST-TIME INITIALIZATION ========"
|
|
542
726
|
${userCreationCommands}
|
|
543
727
|
${restoreSection}
|
|
728
|
+
${postRestoreCommands}
|
|
729
|
+
# Mark initialization complete
|
|
730
|
+
touch "$INIT_MARKER"
|
|
731
|
+
chown spindb:spindb "$INIT_MARKER"
|
|
732
|
+
echo "[$(date '+%H:%M:%S')] ======== INITIALIZATION COMPLETE ========"
|
|
733
|
+
else
|
|
734
|
+
echo "[$(date '+%H:%M:%S')] Container already initialized, skipping data restore."
|
|
735
|
+
fi
|
|
736
|
+
|
|
544
737
|
echo "========================================"
|
|
738
|
+
echo "SPINDB_READY"
|
|
545
739
|
echo "SpinDB container ready!"
|
|
546
740
|
echo ""
|
|
547
741
|
echo "Connection: ${getConnectionStringTemplate(engine, port, database, useTLS).replace(/\$/g, '\\$')}"
|
|
@@ -948,3 +1142,150 @@ export function getExportBackupPath(
|
|
|
948
1142
|
const extension = getBackupExtension(engine, format)
|
|
949
1143
|
return join(outputDir, 'data', `${containerName}-${database}${extension}`)
|
|
950
1144
|
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* Get the default Docker export directory for a container
|
|
1148
|
+
*/
|
|
1149
|
+
export function getDefaultDockerExportPath(
|
|
1150
|
+
containerName: string,
|
|
1151
|
+
engine: Engine,
|
|
1152
|
+
): string {
|
|
1153
|
+
return join(
|
|
1154
|
+
homedir(),
|
|
1155
|
+
'.spindb',
|
|
1156
|
+
'containers',
|
|
1157
|
+
engine,
|
|
1158
|
+
containerName,
|
|
1159
|
+
'docker',
|
|
1160
|
+
)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Check if a Docker export already exists for a container
|
|
1165
|
+
*/
|
|
1166
|
+
export function dockerExportExists(
|
|
1167
|
+
containerName: string,
|
|
1168
|
+
engine: Engine,
|
|
1169
|
+
): boolean {
|
|
1170
|
+
const exportPath = getDefaultDockerExportPath(containerName, engine)
|
|
1171
|
+
const envPath = join(exportPath, '.env')
|
|
1172
|
+
return existsSync(envPath)
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export type DockerCredentials = {
|
|
1176
|
+
username: string
|
|
1177
|
+
password: string
|
|
1178
|
+
port: number
|
|
1179
|
+
database: string
|
|
1180
|
+
engine: string
|
|
1181
|
+
version: string
|
|
1182
|
+
containerName: string
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Read Docker credentials from an existing export's .env file
|
|
1187
|
+
*/
|
|
1188
|
+
export async function getDockerCredentials(
|
|
1189
|
+
containerName: string,
|
|
1190
|
+
engine: Engine,
|
|
1191
|
+
): Promise<DockerCredentials | null> {
|
|
1192
|
+
const exportPath = getDefaultDockerExportPath(containerName, engine)
|
|
1193
|
+
const envPath = join(exportPath, '.env')
|
|
1194
|
+
|
|
1195
|
+
if (!existsSync(envPath)) {
|
|
1196
|
+
return null
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
try {
|
|
1200
|
+
const envContent = await readFile(envPath, 'utf-8')
|
|
1201
|
+
const lines = envContent.split('\n')
|
|
1202
|
+
|
|
1203
|
+
const values: Record<string, string> = {}
|
|
1204
|
+
for (const line of lines) {
|
|
1205
|
+
const match = line.match(/^([A-Z_]+)=(.*)$/)
|
|
1206
|
+
if (match) {
|
|
1207
|
+
values[match[1]] = match[2]
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return {
|
|
1212
|
+
username: values.SPINDB_USER || 'spindb',
|
|
1213
|
+
password: values.SPINDB_PASSWORD || '',
|
|
1214
|
+
port: parseInt(values.PORT || '0', 10),
|
|
1215
|
+
database: values.DATABASE || '',
|
|
1216
|
+
engine: values.ENGINE || engine,
|
|
1217
|
+
version: values.VERSION || '',
|
|
1218
|
+
containerName: values.CONTAINER_NAME || containerName,
|
|
1219
|
+
}
|
|
1220
|
+
} catch {
|
|
1221
|
+
return null
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Get the Docker connection string for an existing export
|
|
1227
|
+
* Returns the connection string with actual credentials substituted
|
|
1228
|
+
*/
|
|
1229
|
+
export async function getDockerConnectionString(
|
|
1230
|
+
containerName: string,
|
|
1231
|
+
engine: Engine,
|
|
1232
|
+
options: { host?: string } = {},
|
|
1233
|
+
): Promise<string | null> {
|
|
1234
|
+
const credentials = await getDockerCredentials(containerName, engine)
|
|
1235
|
+
if (!credentials) {
|
|
1236
|
+
return null
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const host = options.host || 'localhost'
|
|
1240
|
+
const { username, password, port, database } = credentials
|
|
1241
|
+
|
|
1242
|
+
// URL-encode credentials to escape reserved URI characters
|
|
1243
|
+
const encodedUsername = encodeURIComponent(username)
|
|
1244
|
+
const encodedPassword = encodeURIComponent(password)
|
|
1245
|
+
const encodedDatabase = encodeURIComponent(database)
|
|
1246
|
+
|
|
1247
|
+
// Build connection string based on engine type
|
|
1248
|
+
switch (engine) {
|
|
1249
|
+
case Engine.PostgreSQL:
|
|
1250
|
+
case Engine.CockroachDB:
|
|
1251
|
+
case Engine.QuestDB:
|
|
1252
|
+
return `postgresql://${encodedUsername}:${encodedPassword}@${host}:${port}/${encodedDatabase}`
|
|
1253
|
+
|
|
1254
|
+
case Engine.MySQL:
|
|
1255
|
+
case Engine.MariaDB:
|
|
1256
|
+
return `mysql://${encodedUsername}:${encodedPassword}@${host}:${port}/${encodedDatabase}`
|
|
1257
|
+
|
|
1258
|
+
case Engine.MongoDB:
|
|
1259
|
+
case Engine.FerretDB:
|
|
1260
|
+
return `mongodb://${encodedUsername}:${encodedPassword}@${host}:${port}/${encodedDatabase}`
|
|
1261
|
+
|
|
1262
|
+
case Engine.Redis:
|
|
1263
|
+
case Engine.Valkey:
|
|
1264
|
+
return `redis://:${encodedPassword}@${host}:${port}`
|
|
1265
|
+
|
|
1266
|
+
case Engine.ClickHouse:
|
|
1267
|
+
return `clickhouse://${encodedUsername}:${encodedPassword}@${host}:${port}/${encodedDatabase}`
|
|
1268
|
+
|
|
1269
|
+
case Engine.Qdrant:
|
|
1270
|
+
return `http://${host}:${port}`
|
|
1271
|
+
|
|
1272
|
+
case Engine.Meilisearch:
|
|
1273
|
+
return `http://${host}:${port}`
|
|
1274
|
+
|
|
1275
|
+
case Engine.CouchDB:
|
|
1276
|
+
return `http://${username}:${password}@${host}:${port}/${database}`
|
|
1277
|
+
|
|
1278
|
+
case Engine.SurrealDB:
|
|
1279
|
+
return `ws://${username}:${password}@${host}:${port}`
|
|
1280
|
+
|
|
1281
|
+
case Engine.SQLite:
|
|
1282
|
+
case Engine.DuckDB:
|
|
1283
|
+
return `File-based database (no network connection)`
|
|
1284
|
+
|
|
1285
|
+
default:
|
|
1286
|
+
assertExhaustive(
|
|
1287
|
+
engine,
|
|
1288
|
+
`Unhandled engine in getDockerConnectionString: ${engine}`,
|
|
1289
|
+
)
|
|
1290
|
+
}
|
|
1291
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spindb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"author": "Bob Bass <bob@bbass.co>",
|
|
5
5
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
6
6
|
"description": "Zero-config Docker-free local database containers. Create, backup, and clone a variety of popular databases.",
|