smoonb 0.0.24 → 0.0.26
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 -117
- package/src/utils/realtime-settings.js +1 -1
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -101,19 +101,21 @@ 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...'));
|
|
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...'));
|
|
111
112
|
const dbResult = await backupDatabaseWithDocker(config.supabase.databaseUrl, backupDir);
|
|
112
113
|
manifest.components.database = {
|
|
113
114
|
success: dbResult.success,
|
|
114
|
-
method: '
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
method: 'pg_dumpall_docker',
|
|
116
|
+
fileName: dbResult.fileName,
|
|
117
|
+
size_kb: dbResult.size,
|
|
118
|
+
dashboard_compatible: true
|
|
117
119
|
};
|
|
118
120
|
|
|
119
121
|
// 2. Backup Edge Functions via Docker
|
|
@@ -146,12 +148,12 @@ async function performFullBackup(config, options) {
|
|
|
146
148
|
|
|
147
149
|
console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
|
|
148
150
|
console.log(chalk.blue(`📁 Localização: ${backupDir}`));
|
|
149
|
-
console.log(chalk.green(
|
|
151
|
+
console.log(chalk.green(`📊 Database: ${dbResult.fileName} (${dbResult.size} KB) - Idêntico ao Dashboard`));
|
|
150
152
|
console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
|
|
151
153
|
console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
|
|
152
154
|
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
|
|
153
155
|
console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
|
|
154
|
-
console.log(chalk.green(`🔄 Realtime: ${realtimeResult.success ? 'Configurações
|
|
156
|
+
console.log(chalk.green(`🔄 Realtime: ${realtimeResult.success ? 'Configurações capturadas interativamente' : 'Falharam'}`));
|
|
155
157
|
|
|
156
158
|
return { success: true, backupDir, manifest };
|
|
157
159
|
}
|
|
@@ -217,95 +219,82 @@ function showDockerMessagesAndExit(reason) {
|
|
|
217
219
|
process.exit(1);
|
|
218
220
|
}
|
|
219
221
|
|
|
220
|
-
// Backup da database usando Docker
|
|
222
|
+
// Backup da database usando pg_dumpall via Docker (idêntico ao Supabase Dashboard)
|
|
221
223
|
async function backupDatabaseWithDocker(databaseUrl, backupDir) {
|
|
222
224
|
try {
|
|
223
|
-
console.log(chalk.gray('🐳
|
|
225
|
+
console.log(chalk.gray('🐳 Criando backup completo via pg_dumpall Docker...'));
|
|
224
226
|
|
|
225
|
-
|
|
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');
|
|
227
|
+
// Extrair credenciais da databaseUrl
|
|
228
|
+
const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
232
229
|
|
|
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;
|
|
230
|
+
if (!urlMatch) {
|
|
231
|
+
throw new Error('Database URL inválida');
|
|
252
232
|
}
|
|
253
|
-
|
|
254
|
-
// 2. Backup dos Dados
|
|
255
|
-
console.log(chalk.gray(' - Exportando dados...'));
|
|
256
|
-
const dataFile = path.join(backupDir, 'data.sql');
|
|
257
233
|
|
|
258
|
-
|
|
259
|
-
await execAsync(`supabase db dump --db-url "${databaseUrl}" --data-only -f "${dataFile}"`);
|
|
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;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// 3. Backup dos Roles
|
|
280
|
-
console.log(chalk.gray(' - Exportando roles...'));
|
|
281
|
-
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
234
|
+
const [, username, password, host, port, database] = urlMatch;
|
|
282
235
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
236
|
+
// Gerar nome do arquivo igual ao dashboard Supabase
|
|
237
|
+
const now = new Date();
|
|
238
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
239
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
240
|
+
const year = now.getFullYear();
|
|
241
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
242
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
243
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
244
|
+
|
|
245
|
+
const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`;
|
|
246
|
+
const filePath = path.join(backupDir, fileName);
|
|
247
|
+
|
|
248
|
+
console.log(chalk.gray(` - Arquivo: ${fileName}`));
|
|
249
|
+
|
|
250
|
+
// Comando pg_dumpall via Docker (idêntico ao dashboard)
|
|
251
|
+
const dockerCmd = [
|
|
252
|
+
'docker run --rm --network host',
|
|
253
|
+
`-v "${backupDir}:/host"`,
|
|
254
|
+
`-e PGPASSWORD="${password}"`,
|
|
255
|
+
'postgres:17 pg_dumpall',
|
|
256
|
+
`-h ${host}`,
|
|
257
|
+
`-p ${port}`,
|
|
258
|
+
`-U ${username}`,
|
|
259
|
+
`-f /host/${fileName}`
|
|
260
|
+
].join(' ');
|
|
261
|
+
|
|
262
|
+
console.log(chalk.gray(' - Executando pg_dumpall via Docker...'));
|
|
263
|
+
await execAsync(dockerCmd, { stdio: 'pipe' });
|
|
264
|
+
|
|
265
|
+
// Compactar igual ao Supabase Dashboard
|
|
266
|
+
console.log(chalk.gray(' - Compactando arquivo...'));
|
|
267
|
+
const gzipCmd = [
|
|
268
|
+
'docker run --rm',
|
|
269
|
+
`-v "${backupDir}:/host"`,
|
|
270
|
+
'postgres:17 gzip /host/' + fileName
|
|
271
|
+
].join(' ');
|
|
272
|
+
|
|
273
|
+
await execAsync(gzipCmd, { stdio: 'pipe' });
|
|
274
|
+
|
|
275
|
+
const finalFileName = `${fileName}.gz`;
|
|
276
|
+
const finalFilePath = path.join(backupDir, finalFileName);
|
|
277
|
+
|
|
278
|
+
// Validar arquivo gerado
|
|
279
|
+
if (!fs.existsSync(finalFilePath)) {
|
|
280
|
+
throw new Error('Arquivo de backup não foi criado');
|
|
302
281
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
282
|
+
|
|
283
|
+
const stats = fs.statSync(finalFilePath);
|
|
284
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
285
|
+
|
|
286
|
+
console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`));
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
success: true,
|
|
290
|
+
fileName: finalFileName,
|
|
291
|
+
size: sizeKB,
|
|
292
|
+
method: 'pg_dumpall_docker',
|
|
293
|
+
dashboard_compatible: true
|
|
294
|
+
};
|
|
306
295
|
|
|
307
296
|
} catch (error) {
|
|
308
|
-
console.log(chalk.red(
|
|
297
|
+
console.log(chalk.red(` ❌ Erro no backup do database: ${error.message}`));
|
|
309
298
|
return { success: false, error: error.message };
|
|
310
299
|
}
|
|
311
300
|
}
|
|
@@ -571,33 +560,3 @@ async function backupRealtimeSettings(projectId, backupDir, skipInteractive = fa
|
|
|
571
560
|
}
|
|
572
561
|
}
|
|
573
562
|
|
|
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
|
-
}
|
|
@@ -161,7 +161,7 @@ async function captureSettingsInteractively(projectId, previousSettings) {
|
|
|
161
161
|
allow_public_access: {
|
|
162
162
|
label: "Allow public access",
|
|
163
163
|
description: "If disabled, only private channels will be allowed",
|
|
164
|
-
value: allowPublicAccess === 'true'
|
|
164
|
+
value: allowPublicAccess === 'true' || allowPublicAccess === true
|
|
165
165
|
},
|
|
166
166
|
database_connection_pool_size: {
|
|
167
167
|
label: "Database connection pool size",
|