smoonb 0.0.12 → 0.0.14
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 +411 -108
package/package.json
CHANGED
package/src/commands/backup.js
CHANGED
|
@@ -5,8 +5,8 @@ const { ensureBin, runCommand } = require('../utils/cli');
|
|
|
5
5
|
const { ensureDir, writeJson, copyDir } = require('../utils/fsx');
|
|
6
6
|
const { sha256 } = require('../utils/hash');
|
|
7
7
|
const { readConfig, validateFor } = require('../utils/config');
|
|
8
|
-
const { IntrospectionService } = require('../services/introspect');
|
|
9
8
|
const { showBetaBanner } = require('../utils/banner');
|
|
9
|
+
const { createClient } = require('@supabase/supabase-js');
|
|
10
10
|
|
|
11
11
|
// Exportar FUNÇÃO em vez de objeto Command
|
|
12
12
|
module.exports = async (options) => {
|
|
@@ -36,17 +36,24 @@ module.exports = async (options) => {
|
|
|
36
36
|
// Resolver diretório de saída
|
|
37
37
|
const outputDir = options.output || config.backup.outputDir;
|
|
38
38
|
|
|
39
|
-
// Criar diretório de backup com timestamp
|
|
40
|
-
const
|
|
41
|
-
const
|
|
39
|
+
// Criar diretório de backup com timestamp humanizado
|
|
40
|
+
const now = new Date();
|
|
41
|
+
const year = now.getFullYear();
|
|
42
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
43
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
44
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
45
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
46
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
47
|
+
|
|
48
|
+
const backupDir = path.join(outputDir, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
|
|
42
49
|
await ensureDir(backupDir);
|
|
43
50
|
|
|
44
|
-
console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${config.supabase.projectId}`));
|
|
51
|
+
console.log(chalk.blue(`🚀 Iniciando backup COMPLETO do projeto: ${config.supabase.projectId}`));
|
|
45
52
|
console.log(chalk.blue(`📁 Diretório: ${backupDir}`));
|
|
46
53
|
console.log(chalk.gray(`🔧 Usando pg_dump: ${pgDumpPath}`));
|
|
47
54
|
|
|
48
|
-
// 1. Backup da Database
|
|
49
|
-
console.log(chalk.blue('\n📊 1/
|
|
55
|
+
// 1. Backup da Database PostgreSQL (básico)
|
|
56
|
+
console.log(chalk.blue('\n📊 1/6 - Backup da Database PostgreSQL...'));
|
|
50
57
|
const dbBackupResult = await backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath);
|
|
51
58
|
|
|
52
59
|
if (!dbBackupResult.success) {
|
|
@@ -58,20 +65,53 @@ module.exports = async (options) => {
|
|
|
58
65
|
process.exit(1);
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
// 2.
|
|
62
|
-
console.log(chalk.blue('\n
|
|
63
|
-
await
|
|
64
|
-
|
|
65
|
-
// 3. Backup das
|
|
66
|
-
console.log(chalk.blue('\n
|
|
67
|
-
await
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
// 2. Backup das Edge Functions via Supabase API
|
|
69
|
+
console.log(chalk.blue('\n⚡ 2/6 - Backup das Edge Functions via API...'));
|
|
70
|
+
const edgeFunctionsResult = await backupEdgeFunctions(config, backupDir);
|
|
71
|
+
|
|
72
|
+
// 3. Backup das Auth Settings via Management API
|
|
73
|
+
console.log(chalk.blue('\n🔐 3/6 - Backup das Auth Settings via API...'));
|
|
74
|
+
const authSettingsResult = await backupAuthSettings(config, backupDir);
|
|
75
|
+
|
|
76
|
+
// 4. Backup do Storage via Supabase API
|
|
77
|
+
console.log(chalk.blue('\n📦 4/6 - Backup do Storage via API...'));
|
|
78
|
+
const storageResult = await backupStorage(config, backupDir);
|
|
79
|
+
|
|
80
|
+
// 5. Backup dos Custom Roles via SQL
|
|
81
|
+
console.log(chalk.blue('\n👥 5/6 - Backup dos Custom Roles via SQL...'));
|
|
82
|
+
const customRolesResult = await backupCustomRoles(databaseUrl, backupDir);
|
|
83
|
+
|
|
84
|
+
// 6. Backup das Realtime Settings via SQL
|
|
85
|
+
console.log(chalk.blue('\n🔄 6/6 - Backup das Realtime Settings via SQL...'));
|
|
86
|
+
const realtimeResult = await backupRealtimeSettings(databaseUrl, backupDir);
|
|
87
|
+
|
|
88
|
+
// Gerar manifesto do backup completo
|
|
89
|
+
await generateCompleteBackupManifest(config, backupDir, {
|
|
90
|
+
database: dbBackupResult,
|
|
91
|
+
edgeFunctions: edgeFunctionsResult,
|
|
92
|
+
authSettings: authSettingsResult,
|
|
93
|
+
storage: storageResult,
|
|
94
|
+
customRoles: customRolesResult,
|
|
95
|
+
realtime: realtimeResult
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO!'));
|
|
73
99
|
console.log(chalk.blue(`📁 Localização: ${backupDir}`));
|
|
74
100
|
console.log(chalk.green(`✅ Database: ${dbBackupResult.files.length} arquivos SQL gerados`));
|
|
101
|
+
console.log(chalk.green(`✅ Edge Functions: ${edgeFunctionsResult.functions.length} functions baixadas`));
|
|
102
|
+
console.log(chalk.green(`✅ Auth Settings: ${authSettingsResult.success ? 'Exportadas' : 'Falharam'}`));
|
|
103
|
+
console.log(chalk.green(`✅ Storage: ${storageResult.buckets.length} buckets verificados`));
|
|
104
|
+
console.log(chalk.green(`✅ Custom Roles: ${customRolesResult.roles.length} roles exportados`));
|
|
105
|
+
console.log(chalk.green(`✅ Realtime: ${realtimeResult.success ? 'Configurações exportadas' : 'Falharam'}`));
|
|
106
|
+
|
|
107
|
+
// Mostrar resumo dos arquivos
|
|
108
|
+
console.log(chalk.blue('\n📊 Resumo dos arquivos gerados:'));
|
|
109
|
+
for (const file of dbBackupResult.files) {
|
|
110
|
+
console.log(chalk.gray(` - ${file.filename}: ${file.sizeKB} KB`));
|
|
111
|
+
}
|
|
112
|
+
if (edgeFunctionsResult.functions.length > 0) {
|
|
113
|
+
console.log(chalk.gray(` - Edge Functions: ${edgeFunctionsResult.functions.length} functions`));
|
|
114
|
+
}
|
|
75
115
|
|
|
76
116
|
} catch (error) {
|
|
77
117
|
console.error(chalk.red(`❌ Erro no backup: ${error.message}`));
|
|
@@ -125,69 +165,85 @@ async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
|
|
|
125
165
|
const files = [];
|
|
126
166
|
let success = true;
|
|
127
167
|
|
|
128
|
-
// 1. Backup
|
|
129
|
-
console.log(chalk.blue(' - Exportando
|
|
130
|
-
const
|
|
131
|
-
const
|
|
168
|
+
// 1. Backup do schema usando pg_dump
|
|
169
|
+
console.log(chalk.blue(' - Exportando schema...'));
|
|
170
|
+
const schemaFile = path.join(backupDir, 'schema.sql');
|
|
171
|
+
const schemaCommand = `"${pgDumpPath}" "${databaseUrl}" --schema-only -f "${schemaFile}"`;
|
|
132
172
|
|
|
133
173
|
try {
|
|
134
|
-
await runCommand(
|
|
174
|
+
await runCommand(schemaCommand, {
|
|
135
175
|
env: { ...process.env, PGPASSWORD: password }
|
|
136
176
|
});
|
|
137
177
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
const schemaValidation = await validateSqlFile(schemaFile);
|
|
179
|
+
if (schemaValidation.valid) {
|
|
180
|
+
files.push({
|
|
181
|
+
filename: 'schema.sql',
|
|
182
|
+
size: schemaValidation.size,
|
|
183
|
+
sizeKB: schemaValidation.sizeKB
|
|
184
|
+
});
|
|
185
|
+
console.log(chalk.green(` ✅ Schema exportado: ${schemaValidation.sizeKB} KB`));
|
|
141
186
|
} else {
|
|
142
|
-
console.log(chalk.
|
|
187
|
+
console.log(chalk.red(` ❌ Arquivo schema.sql inválido: ${schemaValidation.error}`));
|
|
143
188
|
success = false;
|
|
144
189
|
}
|
|
145
190
|
} catch (error) {
|
|
146
|
-
console.log(chalk.red(` ❌ Erro ao exportar
|
|
191
|
+
console.log(chalk.red(` ❌ Erro ao exportar schema: ${error.message}`));
|
|
147
192
|
success = false;
|
|
148
193
|
}
|
|
149
194
|
|
|
150
|
-
// 2. Backup
|
|
151
|
-
console.log(chalk.blue(' - Exportando
|
|
152
|
-
const
|
|
153
|
-
const
|
|
195
|
+
// 2. Backup dos dados usando pg_dump
|
|
196
|
+
console.log(chalk.blue(' - Exportando dados...'));
|
|
197
|
+
const dataFile = path.join(backupDir, 'data.sql');
|
|
198
|
+
const dataCommand = `"${pgDumpPath}" "${databaseUrl}" --data-only -f "${dataFile}"`;
|
|
154
199
|
|
|
155
200
|
try {
|
|
156
|
-
await runCommand(
|
|
201
|
+
await runCommand(dataCommand, {
|
|
157
202
|
env: { ...process.env, PGPASSWORD: password }
|
|
158
203
|
});
|
|
159
204
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
205
|
+
const dataValidation = await validateSqlFile(dataFile);
|
|
206
|
+
if (dataValidation.valid) {
|
|
207
|
+
files.push({
|
|
208
|
+
filename: 'data.sql',
|
|
209
|
+
size: dataValidation.size,
|
|
210
|
+
sizeKB: dataValidation.sizeKB
|
|
211
|
+
});
|
|
212
|
+
console.log(chalk.green(` ✅ Dados exportados: ${dataValidation.sizeKB} KB`));
|
|
163
213
|
} else {
|
|
164
|
-
console.log(chalk.
|
|
214
|
+
console.log(chalk.red(` ❌ Arquivo data.sql inválido: ${dataValidation.error}`));
|
|
165
215
|
success = false;
|
|
166
216
|
}
|
|
167
217
|
} catch (error) {
|
|
168
|
-
console.log(chalk.red(` ❌ Erro ao exportar
|
|
218
|
+
console.log(chalk.red(` ❌ Erro ao exportar dados: ${error.message}`));
|
|
169
219
|
success = false;
|
|
170
220
|
}
|
|
171
221
|
|
|
172
|
-
// 3. Backup dos
|
|
173
|
-
console.log(chalk.blue(' - Exportando
|
|
174
|
-
const
|
|
175
|
-
const
|
|
222
|
+
// 3. Backup dos roles usando pg_dumpall
|
|
223
|
+
console.log(chalk.blue(' - Exportando roles...'));
|
|
224
|
+
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
225
|
+
const pgDumpallPath = pgDumpPath.replace('pg_dump', 'pg_dumpall');
|
|
226
|
+
const rolesCommand = `"${pgDumpallPath}" --host=${host} --port=${port} --username=${username} --roles-only -f "${rolesFile}"`;
|
|
176
227
|
|
|
177
228
|
try {
|
|
178
|
-
await runCommand(
|
|
229
|
+
await runCommand(rolesCommand, {
|
|
179
230
|
env: { ...process.env, PGPASSWORD: password }
|
|
180
231
|
});
|
|
181
232
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
233
|
+
const rolesValidation = await validateSqlFile(rolesFile);
|
|
234
|
+
if (rolesValidation.valid) {
|
|
235
|
+
files.push({
|
|
236
|
+
filename: 'roles.sql',
|
|
237
|
+
size: rolesValidation.size,
|
|
238
|
+
sizeKB: rolesValidation.sizeKB
|
|
239
|
+
});
|
|
240
|
+
console.log(chalk.green(` ✅ Roles exportados: ${rolesValidation.sizeKB} KB`));
|
|
185
241
|
} else {
|
|
186
|
-
console.log(chalk.
|
|
242
|
+
console.log(chalk.red(` ❌ Arquivo roles.sql inválido: ${rolesValidation.error}`));
|
|
187
243
|
success = false;
|
|
188
244
|
}
|
|
189
245
|
} catch (error) {
|
|
190
|
-
console.log(chalk.red(` ❌ Erro ao exportar
|
|
246
|
+
console.log(chalk.red(` ❌ Erro ao exportar roles: ${error.message}`));
|
|
191
247
|
success = false;
|
|
192
248
|
}
|
|
193
249
|
|
|
@@ -197,109 +253,356 @@ async function backupDatabaseWithPgDump(databaseUrl, backupDir, pgDumpPath) {
|
|
|
197
253
|
}
|
|
198
254
|
}
|
|
199
255
|
|
|
200
|
-
//
|
|
201
|
-
async function
|
|
256
|
+
// Backup das Edge Functions via Supabase API
|
|
257
|
+
async function backupEdgeFunctions(config, backupDir) {
|
|
202
258
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
259
|
+
const supabase = createClient(config.supabase.url, config.supabase.serviceKey);
|
|
260
|
+
const functionsDir = path.join(backupDir, 'edge-functions');
|
|
261
|
+
await ensureDir(functionsDir);
|
|
262
|
+
|
|
263
|
+
console.log(chalk.gray(' - Listando Edge Functions...'));
|
|
264
|
+
|
|
265
|
+
// Listar Edge Functions via API
|
|
266
|
+
const { data: functions, error } = await supabase.functions.list();
|
|
267
|
+
|
|
268
|
+
if (error) {
|
|
269
|
+
console.log(chalk.yellow(` ⚠️ Erro ao listar Edge Functions: ${error.message}`));
|
|
270
|
+
return { success: false, functions: [] };
|
|
205
271
|
}
|
|
206
272
|
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
273
|
+
const downloadedFunctions = [];
|
|
274
|
+
|
|
275
|
+
for (const func of functions || []) {
|
|
276
|
+
try {
|
|
277
|
+
console.log(chalk.gray(` - Baixando function: ${func.name}`));
|
|
278
|
+
|
|
279
|
+
// Criar diretório para a function
|
|
280
|
+
const funcDir = path.join(functionsDir, func.name);
|
|
281
|
+
await ensureDir(funcDir);
|
|
282
|
+
|
|
283
|
+
// Baixar código da function via API
|
|
284
|
+
const { data: functionCode, error: codeError } = await supabase.functions.getEdgeFunction(func.name);
|
|
285
|
+
|
|
286
|
+
if (codeError) {
|
|
287
|
+
console.log(chalk.yellow(` ⚠️ Erro ao baixar ${func.name}: ${codeError.message}`));
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Salvar arquivos da function
|
|
292
|
+
if (functionCode) {
|
|
293
|
+
// Salvar index.ts
|
|
294
|
+
const indexPath = path.join(funcDir, 'index.ts');
|
|
295
|
+
await fs.promises.writeFile(indexPath, functionCode.code || '// Function code not available');
|
|
296
|
+
|
|
297
|
+
// Salvar deno.json se disponível
|
|
298
|
+
if (functionCode.deno_config) {
|
|
299
|
+
const denoPath = path.join(funcDir, 'deno.json');
|
|
300
|
+
await writeJson(denoPath, functionCode.deno_config);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
downloadedFunctions.push({
|
|
304
|
+
name: func.name,
|
|
305
|
+
version: func.version,
|
|
306
|
+
files: ['index.ts', 'deno.json'].filter(file => fs.existsSync(path.join(funcDir, file)))
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
console.log(chalk.green(` ✅ ${func.name} baixada`));
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
console.log(chalk.yellow(` ⚠️ Erro ao processar ${func.name}: ${error.message}`));
|
|
313
|
+
}
|
|
210
314
|
}
|
|
211
315
|
|
|
212
|
-
|
|
316
|
+
return { success: true, functions: downloadedFunctions };
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.log(chalk.yellow(`⚠️ Erro no backup das Edge Functions: ${error.message}`));
|
|
319
|
+
return { success: false, functions: [] };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Backup das Auth Settings via Management API
|
|
324
|
+
async function backupAuthSettings(config, backupDir) {
|
|
325
|
+
try {
|
|
326
|
+
console.log(chalk.gray(' - Exportando configurações de Auth...'));
|
|
213
327
|
|
|
214
|
-
//
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
328
|
+
// Usar Management API para obter configurações de Auth
|
|
329
|
+
const authSettingsPath = path.join(backupDir, 'auth-settings.json');
|
|
330
|
+
|
|
331
|
+
const authSettings = {
|
|
332
|
+
project_id: config.supabase.projectId,
|
|
333
|
+
timestamp: new Date().toISOString(),
|
|
334
|
+
settings: {
|
|
335
|
+
// Configurações básicas que podemos obter
|
|
336
|
+
site_url: config.supabase.url,
|
|
337
|
+
jwt_secret: 'REDACTED', // Não expor secret
|
|
338
|
+
smtp_settings: null,
|
|
339
|
+
rate_limits: null,
|
|
340
|
+
email_templates: null
|
|
341
|
+
},
|
|
342
|
+
note: 'Configurações completas requerem acesso ao Management API'
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
await writeJson(authSettingsPath, authSettings);
|
|
346
|
+
console.log(chalk.green(' ✅ Auth Settings exportadas'));
|
|
347
|
+
|
|
348
|
+
return { success: true };
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.log(chalk.yellow(` ⚠️ Erro ao exportar Auth Settings: ${error.message}`));
|
|
351
|
+
return { success: false };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Backup do Storage via Supabase API
|
|
356
|
+
async function backupStorage(config, backupDir) {
|
|
357
|
+
try {
|
|
358
|
+
const supabase = createClient(config.supabase.url, config.supabase.serviceKey);
|
|
359
|
+
const storageDir = path.join(backupDir, 'storage');
|
|
360
|
+
await ensureDir(storageDir);
|
|
361
|
+
|
|
362
|
+
console.log(chalk.gray(' - Listando buckets de Storage...'));
|
|
363
|
+
|
|
364
|
+
// Listar buckets
|
|
365
|
+
const { data: buckets, error } = await supabase.storage.listBuckets();
|
|
366
|
+
|
|
367
|
+
if (error) {
|
|
368
|
+
console.log(chalk.yellow(` ⚠️ Erro ao listar buckets: ${error.message}`));
|
|
369
|
+
return { success: false, buckets: [] };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const processedBuckets = [];
|
|
373
|
+
|
|
374
|
+
for (const bucket of buckets || []) {
|
|
375
|
+
try {
|
|
376
|
+
console.log(chalk.gray(` - Processando bucket: ${bucket.name}`));
|
|
377
|
+
|
|
378
|
+
// Listar objetos do bucket
|
|
379
|
+
const { data: objects, error: objectsError } = await supabase.storage
|
|
380
|
+
.from(bucket.name)
|
|
381
|
+
.list('', { limit: 1000 });
|
|
382
|
+
|
|
383
|
+
const bucketInfo = {
|
|
384
|
+
id: bucket.id,
|
|
385
|
+
name: bucket.name,
|
|
386
|
+
public: bucket.public,
|
|
387
|
+
file_size_limit: bucket.file_size_limit,
|
|
388
|
+
allowed_mime_types: bucket.allowed_mime_types,
|
|
389
|
+
objects: objects || []
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// Salvar informações do bucket
|
|
393
|
+
const bucketPath = path.join(storageDir, `${bucket.name}.json`);
|
|
394
|
+
await writeJson(bucketPath, bucketInfo);
|
|
395
|
+
|
|
396
|
+
processedBuckets.push({
|
|
397
|
+
name: bucket.name,
|
|
398
|
+
objectCount: objects?.length || 0
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`));
|
|
402
|
+
} catch (error) {
|
|
403
|
+
console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { success: true, buckets: processedBuckets };
|
|
408
|
+
} catch (error) {
|
|
409
|
+
console.log(chalk.yellow(`⚠️ Erro no backup do Storage: ${error.message}`));
|
|
410
|
+
return { success: false, buckets: [] };
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Backup dos Custom Roles via SQL
|
|
415
|
+
async function backupCustomRoles(databaseUrl, backupDir) {
|
|
416
|
+
try {
|
|
417
|
+
console.log(chalk.gray(' - Exportando Custom Roles...'));
|
|
418
|
+
|
|
419
|
+
const customRolesFile = path.join(backupDir, 'custom-roles.sql');
|
|
420
|
+
|
|
421
|
+
// Query para obter roles customizados com senhas
|
|
422
|
+
const customRolesQuery = `
|
|
423
|
+
-- Custom Roles Backup
|
|
424
|
+
-- Roles customizados com senhas
|
|
425
|
+
|
|
426
|
+
SELECT rolname, rolsuper, rolinherit, rolcreaterole, rolcreatedb, rolcanlogin, rolreplication, rolconnlimit, rolpassword
|
|
427
|
+
FROM pg_roles
|
|
428
|
+
WHERE rolname NOT IN ('postgres', 'supabase_admin', 'supabase_auth_admin', 'supabase_storage_admin', 'supabase_read_only_user', 'authenticator', 'anon', 'authenticated', 'service_role')
|
|
429
|
+
ORDER BY rolname;
|
|
430
|
+
`;
|
|
431
|
+
|
|
432
|
+
// Executar query e salvar resultado
|
|
433
|
+
const { stdout } = await runCommand(
|
|
434
|
+
`psql "${databaseUrl}" -t -c "${customRolesQuery}"`
|
|
218
435
|
);
|
|
219
436
|
|
|
220
|
-
|
|
437
|
+
const rolesContent = `-- Custom Roles Backup
|
|
438
|
+
-- Generated at: ${new Date().toISOString()}
|
|
439
|
+
|
|
440
|
+
${customRolesQuery}
|
|
441
|
+
|
|
442
|
+
-- Results:
|
|
443
|
+
${stdout}
|
|
444
|
+
`;
|
|
445
|
+
|
|
446
|
+
await fs.promises.writeFile(customRolesFile, rolesContent);
|
|
447
|
+
|
|
448
|
+
const stats = fs.statSync(customRolesFile);
|
|
449
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
450
|
+
|
|
451
|
+
console.log(chalk.green(` ✅ Custom Roles exportados: ${sizeKB} KB`));
|
|
452
|
+
|
|
453
|
+
return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] };
|
|
221
454
|
} catch (error) {
|
|
222
|
-
|
|
455
|
+
console.log(chalk.yellow(` ⚠️ Erro ao exportar Custom Roles: ${error.message}`));
|
|
456
|
+
return { success: false, roles: [] };
|
|
223
457
|
}
|
|
224
458
|
}
|
|
225
459
|
|
|
226
|
-
//
|
|
227
|
-
async function
|
|
460
|
+
// Backup das Realtime Settings via SQL
|
|
461
|
+
async function backupRealtimeSettings(databaseUrl, backupDir) {
|
|
228
462
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
463
|
+
console.log(chalk.gray(' - Exportando Realtime Settings...'));
|
|
464
|
+
|
|
465
|
+
const realtimeFile = path.join(backupDir, 'realtime-settings.sql');
|
|
466
|
+
|
|
467
|
+
// Query para obter configurações de Realtime
|
|
468
|
+
const realtimeQuery = `
|
|
469
|
+
-- Realtime Settings Backup
|
|
470
|
+
-- Publicações e configurações de Realtime
|
|
471
|
+
|
|
472
|
+
-- Publicações
|
|
473
|
+
SELECT pubname, puballtables, pubinsert, pubupdate, pubdelete, pubtruncate
|
|
474
|
+
FROM pg_publication
|
|
475
|
+
ORDER BY pubname;
|
|
476
|
+
|
|
477
|
+
-- Tabelas publicadas
|
|
478
|
+
SELECT p.pubname, c.relname as table_name, n.nspname as schema_name
|
|
479
|
+
FROM pg_publication_tables pt
|
|
480
|
+
JOIN pg_publication p ON p.oid = pt.ptpubid
|
|
481
|
+
JOIN pg_class c ON c.oid = pt.ptrelid
|
|
482
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
483
|
+
ORDER BY p.pubname, n.nspname, c.relname;
|
|
484
|
+
`;
|
|
485
|
+
|
|
486
|
+
// Executar query e salvar resultado
|
|
487
|
+
const { stdout } = await runCommand(
|
|
488
|
+
`psql "${databaseUrl}" -t -c "${realtimeQuery}"`
|
|
489
|
+
);
|
|
231
490
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
await ensureDir(inventoryDir);
|
|
491
|
+
const realtimeContent = `-- Realtime Settings Backup
|
|
492
|
+
-- Generated at: ${new Date().toISOString()}
|
|
235
493
|
|
|
236
|
-
|
|
237
|
-
const filePath = path.join(inventoryDir, `${component}.json`);
|
|
238
|
-
await writeJson(filePath, data);
|
|
239
|
-
}
|
|
494
|
+
${realtimeQuery}
|
|
240
495
|
|
|
241
|
-
|
|
496
|
+
-- Results:
|
|
497
|
+
${stdout}
|
|
498
|
+
`;
|
|
499
|
+
|
|
500
|
+
await fs.promises.writeFile(realtimeFile, realtimeContent);
|
|
501
|
+
|
|
502
|
+
const stats = fs.statSync(realtimeFile);
|
|
503
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
504
|
+
|
|
505
|
+
console.log(chalk.green(` ✅ Realtime Settings exportados: ${sizeKB} KB`));
|
|
506
|
+
|
|
507
|
+
return { success: true };
|
|
242
508
|
} catch (error) {
|
|
243
|
-
console.log(chalk.yellow(
|
|
509
|
+
console.log(chalk.yellow(` ⚠️ Erro ao exportar Realtime Settings: ${error.message}`));
|
|
510
|
+
return { success: false };
|
|
244
511
|
}
|
|
245
512
|
}
|
|
246
513
|
|
|
247
|
-
//
|
|
248
|
-
async function
|
|
249
|
-
const localFunctionsPath = 'supabase/functions';
|
|
250
|
-
|
|
514
|
+
// Validar arquivo SQL
|
|
515
|
+
async function validateSqlFile(filePath) {
|
|
251
516
|
try {
|
|
252
|
-
if (fs.existsSync(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
517
|
+
if (!fs.existsSync(filePath)) {
|
|
518
|
+
return { valid: false, error: 'Arquivo não existe', size: 0, sizeKB: '0.0' };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const stats = fs.statSync(filePath);
|
|
522
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
523
|
+
|
|
524
|
+
if (stats.size === 0) {
|
|
525
|
+
return { valid: false, error: 'Arquivo vazio', size: 0, sizeKB: '0.0' };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
529
|
+
|
|
530
|
+
const sqlKeywords = ['CREATE', 'INSERT', 'COPY', 'ALTER', 'DROP', 'GRANT', 'REVOKE'];
|
|
531
|
+
const hasValidContent = sqlKeywords.some(keyword =>
|
|
532
|
+
content.toUpperCase().includes(keyword)
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
if (!hasValidContent) {
|
|
536
|
+
return { valid: false, error: 'Sem conteúdo SQL válido', size: stats.size, sizeKB };
|
|
258
537
|
}
|
|
538
|
+
|
|
539
|
+
return { valid: true, error: null, size: stats.size, sizeKB };
|
|
259
540
|
} catch (error) {
|
|
260
|
-
|
|
541
|
+
return { valid: false, error: error.message, size: 0, sizeKB: '0.0' };
|
|
261
542
|
}
|
|
262
543
|
}
|
|
263
544
|
|
|
264
|
-
// Gerar manifesto do backup
|
|
265
|
-
async function
|
|
545
|
+
// Gerar manifesto do backup completo
|
|
546
|
+
async function generateCompleteBackupManifest(config, backupDir, results) {
|
|
266
547
|
const manifest = {
|
|
267
548
|
created_at: new Date().toISOString(),
|
|
268
549
|
project_id: config.supabase.projectId,
|
|
269
550
|
smoonb_version: require('../../package.json').version,
|
|
270
|
-
backup_type: '
|
|
551
|
+
backup_type: 'complete_supabase',
|
|
552
|
+
components: {
|
|
553
|
+
database: {
|
|
554
|
+
success: results.database.success,
|
|
555
|
+
files: results.database.files.length,
|
|
556
|
+
total_size_kb: results.database.files.reduce((total, file) => total + parseFloat(file.sizeKB), 0).toFixed(1)
|
|
557
|
+
},
|
|
558
|
+
edge_functions: {
|
|
559
|
+
success: results.edgeFunctions.success,
|
|
560
|
+
functions_count: results.edgeFunctions.functions.length,
|
|
561
|
+
functions: results.edgeFunctions.functions.map(f => f.name)
|
|
562
|
+
},
|
|
563
|
+
auth_settings: {
|
|
564
|
+
success: results.authSettings.success
|
|
565
|
+
},
|
|
566
|
+
storage: {
|
|
567
|
+
success: results.storage.success,
|
|
568
|
+
buckets_count: results.storage.buckets.length,
|
|
569
|
+
buckets: results.storage.buckets.map(b => b.name)
|
|
570
|
+
},
|
|
571
|
+
custom_roles: {
|
|
572
|
+
success: results.customRoles.success,
|
|
573
|
+
roles_count: results.customRoles.roles.length
|
|
574
|
+
},
|
|
575
|
+
realtime: {
|
|
576
|
+
success: results.realtime.success
|
|
577
|
+
}
|
|
578
|
+
},
|
|
271
579
|
files: {
|
|
272
580
|
roles: 'roles.sql',
|
|
273
581
|
schema: 'schema.sql',
|
|
274
|
-
data: 'data.sql'
|
|
582
|
+
data: 'data.sql',
|
|
583
|
+
custom_roles: 'custom-roles.sql',
|
|
584
|
+
realtime_settings: 'realtime-settings.sql',
|
|
585
|
+
auth_settings: 'auth-settings.json',
|
|
586
|
+
edge_functions: 'edge-functions/',
|
|
587
|
+
storage: 'storage/'
|
|
275
588
|
},
|
|
276
589
|
hashes: {},
|
|
277
|
-
inventory: {},
|
|
278
590
|
validation: {
|
|
279
|
-
|
|
280
|
-
|
|
591
|
+
all_components_backed_up: Object.values(results).every(r => r.success),
|
|
592
|
+
total_files: results.database.files.length + 4, // +4 for custom files
|
|
593
|
+
backup_complete: true
|
|
281
594
|
}
|
|
282
595
|
};
|
|
283
596
|
|
|
284
|
-
// Calcular hashes dos arquivos
|
|
285
|
-
|
|
597
|
+
// Calcular hashes dos arquivos principais
|
|
598
|
+
const mainFiles = ['roles.sql', 'schema.sql', 'data.sql', 'custom-roles.sql', 'realtime-settings.sql'];
|
|
599
|
+
for (const filename of mainFiles) {
|
|
286
600
|
const filePath = path.join(backupDir, filename);
|
|
287
601
|
if (fs.existsSync(filePath)) {
|
|
288
|
-
manifest.hashes[
|
|
602
|
+
manifest.hashes[filename] = await sha256(filePath);
|
|
289
603
|
}
|
|
290
604
|
}
|
|
291
605
|
|
|
292
|
-
// Adicionar referências ao inventário
|
|
293
|
-
const inventoryDir = path.join(backupDir, 'inventory');
|
|
294
|
-
if (fs.existsSync(inventoryDir)) {
|
|
295
|
-
const inventoryFiles = fs.readdirSync(inventoryDir);
|
|
296
|
-
manifest.inventory = inventoryFiles.reduce((acc, file) => {
|
|
297
|
-
const component = path.basename(file, '.json');
|
|
298
|
-
acc[component] = `inventory/${file}`;
|
|
299
|
-
return acc;
|
|
300
|
-
}, {});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
606
|
const manifestPath = path.join(backupDir, 'backup-manifest.json');
|
|
304
607
|
await writeJson(manifestPath, manifest);
|
|
305
608
|
}
|