smoonb 0.0.25 → 0.0.27
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/package.json +1 -1
- package/src/commands/backup.js +76 -125
- package/src/utils/realtime-settings.js +1 -1
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -101,20 +101,16 @@ async function performFullBackup(config, options) {
|
|
|
101
101
|
created_at: new Date().toISOString(),
|
|
102
102
|
project_id: config.supabase.projectId,
|
|
103
103
|
smoonb_version: require('../../package.json').version,
|
|
104
|
-
backup_type: '
|
|
104
|
+
backup_type: 'pg_dumpall_docker_dashboard_compatible',
|
|
105
105
|
docker_version: await getDockerVersion(),
|
|
106
|
+
dashboard_compatible: true,
|
|
106
107
|
components: {}
|
|
107
108
|
};
|
|
108
109
|
|
|
109
|
-
// 1. Backup Database via Docker
|
|
110
|
-
console.log(chalk.blue('\n📊 1/6 - Backup da Database PostgreSQL via Docker...'));
|
|
111
|
-
const
|
|
112
|
-
manifest.components.database =
|
|
113
|
-
success: dbResult.success,
|
|
114
|
-
method: 'docker',
|
|
115
|
-
files: dbResult.files?.length || 0,
|
|
116
|
-
total_size_kb: dbResult.totalSizeKB || '0.0'
|
|
117
|
-
};
|
|
110
|
+
// 1. Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
|
|
111
|
+
console.log(chalk.blue('\n📊 1/6 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
|
|
112
|
+
const databaseResult = await backupDatabase(config.supabase.projectId, backupDir);
|
|
113
|
+
manifest.components.database = databaseResult;
|
|
118
114
|
|
|
119
115
|
// 2. Backup Edge Functions via Docker
|
|
120
116
|
console.log(chalk.blue('\n⚡ 2/6 - Backup das Edge Functions via Docker...'));
|
|
@@ -146,12 +142,21 @@ async function performFullBackup(config, options) {
|
|
|
146
142
|
|
|
147
143
|
console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
|
|
148
144
|
console.log(chalk.blue(`📁 Localização: ${backupDir}`));
|
|
149
|
-
console.log(chalk.green(
|
|
145
|
+
console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
|
|
150
146
|
console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
|
|
151
147
|
console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
|
|
152
148
|
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
|
|
153
149
|
console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
|
|
154
|
-
|
|
150
|
+
// Determinar mensagem correta baseada no método usado
|
|
151
|
+
let realtimeMessage = 'Falharam';
|
|
152
|
+
if (realtimeResult.success) {
|
|
153
|
+
if (options.skipRealtime) {
|
|
154
|
+
realtimeMessage = 'Configurações copiadas do backup anterior';
|
|
155
|
+
} else {
|
|
156
|
+
realtimeMessage = 'Configurações capturadas interativamente';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
|
|
155
160
|
|
|
156
161
|
return { success: true, backupDir, manifest };
|
|
157
162
|
}
|
|
@@ -217,96 +222,72 @@ function showDockerMessagesAndExit(reason) {
|
|
|
217
222
|
process.exit(1);
|
|
218
223
|
}
|
|
219
224
|
|
|
220
|
-
// Backup da database usando Docker
|
|
221
|
-
async function
|
|
225
|
+
// Backup da database usando pg_dumpall via Docker (idêntico ao Supabase Dashboard)
|
|
226
|
+
async function backupDatabase(projectId, backupDir) {
|
|
222
227
|
try {
|
|
223
|
-
console.log(chalk.gray('
|
|
228
|
+
console.log(chalk.gray(' - Criando backup completo via pg_dumpall...'));
|
|
224
229
|
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
let totalSizeKB = 0;
|
|
228
|
-
|
|
229
|
-
// 1. Backup do Schema
|
|
230
|
-
console.log(chalk.gray(' - Exportando schema...'));
|
|
231
|
-
const schemaFile = path.join(backupDir, 'schema.sql');
|
|
230
|
+
const { execSync } = require('child_process');
|
|
231
|
+
const config = await readConfig();
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const schemaValidation = await validateSqlFile(schemaFile);
|
|
237
|
-
if (schemaValidation.valid) {
|
|
238
|
-
files.push({
|
|
239
|
-
filename: 'schema.sql',
|
|
240
|
-
size: schemaValidation.size,
|
|
241
|
-
sizeKB: schemaValidation.sizeKB
|
|
242
|
-
});
|
|
243
|
-
totalSizeKB += parseFloat(schemaValidation.sizeKB);
|
|
244
|
-
console.log(chalk.green(` ✅ Schema exportado: ${schemaValidation.sizeKB} KB`));
|
|
245
|
-
} else {
|
|
246
|
-
console.log(chalk.red(` ❌ Arquivo schema.sql inválido: ${schemaValidation.error}`));
|
|
247
|
-
success = false;
|
|
248
|
-
}
|
|
249
|
-
} catch (error) {
|
|
250
|
-
console.log(chalk.red(` ❌ Erro ao exportar schema: ${error.message}`));
|
|
251
|
-
success = false;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// 2. Backup dos Dados
|
|
255
|
-
console.log(chalk.gray(' - Exportando dados...'));
|
|
256
|
-
const dataFile = path.join(backupDir, 'data.sql');
|
|
233
|
+
// Extrair credenciais da databaseUrl
|
|
234
|
+
const dbUrl = config.supabase.databaseUrl;
|
|
235
|
+
const urlMatch = dbUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
257
236
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const dataValidation = await validateSqlFile(dataFile);
|
|
262
|
-
if (dataValidation.valid) {
|
|
263
|
-
files.push({
|
|
264
|
-
filename: 'data.sql',
|
|
265
|
-
size: dataValidation.size,
|
|
266
|
-
sizeKB: dataValidation.sizeKB
|
|
267
|
-
});
|
|
268
|
-
totalSizeKB += parseFloat(dataValidation.sizeKB);
|
|
269
|
-
console.log(chalk.green(` ✅ Dados exportados: ${dataValidation.sizeKB} KB`));
|
|
270
|
-
} else {
|
|
271
|
-
console.log(chalk.red(` ❌ Arquivo data.sql inválido: ${dataValidation.error}`));
|
|
272
|
-
success = false;
|
|
273
|
-
}
|
|
274
|
-
} catch (error) {
|
|
275
|
-
console.log(chalk.red(` ❌ Erro ao exportar dados: ${error.message}`));
|
|
276
|
-
success = false;
|
|
237
|
+
if (!urlMatch) {
|
|
238
|
+
throw new Error('Database URL inválida');
|
|
277
239
|
}
|
|
278
|
-
|
|
279
|
-
// 3. Backup dos Roles
|
|
280
|
-
console.log(chalk.gray(' - Exportando roles...'));
|
|
281
|
-
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
282
240
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
241
|
+
const [, username, password, host, port, database] = urlMatch;
|
|
242
|
+
|
|
243
|
+
// Gerar nome do arquivo igual ao dashboard
|
|
244
|
+
const now = new Date();
|
|
245
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
246
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
247
|
+
const year = now.getFullYear();
|
|
248
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
249
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
250
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
251
|
+
|
|
252
|
+
const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`;
|
|
253
|
+
|
|
254
|
+
// CORREÇÃO: Usar caminho absoluto igual às Edge Functions
|
|
255
|
+
const backupDirAbs = path.resolve(backupDir);
|
|
256
|
+
|
|
257
|
+
// Comando pg_dumpall via Docker (mesma abordagem das Edge Functions)
|
|
258
|
+
const dockerCmd = [
|
|
259
|
+
'docker run --rm --network host',
|
|
260
|
+
`-v "${backupDirAbs}:/host"`,
|
|
261
|
+
`-e PGPASSWORD="${password}"`,
|
|
262
|
+
'postgres:17 pg_dumpall',
|
|
263
|
+
`-h ${host}`,
|
|
264
|
+
`-p ${port}`,
|
|
265
|
+
`-U ${username}`,
|
|
266
|
+
`-f /host/${fileName}`
|
|
267
|
+
].join(' ');
|
|
268
|
+
|
|
269
|
+
console.log(chalk.gray(` - Executando pg_dumpall via Docker...`));
|
|
270
|
+
execSync(dockerCmd, { stdio: 'pipe' });
|
|
271
|
+
|
|
272
|
+
// Compactar igual ao Supabase Dashboard
|
|
273
|
+
const gzipCmd = [
|
|
274
|
+
'docker run --rm',
|
|
275
|
+
`-v "${backupDirAbs}:/host"`,
|
|
276
|
+
`postgres:17 gzip /host/${fileName}`
|
|
277
|
+
].join(' ');
|
|
278
|
+
|
|
279
|
+
execSync(gzipCmd, { stdio: 'pipe' });
|
|
280
|
+
|
|
281
|
+
const finalFileName = `${fileName}.gz`;
|
|
282
|
+
const stats = fs.statSync(path.join(backupDir, finalFileName));
|
|
283
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
284
|
+
|
|
285
|
+
console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`));
|
|
286
|
+
|
|
287
|
+
return { success: true, size: sizeKB, fileName: finalFileName };
|
|
307
288
|
} catch (error) {
|
|
308
|
-
console.log(chalk.
|
|
309
|
-
return { success: false
|
|
289
|
+
console.log(chalk.yellow(` ⚠️ Erro no backup do database: ${error.message}`));
|
|
290
|
+
return { success: false };
|
|
310
291
|
}
|
|
311
292
|
}
|
|
312
293
|
|
|
@@ -571,33 +552,3 @@ async function backupRealtimeSettings(projectId, backupDir, skipInteractive = fa
|
|
|
571
552
|
}
|
|
572
553
|
}
|
|
573
554
|
|
|
574
|
-
// Validar arquivo SQL
|
|
575
|
-
async function validateSqlFile(filePath) {
|
|
576
|
-
try {
|
|
577
|
-
if (!fs.existsSync(filePath)) {
|
|
578
|
-
return { valid: false, error: 'Arquivo não existe', size: 0, sizeKB: '0.0' };
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const stats = fs.statSync(filePath);
|
|
582
|
-
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
583
|
-
|
|
584
|
-
if (stats.size === 0) {
|
|
585
|
-
return { valid: false, error: 'Arquivo vazio', size: 0, sizeKB: '0.0' };
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
589
|
-
|
|
590
|
-
const sqlKeywords = ['CREATE', 'INSERT', 'COPY', 'ALTER', 'DROP', 'GRANT', 'REVOKE'];
|
|
591
|
-
const hasValidContent = sqlKeywords.some(keyword =>
|
|
592
|
-
content.toUpperCase().includes(keyword)
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
if (!hasValidContent) {
|
|
596
|
-
return { valid: false, error: 'Sem conteúdo SQL válido', size: stats.size, sizeKB };
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
return { valid: true, error: null, size: stats.size, sizeKB };
|
|
600
|
-
} catch (error) {
|
|
601
|
-
return { valid: false, error: error.message, size: 0, sizeKB: '0.0' };
|
|
602
|
-
}
|
|
603
|
-
}
|
|
@@ -17,7 +17,7 @@ async function captureRealtimeSettings(projectId, backupDir, skipInteractive = f
|
|
|
17
17
|
const previousSettings = await getPreviousRealtimeSettings(backupDir);
|
|
18
18
|
|
|
19
19
|
if (skipInteractive && previousSettings) {
|
|
20
|
-
console.log('📋
|
|
20
|
+
console.log('📋 Copiando Realtime Settings do backup anterior...');
|
|
21
21
|
await fs.writeFile(settingsFile, JSON.stringify(previousSettings, null, 2));
|
|
22
22
|
return previousSettings;
|
|
23
23
|
}
|