smoonb 1.0.6 → 1.0.8
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/CHANGELOG.md +44 -0
- package/README.md +679 -644
- package/README.pt-BR.md +679 -644
- package/package.json +68 -66
- package/src/commands/backup/index.js +8 -2
- package/src/commands/backup/steps/03-database-separated.js +161 -114
- package/src/commands/backup/steps/06-realtime-settings.js +19 -33
- package/src/commands/backup/steps/07-storage.js +7 -12
- package/src/commands/backup/steps/09-edge-functions.js +76 -202
- package/src/commands/restore/auth-fields.json +237 -0
- package/src/commands/restore/dump.js +284 -0
- package/src/commands/restore/index.js +4 -126
- package/src/commands/restore/management.js +74 -0
- package/src/commands/restore/pg.js +83 -0
- package/src/commands/restore/runner.js +117 -0
- package/src/commands/restore/steps/03-database.js +18 -84
- package/src/commands/restore/steps/04-edge-functions.js +95 -122
- package/src/commands/restore/steps/05-auth-settings.js +26 -54
- package/src/commands/restore/steps/06-storage.js +203 -584
- package/src/commands/restore/steps/07-database-settings.js +87 -69
- package/src/commands/restore/steps/08-realtime-settings.js +61 -53
- package/src/i18n/locales/en.json +800 -796
- package/src/i18n/locales/pt-BR.json +799 -795
- package/src/utils/pg-dump-diagnostics.js +64 -0
- package/src/utils/supabaseCli.js +32 -0
package/package.json
CHANGED
|
@@ -1,66 +1,68 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "smoonb",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
|
-
"preferGlobal": false,
|
|
6
|
-
"preventGlobalInstall": true,
|
|
7
|
-
"main": "index.js",
|
|
8
|
-
"bin": {
|
|
9
|
-
"smoonb": "bin/smoonb.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "
|
|
13
|
-
"start": "node bin/smoonb.js",
|
|
14
|
-
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
-
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
-
"lint": "eslint . --ext .js",
|
|
17
|
-
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
-
"build": "node scripts/build.js",
|
|
19
|
-
"build:check": "node scripts/build.js",
|
|
20
|
-
"prepublishOnly": "
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "smoonb",
|
|
3
|
+
"version": "1.0.8",
|
|
4
|
+
"description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
|
|
5
|
+
"preferGlobal": false,
|
|
6
|
+
"preventGlobalInstall": true,
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"smoonb": "bin/smoonb.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test tests/*.test.js",
|
|
13
|
+
"start": "node bin/smoonb.js",
|
|
14
|
+
"preinstall": "node -e \"if(process.env.npm_config_global) { console.error('\\n❌ SMOONB NÃO DEVE SER INSTALADO GLOBALMENTE!\\n\\n📋 Para usar o smoonb, instale localmente no seu projeto:\\n npm install smoonb\\n\\n💡 Depois execute com:\\n npx smoonb backup\\n\\n🚫 Instalação global cancelada!\\n'); process.exit(1); }\"",
|
|
15
|
+
"postinstall": "echo '\\n✅ smoonb instalado com sucesso!\\n💡 Execute: npx smoonb backup\\n📖 Documentação: https://github.com/almmello/smoonb\\n'",
|
|
16
|
+
"lint": "eslint . --ext .js",
|
|
17
|
+
"lint:fix": "eslint . --ext .js --fix",
|
|
18
|
+
"build": "node scripts/build.js",
|
|
19
|
+
"build:check": "node scripts/build.js",
|
|
20
|
+
"prepublishOnly": "npm test && npm run lint && npm run build",
|
|
21
|
+
"test:docker": "node tests/integration/circular-restore.js",
|
|
22
|
+
"test:restore:docker": "node tests/integration/verified-restore.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"supabase",
|
|
26
|
+
"backup",
|
|
27
|
+
"migration",
|
|
28
|
+
"postgresql",
|
|
29
|
+
"edge-functions",
|
|
30
|
+
"database",
|
|
31
|
+
"cli",
|
|
32
|
+
"tool"
|
|
33
|
+
],
|
|
34
|
+
"author": "Goalmoon Tecnologia LTDA <https://www.smoonb.com>",
|
|
35
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@supabase/supabase-js": "^2.93.0",
|
|
41
|
+
"adm-zip": "^0.5.16",
|
|
42
|
+
"chalk": "^4.1.2",
|
|
43
|
+
"commander": "^11.1.0",
|
|
44
|
+
"env-paths": "^4.0.0",
|
|
45
|
+
"inquirer": "^8.2.7"
|
|
46
|
+
},
|
|
47
|
+
"type": "commonjs",
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"eslint": "^9.38.0"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/almmello/smoonb.git"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/almmello/smoonb/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://www.smoonb.com",
|
|
59
|
+
"files": [
|
|
60
|
+
"bin/",
|
|
61
|
+
"src/",
|
|
62
|
+
"README.md",
|
|
63
|
+
"README.pt-BR.md",
|
|
64
|
+
"LICENSE",
|
|
65
|
+
"LICENSE.pt-BR.md",
|
|
66
|
+
"CHANGELOG.md"
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -267,7 +267,9 @@ module.exports = async (options) => {
|
|
|
267
267
|
const dbSeparatedResult = await step03DatabaseSeparated(context);
|
|
268
268
|
manifest.components.database_separated = {
|
|
269
269
|
success: dbSeparatedResult.success,
|
|
270
|
-
method: 'supabase-cli',
|
|
270
|
+
method: 'supabase-cli',
|
|
271
|
+
warnings: dbSeparatedResult.warnings || [],
|
|
272
|
+
failed_files: dbSeparatedResult.failedFiles || [],
|
|
271
273
|
files: dbSeparatedResult.files || [],
|
|
272
274
|
total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
|
|
273
275
|
};
|
|
@@ -339,7 +341,11 @@ module.exports = async (options) => {
|
|
|
339
341
|
console.log(chalk.green(`\n🎉 ${getT('backup.complete.title')}`));
|
|
340
342
|
console.log(chalk.blue(`📁 ${getT('backup.complete.location', { path: finalBackupDir })}`));
|
|
341
343
|
console.log(chalk.green(`📊 ${getT('backup.complete.database', { fileName: databaseResult.fileName, size: `${databaseResult.size} KB` })}`));
|
|
342
|
-
|
|
344
|
+
if (dbSeparatedResult.success) {
|
|
345
|
+
console.log(chalk.green(`📊 ${getT('backup.complete.databaseSql', { count: dbSeparatedResult.files?.length || 0, size: `${dbSeparatedResult.totalSizeKB} KB` })}`));
|
|
346
|
+
} else {
|
|
347
|
+
console.log(chalk.yellow(`⚠️ ${getT('backup.steps.database.separated.incomplete', { files: (dbSeparatedResult.failedFiles || []).join(', ') })}`));
|
|
348
|
+
}
|
|
343
349
|
console.log(chalk.green(`🔧 ${getT('backup.complete.databaseSettings', { fileName: databaseSettingsResult.fileName, size: `${databaseSettingsResult.size} KB` })}`));
|
|
344
350
|
|
|
345
351
|
if (flags?.includeAuth && manifest.components.auth_settings) {
|
|
@@ -1,114 +1,161 @@
|
|
|
1
|
-
const chalk = require('chalk');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs').promises;
|
|
4
|
-
const { spawn } = require('child_process');
|
|
5
|
-
const { t } = require('../../../i18n');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const { t } = require('../../../i18n');
|
|
6
|
+
const { createPgDumpDiagnostics, hasReplicaRolePrologue } = require('../../../utils/pg-dump-diagnostics');
|
|
7
|
+
|
|
8
|
+
function runWithElapsedTicker(command, args, env, label, collectWarnings = false) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const start = Date.now();
|
|
11
|
+
const useTty = Boolean(process.stdout.isTTY);
|
|
12
|
+
const clearTicker = () => {
|
|
13
|
+
if (useTty) process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
14
|
+
};
|
|
15
|
+
const diagnostics = createPgDumpDiagnostics(chunk => {
|
|
16
|
+
clearTicker();
|
|
17
|
+
process.stderr.write(chunk);
|
|
18
|
+
});
|
|
19
|
+
const ticker = useTty ? setInterval(() => {
|
|
20
|
+
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
21
|
+
process.stdout.write(`\r ⏱ ${label} ${elapsed}s`);
|
|
22
|
+
}, 1000) : null;
|
|
23
|
+
let settled = false;
|
|
24
|
+
const finish = (error) => {
|
|
25
|
+
if (settled) return;
|
|
26
|
+
settled = true;
|
|
27
|
+
clearInterval(ticker);
|
|
28
|
+
clearTicker();
|
|
29
|
+
const result = diagnostics.end();
|
|
30
|
+
if (error) {
|
|
31
|
+
// A failed export must never hide the diagnostics already collected.
|
|
32
|
+
if (result.messages.length) process.stderr.write(result.messages.join('\n') + '\n');
|
|
33
|
+
reject(error);
|
|
34
|
+
} else resolve(result);
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
const proc = spawn(command, args, {
|
|
38
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
|
+
shell: true,
|
|
40
|
+
env: { ...process.env, ...env }
|
|
41
|
+
});
|
|
42
|
+
// Drain stdout as well, so a verbose CLI cannot fill its pipe and hang.
|
|
43
|
+
proc.stdout.on('data', chunk => { clearTicker(); process.stdout.write(chunk); });
|
|
44
|
+
proc.stderr.on('data', chunk => {
|
|
45
|
+
if (collectWarnings) diagnostics.write(chunk);
|
|
46
|
+
else { clearTicker(); process.stderr.write(chunk); }
|
|
47
|
+
});
|
|
48
|
+
proc.on('close', code => finish(code === 0 ? null : new Error(`Exited with code ${code}`)));
|
|
49
|
+
proc.on('error', finish);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
finish(error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Etapa 2: Backup Database Separado (SQL files para troubleshooting)
|
|
58
|
+
*/
|
|
59
|
+
function createDatabaseSeparatedStep(runDump = runWithElapsedTicker) {
|
|
60
|
+
return async ({ databaseUrl, backupDir, accessToken }) => {
|
|
61
|
+
try {
|
|
62
|
+
const getT = global.smoonbI18n?.t || t;
|
|
63
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.creating')}`));
|
|
64
|
+
|
|
65
|
+
const dbUrl = databaseUrl;
|
|
66
|
+
const files = [];
|
|
67
|
+
let totalSizeKB = 0;
|
|
68
|
+
const warnings = [];
|
|
69
|
+
const failedFiles = [];
|
|
70
|
+
|
|
71
|
+
// 1. Backup do Schema
|
|
72
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingSchema')}`));
|
|
73
|
+
const schemaFile = path.join(backupDir, 'schema.sql');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await runDump(
|
|
77
|
+
`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`,
|
|
78
|
+
[],
|
|
79
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
80
|
+
getT('backup.steps.database.separated.exportingSchema')
|
|
81
|
+
);
|
|
82
|
+
const stats = await fs.stat(schemaFile);
|
|
83
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
84
|
+
files.push({ filename: 'schema.sql', sizeKB });
|
|
85
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
86
|
+
console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
failedFiles.push('schema.sql');
|
|
89
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.schemaError', { message: error.message })}`));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Backup dos Dados
|
|
93
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingData')}`));
|
|
94
|
+
const dataFile = path.join(backupDir, 'data.sql');
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const diagnostics = await runDump(
|
|
98
|
+
`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`,
|
|
99
|
+
[],
|
|
100
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
101
|
+
getT('backup.steps.database.separated.exportingData'),
|
|
102
|
+
true
|
|
103
|
+
);
|
|
104
|
+
const stats = await fs.stat(dataFile);
|
|
105
|
+
if (diagnostics.circularForeignKeys) {
|
|
106
|
+
const replicaRolePrologue = await hasReplicaRolePrologue(dataFile);
|
|
107
|
+
warnings.push({ code: 'CIRCULAR_FOREIGN_KEYS', ...diagnostics, replicaRolePrologue });
|
|
108
|
+
console.log(chalk.cyan(` ℹ️ ${getT('backup.steps.database.separated.circularSummary', { tables: diagnostics.tables.join(', ') })}`));
|
|
109
|
+
console.log(chalk.yellow(` ⚠️ ${getT(replicaRolePrologue
|
|
110
|
+
? 'backup.steps.database.separated.circularProtected'
|
|
111
|
+
: 'backup.steps.database.separated.circularUnprotected')}`));
|
|
112
|
+
}
|
|
113
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
114
|
+
files.push({ filename: 'data.sql', sizeKB });
|
|
115
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
116
|
+
console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
failedFiles.push('data.sql');
|
|
119
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.dataError', { message: error.message })}`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Backup dos Roles
|
|
123
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingRoles')}`));
|
|
124
|
+
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await runDump(
|
|
128
|
+
`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`,
|
|
129
|
+
[],
|
|
130
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
131
|
+
getT('backup.steps.database.separated.exportingRoles')
|
|
132
|
+
);
|
|
133
|
+
const stats = await fs.stat(rolesFile);
|
|
134
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
135
|
+
files.push({ filename: 'roles.sql', sizeKB });
|
|
136
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
137
|
+
console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
|
|
138
|
+
} catch (error) {
|
|
139
|
+
failedFiles.push('roles.sql');
|
|
140
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.rolesError', { message: error.message })}`));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
success: files.length === 3,
|
|
145
|
+
warnings,
|
|
146
|
+
failedFiles,
|
|
147
|
+
files,
|
|
148
|
+
totalSizeKB: totalSizeKB.toFixed(1)
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const getT = global.smoonbI18n?.t || t;
|
|
153
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.error', { message: error.message })}`));
|
|
154
|
+
return { success: false, files: [], warnings: [], failedFiles: ['schema.sql', 'data.sql', 'roles.sql'], totalSizeKB: '0.0' };
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = createDatabaseSeparatedStep();
|
|
160
|
+
module.exports.createDatabaseSeparatedStep = createDatabaseSeparatedStep;
|
|
161
|
+
module.exports.runWithElapsedTicker = runWithElapsedTicker;
|
|
@@ -1,33 +1,19 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const stats = await fs.stat(path.join(backupDir, 'realtime-settings.json'));
|
|
22
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
23
|
-
|
|
24
|
-
console.log(chalk.green(` ✅ Realtime Settings capturadas: ${sizeKB} KB`));
|
|
25
|
-
|
|
26
|
-
return { success: true, settings, source, reusedFrom };
|
|
27
|
-
} catch (error) {
|
|
28
|
-
const getT = global.smoonbI18n?.t || t;
|
|
29
|
-
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.realtime.error', { message: error.message })}`));
|
|
30
|
-
return { success: false };
|
|
31
|
-
}
|
|
32
|
-
};
|
|
33
|
-
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs').promises;
|
|
3
|
+
const { management } = require('../../restore/management');
|
|
4
|
+
module.exports = async ({ projectId, accessToken, backupDir }) => {
|
|
5
|
+
try {
|
|
6
|
+
const settings = await management(
|
|
7
|
+
{ targetProjectId: projectId, targetAccessToken: accessToken },
|
|
8
|
+
'config/realtime',
|
|
9
|
+
);
|
|
10
|
+
const captured_at = new Date().toISOString();
|
|
11
|
+
await fs.writeFile(
|
|
12
|
+
path.join(backupDir, 'realtime-settings.json'),
|
|
13
|
+
JSON.stringify({ project_id: projectId, captured_at, settings }, null, 2),
|
|
14
|
+
);
|
|
15
|
+
return { success: true, settings, source: 'management-api', captured_at };
|
|
16
|
+
} catch {
|
|
17
|
+
return { success: false, reason: 'realtime_api_capture_failed' };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -106,18 +106,13 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
106
106
|
try {
|
|
107
107
|
console.log(chalk.white(` - ${getT('backup.steps.storage.processing', { bucketName: bucket.name })}`));
|
|
108
108
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let objects = [];
|
|
117
|
-
if (objectsResponse.ok) {
|
|
118
|
-
objects = await objectsResponse.json();
|
|
119
|
-
}
|
|
120
|
-
|
|
109
|
+
const { readDump, records } = require('../../restore/dump');
|
|
110
|
+
const dumpFile = (await fs.readdir(backupDir)).find(f => /\.backup(?:\.gz)?$/.test(f));
|
|
111
|
+
if (!dumpFile) throw new Error('Database inventory required for Storage metadata');
|
|
112
|
+
const dump = readDump(path.join(backupDir, dumpFile));
|
|
113
|
+
const objects = records(dump.tables.find(t => t.schema === 'storage' && t.name === 'objects'))
|
|
114
|
+
.filter(o => o.bucket_id === bucket.id).map(o => ({ ...o, metadata: JSON.parse(o.metadata || '{}') }));
|
|
115
|
+
|
|
121
116
|
const bucketInfo = {
|
|
122
117
|
id: bucket.id,
|
|
123
118
|
name: bucket.name,
|