smoonb 1.0.11 → 1.0.13
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 +167 -150
- package/README.md +695 -687
- package/README.pt-BR.md +697 -687
- package/npm-shrinkwrap.json +2030 -0
- package/package.json +71 -68
- package/src/commands/backup/index.js +39 -21
- package/src/commands/backup/outcome.js +16 -0
- package/src/commands/backup/steps/00-license.js +427 -427
- package/src/commands/backup/steps/02-database.js +6 -6
- package/src/commands/backup/steps/03-database-separated.js +129 -161
- package/src/commands/backup/steps/04-database-settings.js +24 -40
- package/src/commands/backup/steps/07-storage.js +11 -10
- package/src/commands/backup/steps/08-custom-roles.js +5 -55
- package/src/commands/backup/steps/10-supabase-temp.js +0 -9
- package/src/commands/backup/steps/11-migrations.js +30 -74
- package/src/i18n/locales/en.json +825 -824
- package/src/i18n/locales/pt-BR.json +824 -823
- package/src/interactive/envMapper.js +6 -12
- package/src/utils/docker.js +6 -53
- package/src/utils/pg-dump-diagnostics.js +68 -64
- package/src/utils/supabaseCli.js +26 -32
|
@@ -47,13 +47,13 @@ async function estimateClusterBytes({ postgresImage, username, password, host, p
|
|
|
47
47
|
const query = "SELECT COALESCE(sum(pg_database_size(datname)),0) FROM pg_database WHERE datistemplate = false;";
|
|
48
48
|
const args = [
|
|
49
49
|
'run', '--rm', '--network', 'host',
|
|
50
|
-
'-e',
|
|
50
|
+
'-e', 'PGPASSWORD',
|
|
51
51
|
postgresImage, 'psql',
|
|
52
52
|
'-h', host, '-p', port, '-U', username, '-d', 'postgres',
|
|
53
53
|
'-t', '-A', '-c', query
|
|
54
54
|
];
|
|
55
55
|
return new Promise((resolve) => {
|
|
56
|
-
const proc = spawn('docker', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
56
|
+
const proc = spawn('docker', args, { env: { ...process.env, PGPASSWORD: password }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
57
57
|
let stdout = '';
|
|
58
58
|
let done = false;
|
|
59
59
|
const timeout = setTimeout(() => {
|
|
@@ -163,7 +163,7 @@ module.exports = async ({ databaseUrl, backupDir, postgresMajor: contextMajor })
|
|
|
163
163
|
const dockerArgs = [
|
|
164
164
|
'run', '--rm', '--network', 'host',
|
|
165
165
|
'-v', `${backupDirAbs}:/host`,
|
|
166
|
-
'-e',
|
|
166
|
+
'-e', 'PGPASSWORD',
|
|
167
167
|
postgresImage, 'pg_dumpall',
|
|
168
168
|
'-h', host,
|
|
169
169
|
'-p', port,
|
|
@@ -183,10 +183,10 @@ module.exports = async ({ databaseUrl, backupDir, postgresMajor: contextMajor })
|
|
|
183
183
|
let lastProgressLine = '';
|
|
184
184
|
|
|
185
185
|
const runDump = () => new Promise((resolve, reject) => {
|
|
186
|
-
const proc = spawn('docker', dockerArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
186
|
+
const proc = spawn('docker', dockerArgs, { env: { ...process.env, PGPASSWORD: password }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
187
187
|
|
|
188
188
|
proc.stderr.on('data', (chunk) => {
|
|
189
|
-
|
|
189
|
+
void chunk;
|
|
190
190
|
if (useTty && lastProgressLine) {
|
|
191
191
|
readline.cursorTo(process.stdout, 0);
|
|
192
192
|
process.stdout.write(lastProgressLine);
|
|
@@ -302,7 +302,7 @@ module.exports = async ({ databaseUrl, backupDir, postgresMajor: contextMajor })
|
|
|
302
302
|
const runGzip = () => new Promise((resolve, reject) => {
|
|
303
303
|
const proc = spawn('docker', gzipArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
304
304
|
|
|
305
|
-
proc.stderr.on('data', (chunk) =>
|
|
305
|
+
proc.stderr.on('data', (chunk) => void chunk);
|
|
306
306
|
|
|
307
307
|
const pollGzip = async () => {
|
|
308
308
|
if (!(await exists(gzipOutputPath))) return;
|
|
@@ -1,161 +1,129 @@
|
|
|
1
|
-
const chalk = require('chalk');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs').promises;
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
function runWithElapsedTicker(
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
const
|
|
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
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { t } = require('../../../i18n');
|
|
5
|
+
const { createPgDumpDiagnostics, hasReplicaRolePrologue, withoutCliUpgradeNotice } = require('../../../utils/pg-dump-diagnostics');
|
|
6
|
+
|
|
7
|
+
const { isolatedSupabase } = require('../../../utils/supabaseCli');
|
|
8
|
+
async function runWithElapsedTicker(_command, args, env) {
|
|
9
|
+
const result = await isolatedSupabase(args, env, { capture: true });
|
|
10
|
+
let stderr = withoutCliUpgradeNotice(result.stderr || '');
|
|
11
|
+
const secrets = Object.values(env || {}).filter(Boolean);
|
|
12
|
+
const databaseUrl = args[args.indexOf('--db-url') + 1];
|
|
13
|
+
if (databaseUrl) {
|
|
14
|
+
secrets.push(databaseUrl);
|
|
15
|
+
try { secrets.push(decodeURIComponent(new URL(databaseUrl).password)); } catch { /* invalid URLs are handled by the CLI */ }
|
|
16
|
+
}
|
|
17
|
+
for (const secret of secrets.filter(Boolean).sort((a, b) => b.length - a.length)) stderr = stderr.split(secret).join('[redacted]');
|
|
18
|
+
stderr = stderr.replace(/postgres(?:ql)?:\/\/[^\s"']+/gi, '[database URL redacted]');
|
|
19
|
+
const diagnostics = createPgDumpDiagnostics(chunk => process.stderr.write(chunk));
|
|
20
|
+
diagnostics.write(stderr);
|
|
21
|
+
return diagnostics.end();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Etapa 2: Backup Database Separado (SQL files para troubleshooting)
|
|
26
|
+
*/
|
|
27
|
+
function createDatabaseSeparatedStep(runDump = runWithElapsedTicker) {
|
|
28
|
+
return async ({ databaseUrl, backupDir, accessToken }) => {
|
|
29
|
+
try {
|
|
30
|
+
const getT = global.smoonbI18n?.t || t;
|
|
31
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.creating')}`));
|
|
32
|
+
|
|
33
|
+
const dbUrl = databaseUrl;
|
|
34
|
+
const files = [];
|
|
35
|
+
let totalSizeKB = 0;
|
|
36
|
+
const warnings = [];
|
|
37
|
+
const failedFiles = [];
|
|
38
|
+
|
|
39
|
+
// 1. Backup do Schema
|
|
40
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingSchema')}`));
|
|
41
|
+
const schemaFile = path.join(backupDir, 'schema.sql');
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
await runDump(
|
|
45
|
+
'supabase',
|
|
46
|
+
['db', 'dump', '--db-url', dbUrl, '-f', path.resolve(schemaFile)],
|
|
47
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
48
|
+
getT('backup.steps.database.separated.exportingSchema')
|
|
49
|
+
);
|
|
50
|
+
const stats = await fs.stat(schemaFile);
|
|
51
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
52
|
+
files.push({ filename: 'schema.sql', sizeKB });
|
|
53
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
54
|
+
console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
failedFiles.push('schema.sql');
|
|
57
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.schemaError', { message: error.message })}`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2. Backup dos Dados
|
|
61
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingData')}`));
|
|
62
|
+
const dataFile = path.join(backupDir, 'data.sql');
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const diagnostics = await runDump(
|
|
66
|
+
'supabase',
|
|
67
|
+
['db', 'dump', '--db-url', dbUrl, '--data-only', '-f', path.resolve(dataFile)],
|
|
68
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
69
|
+
getT('backup.steps.database.separated.exportingData'),
|
|
70
|
+
true
|
|
71
|
+
);
|
|
72
|
+
const stats = await fs.stat(dataFile);
|
|
73
|
+
if (diagnostics.circularForeignKeys) {
|
|
74
|
+
const replicaRolePrologue = await hasReplicaRolePrologue(dataFile);
|
|
75
|
+
warnings.push({ code: 'CIRCULAR_FOREIGN_KEYS', ...diagnostics, replicaRolePrologue });
|
|
76
|
+
console.log(chalk.cyan(` ℹ️ ${getT('backup.steps.database.separated.circularSummary', { tables: diagnostics.tables.join(', ') })}`));
|
|
77
|
+
console.log(chalk.yellow(` ⚠️ ${getT(replicaRolePrologue
|
|
78
|
+
? 'backup.steps.database.separated.circularProtected'
|
|
79
|
+
: 'backup.steps.database.separated.circularUnprotected')}`));
|
|
80
|
+
}
|
|
81
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
82
|
+
files.push({ filename: 'data.sql', sizeKB });
|
|
83
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
84
|
+
console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
|
|
85
|
+
} catch (error) {
|
|
86
|
+
failedFiles.push('data.sql');
|
|
87
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.dataError', { message: error.message })}`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. Backup dos Roles
|
|
91
|
+
console.log(chalk.white(` - ${getT('backup.steps.database.separated.exportingRoles')}`));
|
|
92
|
+
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await runDump(
|
|
96
|
+
'supabase',
|
|
97
|
+
['db', 'dump', '--db-url', dbUrl, '--role-only', '-f', path.resolve(rolesFile)],
|
|
98
|
+
{ SUPABASE_ACCESS_TOKEN: accessToken || '' },
|
|
99
|
+
getT('backup.steps.database.separated.exportingRoles')
|
|
100
|
+
);
|
|
101
|
+
const stats = await fs.stat(rolesFile);
|
|
102
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
103
|
+
files.push({ filename: 'roles.sql', sizeKB });
|
|
104
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
105
|
+
console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
failedFiles.push('roles.sql');
|
|
108
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.rolesError', { message: error.message })}`));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
success: files.length === 3,
|
|
113
|
+
warnings,
|
|
114
|
+
failedFiles,
|
|
115
|
+
files,
|
|
116
|
+
totalSizeKB: totalSizeKB.toFixed(1)
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const getT = global.smoonbI18n?.t || t;
|
|
121
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.database.separated.error', { message: error.message })}`));
|
|
122
|
+
return { success: false, files: [], warnings: [], failedFiles: ['schema.sql', 'data.sql', 'roles.sql'], totalSizeKB: '0.0' };
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = createDatabaseSeparatedStep();
|
|
128
|
+
module.exports.createDatabaseSeparatedStep = createDatabaseSeparatedStep;
|
|
129
|
+
module.exports.runWithElapsedTicker = runWithElapsedTicker;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const fs = require('fs').promises;
|
|
4
|
-
const {
|
|
4
|
+
const { runSql } = require('../../restore/pg');
|
|
5
5
|
const { t } = require('../../../i18n');
|
|
6
6
|
const { parseDatabaseUrl } = require('../utils');
|
|
7
7
|
|
|
@@ -21,8 +21,8 @@ module.exports = async ({ databaseUrl, projectId, backupDir, postgresMajor }) =>
|
|
|
21
21
|
if (!parsed) {
|
|
22
22
|
throw new Error(getT('error.databaseUrlInvalidSimple'));
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
|
|
25
|
+
|
|
26
26
|
// Gerar nome do arquivo
|
|
27
27
|
const now = new Date();
|
|
28
28
|
const day = String(now.getDate()).padStart(2, '0');
|
|
@@ -31,12 +31,12 @@ module.exports = async ({ databaseUrl, projectId, backupDir, postgresMajor }) =>
|
|
|
31
31
|
const hours = String(now.getHours()).padStart(2, '0');
|
|
32
32
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
33
33
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
const fileName = `database-settings-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.json`;
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
// Usar caminho absoluto igual às outras funções
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
|
|
39
|
+
|
|
40
40
|
// Script SQL para capturar todas as configurações
|
|
41
41
|
const sqlScript = `
|
|
42
42
|
-- Database Extensions and Settings Backup
|
|
@@ -62,10 +62,10 @@ SELECT json_agg(
|
|
|
62
62
|
'description', short_desc
|
|
63
63
|
)
|
|
64
64
|
) as postgres_settings
|
|
65
|
-
FROM pg_settings
|
|
65
|
+
FROM pg_settings
|
|
66
66
|
WHERE name IN (
|
|
67
67
|
'statement_timeout',
|
|
68
|
-
'idle_in_transaction_session_timeout',
|
|
68
|
+
'idle_in_transaction_session_timeout',
|
|
69
69
|
'lock_timeout',
|
|
70
70
|
'shared_buffers',
|
|
71
71
|
'work_mem',
|
|
@@ -75,7 +75,7 @@ WHERE name IN (
|
|
|
75
75
|
'log_statement',
|
|
76
76
|
'log_min_duration_statement',
|
|
77
77
|
'timezone',
|
|
78
|
-
'log_timezone',
|
|
78
|
+
'log_timezone',
|
|
79
79
|
'default_transaction_isolation',
|
|
80
80
|
'default_transaction_read_only',
|
|
81
81
|
'checkpoint_completion_target',
|
|
@@ -91,7 +91,7 @@ SELECT json_agg(
|
|
|
91
91
|
'config', rolconfig
|
|
92
92
|
)
|
|
93
93
|
) as role_configurations
|
|
94
|
-
FROM pg_roles
|
|
94
|
+
FROM pg_roles
|
|
95
95
|
WHERE rolname IN ('anon', 'authenticated', 'authenticator', 'postgres', 'service_role')
|
|
96
96
|
AND rolconfig IS NOT NULL;
|
|
97
97
|
|
|
@@ -102,10 +102,10 @@ SELECT json_agg(
|
|
|
102
102
|
'config', rolconfig
|
|
103
103
|
)
|
|
104
104
|
) as pgaudit_configurations
|
|
105
|
-
FROM pg_roles
|
|
106
|
-
WHERE rolconfig IS NOT NULL
|
|
105
|
+
FROM pg_roles
|
|
106
|
+
WHERE rolconfig IS NOT NULL
|
|
107
107
|
AND EXISTS (
|
|
108
|
-
SELECT 1 FROM unnest(rolconfig) AS config
|
|
108
|
+
SELECT 1 FROM unnest(rolconfig) AS config
|
|
109
109
|
WHERE config LIKE '%pgaudit%'
|
|
110
110
|
);
|
|
111
111
|
`;
|
|
@@ -113,29 +113,14 @@ AND EXISTS (
|
|
|
113
113
|
// Salvar script SQL temporário
|
|
114
114
|
const sqlFile = path.join(backupDir, 'temp_settings.sql');
|
|
115
115
|
await fs.writeFile(sqlFile, sqlScript);
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
// Executar via Docker (mesma major do step Postgres version)
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
`-v "${backupDirAbs}:/host"`,
|
|
122
|
-
`-e PGPASSWORD="${password}"`,
|
|
123
|
-
`${postgresImage} psql`,
|
|
124
|
-
`-h ${host}`,
|
|
125
|
-
`-p ${port}`,
|
|
126
|
-
`-U ${username}`,
|
|
127
|
-
`-d ${database}`,
|
|
128
|
-
'-f /host/temp_settings.sql',
|
|
129
|
-
'-t', // Tuples only
|
|
130
|
-
'-A' // Unaligned output
|
|
131
|
-
].join(' ');
|
|
132
|
-
|
|
133
|
-
console.log(chalk.white(` - ${getT('backup.steps.databaseSettings.executing')}`));
|
|
134
|
-
const output = execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
135
|
-
|
|
118
|
+
|
|
119
|
+
const output = await runSql(databaseUrl, sqlScript, { postgresMajor });
|
|
120
|
+
|
|
136
121
|
// Processar output e criar JSON estruturado
|
|
137
122
|
const lines = output.trim().split('\n').filter(line => line.trim());
|
|
138
|
-
|
|
123
|
+
|
|
139
124
|
const result = {
|
|
140
125
|
database_settings: {
|
|
141
126
|
note: "Configurações específicas do database Supabase capturadas via SQL",
|
|
@@ -161,19 +146,19 @@ AND EXISTS (
|
|
|
161
146
|
}
|
|
162
147
|
}
|
|
163
148
|
};
|
|
164
|
-
|
|
149
|
+
|
|
165
150
|
// Salvar arquivo JSON
|
|
166
151
|
const jsonFile = path.join(backupDir, fileName);
|
|
167
152
|
await fs.writeFile(jsonFile, JSON.stringify(result, null, 2));
|
|
168
|
-
|
|
153
|
+
|
|
169
154
|
// Limpar arquivo temporário
|
|
170
155
|
await fs.unlink(sqlFile);
|
|
171
|
-
|
|
156
|
+
|
|
172
157
|
const stats = await fs.stat(jsonFile);
|
|
173
158
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
174
|
-
|
|
159
|
+
|
|
175
160
|
console.log(chalk.green(` ✅ Database Settings: ${fileName} (${sizeKB} KB)`));
|
|
176
|
-
|
|
161
|
+
|
|
177
162
|
return { success: true, size: sizeKB, fileName: fileName };
|
|
178
163
|
} catch (error) {
|
|
179
164
|
const getT = global.smoonbI18n?.t || t;
|
|
@@ -181,4 +166,3 @@ AND EXISTS (
|
|
|
181
166
|
return { success: false };
|
|
182
167
|
}
|
|
183
168
|
};
|
|
184
|
-
|
|
@@ -88,7 +88,7 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
88
88
|
|
|
89
89
|
if (!supabaseUrl || !supabaseServiceKey) {
|
|
90
90
|
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.credentialsNotAvailable')}`));
|
|
91
|
-
return await backupMetadataOnly(buckets, storageDir, projectId, accessToken);
|
|
91
|
+
return { ...await backupMetadataOnly(buckets, storageDir, projectId, accessToken), success: false, reason: 'storage_files_not_exported' };
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
@@ -106,13 +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 { 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
|
-
|
|
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
|
+
|
|
116
116
|
const bucketInfo = {
|
|
117
117
|
id: bucket.id,
|
|
118
118
|
name: bucket.name,
|
|
@@ -211,6 +211,7 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
211
211
|
objectCount: objects?.length || 0,
|
|
212
212
|
filesDownloaded,
|
|
213
213
|
filesSkipped,
|
|
214
|
+
listingFailed,
|
|
214
215
|
totalFiles: allFiles.length
|
|
215
216
|
});
|
|
216
217
|
|
|
@@ -251,9 +252,9 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
251
252
|
console.log(chalk.white(` ℹ️ ${getT('backup.steps.storage.tempKept', { path: path.relative(process.cwd(), tempStorageDir) })}`));
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
console.log(
|
|
255
|
+
console.log(require('../outcome').storageComplete(processedBuckets, buckets.length) ? 'Storage: exportação concluída.' : 'Storage: exportação incompleta; arquivos disponíveis preservados.');
|
|
255
256
|
return {
|
|
256
|
-
success:
|
|
257
|
+
success: require('../outcome').storageComplete(processedBuckets, buckets.length),
|
|
257
258
|
buckets: processedBuckets,
|
|
258
259
|
zipFile: zipFileName,
|
|
259
260
|
zipSizeMB,
|
|
@@ -1,60 +1,10 @@
|
|
|
1
|
-
const chalk = require('chalk');
|
|
2
1
|
const path = require('path');
|
|
3
2
|
const fs = require('fs').promises;
|
|
4
|
-
const {
|
|
5
|
-
const { t } = require('../../../i18n');
|
|
6
|
-
|
|
7
|
-
function runWithElapsedTicker(command, env, label) {
|
|
8
|
-
return new Promise((resolve, reject) => {
|
|
9
|
-
const start = Date.now();
|
|
10
|
-
const ticker = setInterval(() => {
|
|
11
|
-
const elapsed = Math.floor((Date.now() - start) / 1000);
|
|
12
|
-
process.stdout.write(`\r ⏱ ${label} ${elapsed}s`);
|
|
13
|
-
}, 1000);
|
|
14
|
-
const proc = spawn(command, [], {
|
|
15
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
16
|
-
shell: true,
|
|
17
|
-
env: { ...process.env, ...env }
|
|
18
|
-
});
|
|
19
|
-
proc.stderr.on('data', (chunk) => process.stderr.write(chunk));
|
|
20
|
-
proc.on('close', (code) => {
|
|
21
|
-
clearInterval(ticker);
|
|
22
|
-
process.stdout.write('\r' + ' '.repeat(60) + '\r');
|
|
23
|
-
if (code !== 0) reject(new Error(`Exited with code ${code}`));
|
|
24
|
-
else resolve();
|
|
25
|
-
});
|
|
26
|
-
proc.on('error', reject);
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Etapa 7: Backup Custom Roles via SQL
|
|
32
|
-
*/
|
|
3
|
+
const { isolatedSupabase } = require('../../../utils/supabaseCli');
|
|
33
4
|
module.exports = async ({ databaseUrl, backupDir, accessToken }) => {
|
|
34
5
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const cmd = `supabase db dump --db-url "${databaseUrl}" --role-only -f "${customRolesFile}"`;
|
|
40
|
-
|
|
41
|
-
try {
|
|
42
|
-
await runWithElapsedTicker(cmd, { SUPABASE_ACCESS_TOKEN: accessToken || '' }, getT('backup.steps.roles.exporting'));
|
|
43
|
-
|
|
44
|
-
const stats = await fs.stat(customRolesFile);
|
|
45
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
46
|
-
|
|
47
|
-
console.log(chalk.green(` ✅ Custom Roles exportados via Docker: ${sizeKB} KB`));
|
|
48
|
-
|
|
49
|
-
return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] };
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.roles.exportError', { message: error.message })}`));
|
|
52
|
-
return { success: false, roles: [] };
|
|
53
|
-
}
|
|
54
|
-
} catch (error) {
|
|
55
|
-
const getT = global.smoonbI18n?.t || t;
|
|
56
|
-
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.roles.error', { message: error.message })}`));
|
|
57
|
-
return { success: false, roles: [] };
|
|
58
|
-
}
|
|
6
|
+
const file = path.resolve(backupDir, 'custom-roles.sql');
|
|
7
|
+
await isolatedSupabase(['db', 'dump', '--db-url', databaseUrl, '--role-only', '-f', file], { SUPABASE_ACCESS_TOKEN: accessToken || '' });
|
|
8
|
+
return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB: ((await fs.stat(file)).size / 1024).toFixed(1) }] };
|
|
9
|
+
} catch { return { success: false, roles: [], reason: 'roles_export_failed' }; }
|
|
59
10
|
};
|
|
60
|
-
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { copyDirSafe } = require('../../../utils/fsExtra');
|
|
4
|
-
const { cleanDir } = require('../../../utils/fsExtra');
|
|
5
4
|
const { t } = require('../../../i18n');
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -24,14 +23,6 @@ module.exports = async (context) => {
|
|
|
24
23
|
console.log(chalk.green(` ✅ ${getT('backup.steps.temp.copied', { count: fileCount })}`));
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
// Usar flag de limpeza do contexto (já foi perguntado no início)
|
|
28
|
-
const shouldClean = context?.cleanupFlags?.cleanTemp || false;
|
|
29
|
-
|
|
30
|
-
if (shouldClean) {
|
|
31
|
-
await cleanDir(tempDir);
|
|
32
|
-
console.log(chalk.white(` - ${getT('backup.steps.temp.cleaned')}`));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
26
|
return {
|
|
36
27
|
success: true,
|
|
37
28
|
file_count: fileCount
|