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.
@@ -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', `PGPASSWORD=${password}`,
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', `PGPASSWORD=${password}`,
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
- process.stderr.write(chunk);
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) => process.stderr.write(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 { 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
+ 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 { execSync } = require('child_process');
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
- const { username, password, host, port, database } = parsed;
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
- const backupDirAbs = path.resolve(backupDir);
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
- const postgresImage = `postgres:${postgresMajor}`;
119
- const dockerCmd = [
120
- 'docker run --rm --network host',
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(chalk.green(`✅ ${getT('backup.steps.storage.done', { buckets: processedBuckets.length, files: totalFilesDownloaded })}`));
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: true,
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 { spawn } = require('child_process');
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 getT = global.smoonbI18n?.t || t;
36
- console.log(chalk.white(` - ${getT('backup.steps.roles.exporting')}`));
37
-
38
- const customRolesFile = path.join(backupDir, 'custom-roles.sql');
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