smoonb 0.0.46 → 0.0.48
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/index.js +290 -0
- package/src/commands/backup/steps/00-docker-validation.js +24 -0
- package/src/commands/backup/steps/01-database.js +72 -0
- package/src/commands/backup/steps/02-database-separated.js +82 -0
- package/src/commands/backup/steps/03-database-settings.js +178 -0
- package/src/commands/backup/steps/04-auth-settings.js +43 -0
- package/src/commands/backup/steps/05-realtime-settings.js +26 -0
- package/src/commands/backup/steps/06-storage.js +90 -0
- package/src/commands/backup/steps/07-custom-roles.js +39 -0
- package/src/commands/backup/steps/08-edge-functions.js +159 -0
- package/src/commands/backup/steps/09-supabase-temp.js +48 -0
- package/src/commands/backup/steps/10-migrations.js +80 -0
- package/src/commands/backup/utils.js +69 -0
- package/src/commands/restore/index.js +190 -0
- package/src/commands/restore/steps/00-backup-selection.js +38 -0
- package/src/commands/restore/steps/01-components-selection.js +84 -0
- package/src/commands/restore/steps/02-confirmation.js +19 -0
- package/src/commands/restore/steps/03-database.js +81 -0
- package/src/commands/restore/steps/04-edge-functions.js +112 -0
- package/src/commands/restore/steps/05-auth-settings.js +51 -0
- package/src/commands/restore/steps/06-storage.js +58 -0
- package/src/commands/restore/steps/07-database-settings.js +65 -0
- package/src/commands/restore/steps/08-realtime-settings.js +50 -0
- package/src/commands/restore/utils.js +139 -0
- package/src/interactive/envMapper.js +37 -23
- package/src/utils/fsExtra.js +98 -0
- package/src/utils/supabaseLink.js +82 -0
- package/src/commands/backup.js +0 -939
- package/src/commands/restore.js +0 -786
package/src/commands/restore.js
DELETED
|
@@ -1,786 +0,0 @@
|
|
|
1
|
-
const chalk = require('chalk');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const { readConfig, getSourceProject, getTargetProject } = require('../utils/config');
|
|
5
|
-
const { showBetaBanner } = require('../utils/banner');
|
|
6
|
-
const inquirer = require('inquirer');
|
|
7
|
-
const { readEnvFile, writeEnvFile, backupEnvFile } = require('../utils/env');
|
|
8
|
-
const { saveEnvMap } = require('../utils/envMap');
|
|
9
|
-
const { mapEnvVariablesInteractively } = require('../interactive/envMapper');
|
|
10
|
-
|
|
11
|
-
module.exports = async (options) => {
|
|
12
|
-
showBetaBanner();
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
// Consentimento para leitura e escrita do .env.local
|
|
16
|
-
console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
|
|
17
|
-
console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
|
|
18
|
-
const consent = await inquirer.prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
|
|
19
|
-
if (!consent.ok) {
|
|
20
|
-
console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Preparar diretório de processo restore-YYYY-...
|
|
25
|
-
const rootBackupsDir = path.join(process.cwd(), 'backups');
|
|
26
|
-
const now = new Date();
|
|
27
|
-
const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}-${String(now.getHours()).padStart(2,'0')}-${String(now.getMinutes()).padStart(2,'0')}-${String(now.getSeconds()).padStart(2,'0')}`;
|
|
28
|
-
const processDir = path.join(rootBackupsDir, `restore-${ts}`);
|
|
29
|
-
fs.mkdirSync(path.join(processDir, 'env'), { recursive: true });
|
|
30
|
-
|
|
31
|
-
// Backup do .env.local
|
|
32
|
-
const envPath = path.join(process.cwd(), '.env.local');
|
|
33
|
-
const envBackupPath = path.join(processDir, 'env', '.env.local');
|
|
34
|
-
await backupEnvFile(envPath, envBackupPath);
|
|
35
|
-
console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
|
|
36
|
-
|
|
37
|
-
// Leitura e mapeamento interativo
|
|
38
|
-
const currentEnv = await readEnvFile(envPath);
|
|
39
|
-
const expectedKeys = [
|
|
40
|
-
'NEXT_PUBLIC_SUPABASE_URL',
|
|
41
|
-
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
42
|
-
'SUPABASE_SERVICE_ROLE_KEY',
|
|
43
|
-
'SUPABASE_DB_URL',
|
|
44
|
-
'SUPABASE_PROJECT_ID',
|
|
45
|
-
'SUPABASE_ACCESS_TOKEN',
|
|
46
|
-
'SMOONB_OUTPUT_DIR'
|
|
47
|
-
];
|
|
48
|
-
const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
|
|
49
|
-
await writeEnvFile(envPath, finalEnv);
|
|
50
|
-
await saveEnvMap(dePara, path.join(processDir, 'env', 'env-map.json'));
|
|
51
|
-
console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
|
|
52
|
-
|
|
53
|
-
// Resolver valores esperados a partir do de-para
|
|
54
|
-
function getValue(expectedKey) {
|
|
55
|
-
const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
|
|
56
|
-
return clientKey ? finalEnv[clientKey] : '';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Construir targetProject a partir do .env.local mapeado
|
|
60
|
-
const targetProject = {
|
|
61
|
-
targetProjectId: getValue('SUPABASE_PROJECT_ID'),
|
|
62
|
-
targetUrl: getValue('NEXT_PUBLIC_SUPABASE_URL'),
|
|
63
|
-
targetAnonKey: getValue('NEXT_PUBLIC_SUPABASE_ANON_KEY'),
|
|
64
|
-
targetServiceKey: getValue('SUPABASE_SERVICE_ROLE_KEY'),
|
|
65
|
-
targetDatabaseUrl: getValue('SUPABASE_DB_URL'),
|
|
66
|
-
targetAccessToken: getValue('SUPABASE_ACCESS_TOKEN')
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
console.log(chalk.blue(`📁 Buscando backups em: ${getValue('SMOONB_OUTPUT_DIR') || './backups'}`));
|
|
70
|
-
|
|
71
|
-
// 1. Listar backups válidos (.backup.gz)
|
|
72
|
-
const validBackups = await listValidBackups(getValue('SMOONB_OUTPUT_DIR') || './backups');
|
|
73
|
-
|
|
74
|
-
if (validBackups.length === 0) {
|
|
75
|
-
console.error(chalk.red('❌ Nenhum backup válido encontrado'));
|
|
76
|
-
console.log(chalk.yellow('💡 Execute primeiro: npx smoonb backup'));
|
|
77
|
-
process.exit(1);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 2. Selecionar backup interativamente
|
|
81
|
-
const selectedBackup = await selectBackupInteractive(validBackups);
|
|
82
|
-
|
|
83
|
-
// 3. Perguntar quais componentes restaurar
|
|
84
|
-
const components = await askRestoreComponents(selectedBackup.path);
|
|
85
|
-
|
|
86
|
-
// Validar que pelo menos um componente foi selecionado
|
|
87
|
-
if (!Object.values(components).some(Boolean)) {
|
|
88
|
-
console.error(chalk.red('\n❌ Nenhum componente selecionado para restauração!'));
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// 4. Mostrar resumo
|
|
93
|
-
showRestoreSummary(selectedBackup, components, targetProject);
|
|
94
|
-
|
|
95
|
-
// 5. Confirmar execução
|
|
96
|
-
const confirmed = await confirmExecution();
|
|
97
|
-
if (!confirmed) {
|
|
98
|
-
console.log(chalk.yellow('Restauração cancelada.'));
|
|
99
|
-
process.exit(0);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// 6. Executar restauração
|
|
103
|
-
console.log(chalk.blue('\n🚀 Iniciando restauração...'));
|
|
104
|
-
|
|
105
|
-
// 6.1 Database (se selecionado)
|
|
106
|
-
if (components.database) {
|
|
107
|
-
await restoreDatabaseGz(
|
|
108
|
-
path.join(selectedBackup.path, selectedBackup.backupFile),
|
|
109
|
-
targetProject.targetDatabaseUrl
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 6.2 Edge Functions (se selecionado)
|
|
114
|
-
if (components.edgeFunctions) {
|
|
115
|
-
await restoreEdgeFunctions(selectedBackup.path, targetProject);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// 6.3 Auth Settings (se selecionado)
|
|
119
|
-
if (components.authSettings) {
|
|
120
|
-
await restoreAuthSettings(selectedBackup.path, targetProject);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 6.4 Storage Buckets (se selecionado)
|
|
124
|
-
if (components.storage) {
|
|
125
|
-
await restoreStorageBuckets(selectedBackup.path, targetProject);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 6.5 Database Settings (se selecionado)
|
|
129
|
-
if (components.databaseSettings) {
|
|
130
|
-
await restoreDatabaseSettings(selectedBackup.path, targetProject);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 6.6 Realtime Settings (se selecionado)
|
|
134
|
-
if (components.realtimeSettings) {
|
|
135
|
-
await restoreRealtimeSettings(selectedBackup.path, targetProject);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// report.json de restauração
|
|
139
|
-
const report = {
|
|
140
|
-
process: 'restore',
|
|
141
|
-
created_at: new Date().toISOString(),
|
|
142
|
-
target_project_id: targetProject.targetProjectId,
|
|
143
|
-
assets: {
|
|
144
|
-
env: path.join(processDir, 'env', '.env.local'),
|
|
145
|
-
env_map: path.join(processDir, 'env', 'env-map.json')
|
|
146
|
-
},
|
|
147
|
-
components: components,
|
|
148
|
-
notes: [
|
|
149
|
-
'supabase/functions limpo antes e depois do deploy (se Edge Functions selecionado)'
|
|
150
|
-
]
|
|
151
|
-
};
|
|
152
|
-
try {
|
|
153
|
-
require('fs').writeFileSync(path.join(processDir, 'report.json'), JSON.stringify(report, null, 2));
|
|
154
|
-
} catch (e) {
|
|
155
|
-
// silencioso
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
console.log(chalk.green('\n🎉 Restauração completa finalizada!'));
|
|
159
|
-
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.error(chalk.red(`❌ Erro na restauração: ${error.message}`));
|
|
162
|
-
process.exit(1);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
// Listar backups válidos (aceita .backup.gz e .backup)
|
|
167
|
-
async function listValidBackups(backupsDir) {
|
|
168
|
-
if (!fs.existsSync(backupsDir)) {
|
|
169
|
-
return [];
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const items = fs.readdirSync(backupsDir, { withFileTypes: true });
|
|
173
|
-
const validBackups = [];
|
|
174
|
-
|
|
175
|
-
for (const item of items) {
|
|
176
|
-
if (item.isDirectory() && item.name.startsWith('backup-')) {
|
|
177
|
-
const backupPath = path.join(backupsDir, item.name);
|
|
178
|
-
const files = fs.readdirSync(backupPath);
|
|
179
|
-
// Aceitar tanto .backup.gz quanto .backup
|
|
180
|
-
const backupFile = files.find(file =>
|
|
181
|
-
file.endsWith('.backup.gz') || file.endsWith('.backup')
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
if (backupFile) {
|
|
185
|
-
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
186
|
-
let manifest = null;
|
|
187
|
-
|
|
188
|
-
if (fs.existsSync(manifestPath)) {
|
|
189
|
-
try {
|
|
190
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
191
|
-
} catch (error) {
|
|
192
|
-
// Ignorar erro de leitura do manifest
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const stats = fs.statSync(path.join(backupPath, backupFile));
|
|
197
|
-
|
|
198
|
-
validBackups.push({
|
|
199
|
-
name: item.name,
|
|
200
|
-
path: backupPath,
|
|
201
|
-
backupFile: backupFile,
|
|
202
|
-
created: manifest?.created_at || stats.birthtime.toISOString(),
|
|
203
|
-
projectId: manifest?.project_id || 'Desconhecido',
|
|
204
|
-
size: formatBytes(stats.size),
|
|
205
|
-
manifest: manifest
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return validBackups.sort((a, b) => new Date(b.created) - new Date(a.created));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Formatar bytes
|
|
215
|
-
function formatBytes(bytes) {
|
|
216
|
-
if (bytes === 0) return '0 Bytes';
|
|
217
|
-
const k = 1024;
|
|
218
|
-
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
219
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
220
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Seleção interativa de backup
|
|
224
|
-
async function selectBackupInteractive(backups) {
|
|
225
|
-
console.log(chalk.blue('\n📋 Backups disponíveis:'));
|
|
226
|
-
console.log(chalk.blue('═'.repeat(80)));
|
|
227
|
-
|
|
228
|
-
backups.forEach((backup, index) => {
|
|
229
|
-
const date = new Date(backup.created).toLocaleString('pt-BR');
|
|
230
|
-
const projectInfo = backup.projectId !== 'Desconhecido' ? ` (${backup.projectId})` : '';
|
|
231
|
-
|
|
232
|
-
console.log(`${index + 1}. ${backup.name}${projectInfo}`);
|
|
233
|
-
console.log(` 📅 ${date} | 📦 ${backup.size}`);
|
|
234
|
-
console.log('');
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const readline = require('readline').createInterface({
|
|
238
|
-
input: process.stdin,
|
|
239
|
-
output: process.stdout
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
const question = (query) => new Promise(resolve => readline.question(query, resolve));
|
|
243
|
-
|
|
244
|
-
const choice = await question(`\nDigite o número do backup para restaurar (1-${backups.length}): `);
|
|
245
|
-
readline.close();
|
|
246
|
-
|
|
247
|
-
const backupIndex = parseInt(choice) - 1;
|
|
248
|
-
|
|
249
|
-
if (backupIndex < 0 || backupIndex >= backups.length) {
|
|
250
|
-
throw new Error('Número inválido');
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return backups[backupIndex];
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Perguntar quais componentes restaurar
|
|
257
|
-
async function askRestoreComponents(backupPath) {
|
|
258
|
-
const questions = [];
|
|
259
|
-
|
|
260
|
-
// Database
|
|
261
|
-
questions.push({
|
|
262
|
-
type: 'confirm',
|
|
263
|
-
name: 'restoreDatabase',
|
|
264
|
-
message: 'Deseja restaurar Database (S/n):',
|
|
265
|
-
default: true
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Edge Functions
|
|
269
|
-
const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
|
|
270
|
-
if (fs.existsSync(edgeFunctionsDir) && fs.readdirSync(edgeFunctionsDir).length > 0) {
|
|
271
|
-
questions.push({
|
|
272
|
-
type: 'confirm',
|
|
273
|
-
name: 'restoreEdgeFunctions',
|
|
274
|
-
message: 'Deseja restaurar Edge Functions (S/n):',
|
|
275
|
-
default: true
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Auth Settings
|
|
280
|
-
if (fs.existsSync(path.join(backupPath, 'auth-settings.json'))) {
|
|
281
|
-
questions.push({
|
|
282
|
-
type: 'confirm',
|
|
283
|
-
name: 'restoreAuthSettings',
|
|
284
|
-
message: 'Deseja restaurar Auth Settings (s/N):',
|
|
285
|
-
default: false
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Storage Buckets
|
|
290
|
-
const storageDir = path.join(backupPath, 'storage');
|
|
291
|
-
if (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0) {
|
|
292
|
-
questions.push({
|
|
293
|
-
type: 'confirm',
|
|
294
|
-
name: 'restoreStorage',
|
|
295
|
-
message: 'Deseja ver informações de Storage Buckets (s/N):',
|
|
296
|
-
default: false
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Database Extensions and Settings
|
|
301
|
-
const dbSettingsFiles = fs.readdirSync(backupPath)
|
|
302
|
-
.filter(file => file.startsWith('database-settings-') && file.endsWith('.json'));
|
|
303
|
-
if (dbSettingsFiles.length > 0) {
|
|
304
|
-
questions.push({
|
|
305
|
-
type: 'confirm',
|
|
306
|
-
name: 'restoreDatabaseSettings',
|
|
307
|
-
message: 'Deseja restaurar Database Extensions and Settings (s/N):',
|
|
308
|
-
default: false
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Realtime Settings
|
|
313
|
-
if (fs.existsSync(path.join(backupPath, 'realtime-settings.json'))) {
|
|
314
|
-
questions.push({
|
|
315
|
-
type: 'confirm',
|
|
316
|
-
name: 'restoreRealtimeSettings',
|
|
317
|
-
message: 'Deseja restaurar Realtime Settings (s/N):',
|
|
318
|
-
default: false
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const answers = await inquirer.prompt(questions);
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
database: answers.restoreDatabase,
|
|
326
|
-
edgeFunctions: answers.restoreEdgeFunctions || false,
|
|
327
|
-
storage: answers.restoreStorage || false,
|
|
328
|
-
authSettings: answers.restoreAuthSettings || false,
|
|
329
|
-
databaseSettings: answers.restoreDatabaseSettings || false,
|
|
330
|
-
realtimeSettings: answers.restoreRealtimeSettings || false
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Mostrar resumo da restauração
|
|
335
|
-
function showRestoreSummary(backup, components, targetProject) {
|
|
336
|
-
console.log(chalk.blue('\n📋 Resumo da Restauração:'));
|
|
337
|
-
console.log(chalk.blue('═'.repeat(80)));
|
|
338
|
-
console.log(chalk.cyan(`📦 Backup: ${backup.name}`));
|
|
339
|
-
console.log(chalk.cyan(`📤 Projeto Origem: ${backup.projectId}`));
|
|
340
|
-
console.log(chalk.cyan(`📥 Projeto Destino: ${targetProject.targetProjectId}`));
|
|
341
|
-
console.log('');
|
|
342
|
-
console.log(chalk.cyan('Componentes que serão restaurados:'));
|
|
343
|
-
console.log('');
|
|
344
|
-
|
|
345
|
-
if (components.database) {
|
|
346
|
-
console.log('✅ Database (psql -f via Docker)');
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (components.edgeFunctions) {
|
|
350
|
-
const edgeFunctionsDir = path.join(backup.path, 'edge-functions');
|
|
351
|
-
const functions = fs.readdirSync(edgeFunctionsDir).filter(item =>
|
|
352
|
-
fs.statSync(path.join(edgeFunctionsDir, item)).isDirectory()
|
|
353
|
-
);
|
|
354
|
-
console.log(`⚡ Edge Functions: ${functions.length} function(s)`);
|
|
355
|
-
functions.forEach(func => console.log(` - ${func}`));
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (components.authSettings) {
|
|
359
|
-
console.log('🔐 Auth Settings: Exibir URL e valores para configuração manual');
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (components.storage) {
|
|
363
|
-
console.log('📦 Storage Buckets: Exibir informações e instruções do Google Colab');
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (components.databaseSettings) {
|
|
367
|
-
console.log('🔧 Database Extensions and Settings: Restaurar via SQL');
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (components.realtimeSettings) {
|
|
371
|
-
console.log('🔄 Realtime Settings: Exibir URL e valores para configuração manual');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
console.log('');
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Confirmar execução
|
|
378
|
-
async function confirmExecution() {
|
|
379
|
-
const readline = require('readline').createInterface({
|
|
380
|
-
input: process.stdin,
|
|
381
|
-
output: process.stdout
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
const question = (query) => new Promise(resolve => readline.question(query, resolve));
|
|
385
|
-
|
|
386
|
-
const confirm = await question('Deseja continuar com a restauração? (s/N): ');
|
|
387
|
-
readline.close();
|
|
388
|
-
|
|
389
|
-
return confirm.toLowerCase() === 's';
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Restaurar Database via psql (conforme documentação oficial Supabase: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore)
|
|
393
|
-
// Aceita tanto arquivos .backup.gz quanto .backup já descompactados
|
|
394
|
-
async function restoreDatabaseGz(backupFilePath, targetDatabaseUrl) {
|
|
395
|
-
console.log(chalk.blue('📊 Restaurando Database...'));
|
|
396
|
-
|
|
397
|
-
try {
|
|
398
|
-
const { execSync } = require('child_process');
|
|
399
|
-
|
|
400
|
-
const backupDirAbs = path.resolve(path.dirname(backupFilePath));
|
|
401
|
-
const fileName = path.basename(backupFilePath);
|
|
402
|
-
let uncompressedFile = fileName;
|
|
403
|
-
|
|
404
|
-
// Verificar se é arquivo .backup.gz (compactado) ou .backup (descompactado)
|
|
405
|
-
if (fileName.endsWith('.backup.gz')) {
|
|
406
|
-
console.log(chalk.gray(' - Arquivo .backup.gz detectado'));
|
|
407
|
-
console.log(chalk.gray(' - Extraindo arquivo .gz...'));
|
|
408
|
-
|
|
409
|
-
const unzipCmd = [
|
|
410
|
-
'docker run --rm',
|
|
411
|
-
`-v "${backupDirAbs}:/host"`,
|
|
412
|
-
'postgres:17 gunzip /host/' + fileName
|
|
413
|
-
].join(' ');
|
|
414
|
-
|
|
415
|
-
execSync(unzipCmd, { stdio: 'pipe' });
|
|
416
|
-
uncompressedFile = fileName.replace('.gz', '');
|
|
417
|
-
console.log(chalk.gray(' - Arquivo descompactado: ' + uncompressedFile));
|
|
418
|
-
} else if (fileName.endsWith('.backup')) {
|
|
419
|
-
console.log(chalk.gray(' - Arquivo .backup detectado (já descompactado)'));
|
|
420
|
-
console.log(chalk.gray(' - Prosseguindo com restauração direta'));
|
|
421
|
-
} else {
|
|
422
|
-
throw new Error(`Formato de arquivo inválido. Esperado .backup.gz ou .backup, recebido: ${fileName}`);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Extrair credenciais da URL de conexão
|
|
426
|
-
const urlMatch = targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
|
|
427
|
-
|
|
428
|
-
if (!urlMatch) {
|
|
429
|
-
throw new Error('Database URL inválida. Formato esperado: postgresql://user:password@host/database');
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Comando psql conforme documentação oficial Supabase
|
|
433
|
-
// Formato: psql -d [CONNECTION_STRING] -f /file/path
|
|
434
|
-
// Referência: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore
|
|
435
|
-
const restoreCmd = [
|
|
436
|
-
'docker run --rm --network host',
|
|
437
|
-
`-v "${backupDirAbs}:/host"`,
|
|
438
|
-
`-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
|
|
439
|
-
'postgres:17 psql',
|
|
440
|
-
`-d "${targetDatabaseUrl}"`,
|
|
441
|
-
`-f /host/${uncompressedFile}`
|
|
442
|
-
].join(' ');
|
|
443
|
-
|
|
444
|
-
console.log(chalk.gray(' - Executando psql via Docker...'));
|
|
445
|
-
console.log(chalk.gray(' ℹ️ Seguindo documentação oficial Supabase'));
|
|
446
|
-
console.log(chalk.yellow(' ⚠️ AVISO: Erros como "object already exists" são ESPERADOS'));
|
|
447
|
-
console.log(chalk.yellow(' ⚠️ Isto acontece porque o backup contém CREATE para todos os schemas'));
|
|
448
|
-
console.log(chalk.yellow(' ⚠️ Supabase já tem auth e storage criados, então esses erros são normais'));
|
|
449
|
-
|
|
450
|
-
// Executar comando de restauração
|
|
451
|
-
execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
|
|
452
|
-
|
|
453
|
-
console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
|
|
454
|
-
console.log(chalk.gray(' ℹ️ Erros "already exists" são normais e não afetam a restauração'));
|
|
455
|
-
|
|
456
|
-
} catch (error) {
|
|
457
|
-
// Erros esperados conforme documentação oficial Supabase
|
|
458
|
-
// Referência: https://supabase.com/docs/guides/platform/migrating-within-supabase/dashboard-restore#common-errors
|
|
459
|
-
if (error.message.includes('already exists') ||
|
|
460
|
-
error.message.includes('constraint') ||
|
|
461
|
-
error.message.includes('duplicate') ||
|
|
462
|
-
error.stdout?.includes('already exists')) {
|
|
463
|
-
console.log(chalk.yellow(' ⚠️ Erros esperados encontrados (conforme documentação Supabase)'));
|
|
464
|
-
console.log(chalk.green(' ✅ Database restaurada com sucesso!'));
|
|
465
|
-
console.log(chalk.gray(' ℹ️ Erros são ignorados pois são comandos de CREATE que já existem'));
|
|
466
|
-
} else {
|
|
467
|
-
console.error(chalk.red(` ❌ Erro inesperado na restauração: ${error.message}`));
|
|
468
|
-
throw error;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Restaurar Edge Functions via supabase functions deploy
|
|
474
|
-
async function restoreEdgeFunctions(backupPath, targetProject) {
|
|
475
|
-
console.log(chalk.blue('\n⚡ Restaurando Edge Functions...'));
|
|
476
|
-
|
|
477
|
-
try {
|
|
478
|
-
const fs = require('fs').promises;
|
|
479
|
-
const { execSync } = require('child_process');
|
|
480
|
-
const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
|
|
481
|
-
|
|
482
|
-
if (!await fs.access(edgeFunctionsDir).then(() => true).catch(() => false)) {
|
|
483
|
-
console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const items = await fs.readdir(edgeFunctionsDir);
|
|
488
|
-
const functions = [];
|
|
489
|
-
|
|
490
|
-
for (const item of items) {
|
|
491
|
-
const itemPath = path.join(edgeFunctionsDir, item);
|
|
492
|
-
const stats = await fs.stat(itemPath);
|
|
493
|
-
if (stats.isDirectory()) {
|
|
494
|
-
functions.push(item);
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (functions.length === 0) {
|
|
499
|
-
console.log(chalk.yellow(' ⚠️ Nenhuma Edge Function encontrada no backup'));
|
|
500
|
-
return;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
console.log(chalk.gray(` - Encontradas ${functions.length} Edge Function(s)`));
|
|
504
|
-
|
|
505
|
-
// ✅ COPIAR Edge Functions de backups/backup-XXX/edge-functions para supabase/functions
|
|
506
|
-
const supabaseFunctionsDir = path.join(process.cwd(), 'supabase', 'functions');
|
|
507
|
-
|
|
508
|
-
// Criar diretório supabase/functions se não existir
|
|
509
|
-
await fs.mkdir(supabaseFunctionsDir, { recursive: true });
|
|
510
|
-
|
|
511
|
-
// Limpar supabase/functions antes de copiar
|
|
512
|
-
console.log(chalk.gray(' - Limpando supabase/functions...'));
|
|
513
|
-
try {
|
|
514
|
-
await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
|
|
515
|
-
await fs.mkdir(supabaseFunctionsDir, { recursive: true });
|
|
516
|
-
} catch (cleanError) {
|
|
517
|
-
// Ignorar erro de limpeza se não existir
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Copiar cada Edge Function para supabase/functions
|
|
521
|
-
for (const funcName of functions) {
|
|
522
|
-
const backupFuncPath = path.join(edgeFunctionsDir, funcName);
|
|
523
|
-
const targetFuncPath = path.join(supabaseFunctionsDir, funcName);
|
|
524
|
-
|
|
525
|
-
console.log(chalk.gray(` - Copiando ${funcName} para supabase/functions...`));
|
|
526
|
-
|
|
527
|
-
// Copiar recursivamente
|
|
528
|
-
await copyDirectoryRecursive(backupFuncPath, targetFuncPath);
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
console.log(chalk.gray(` - Linkando com projeto ${targetProject.targetProjectId}...`));
|
|
532
|
-
|
|
533
|
-
// Linkar com o projeto destino
|
|
534
|
-
try {
|
|
535
|
-
execSync(`supabase link --project-ref ${targetProject.targetProjectId}`, {
|
|
536
|
-
stdio: 'pipe',
|
|
537
|
-
encoding: 'utf8',
|
|
538
|
-
timeout: 10000,
|
|
539
|
-
env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
|
|
540
|
-
});
|
|
541
|
-
} catch (linkError) {
|
|
542
|
-
console.log(chalk.yellow(' ⚠️ Link pode já existir, continuando...'));
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Deploy das Edge Functions
|
|
546
|
-
for (const funcName of functions) {
|
|
547
|
-
console.log(chalk.gray(` - Deployando ${funcName}...`));
|
|
548
|
-
|
|
549
|
-
try {
|
|
550
|
-
execSync(`supabase functions deploy ${funcName}`, {
|
|
551
|
-
cwd: process.cwd(),
|
|
552
|
-
stdio: 'pipe',
|
|
553
|
-
encoding: 'utf8',
|
|
554
|
-
timeout: 120000,
|
|
555
|
-
env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
|
|
556
|
-
});
|
|
557
|
-
|
|
558
|
-
console.log(chalk.green(` ✅ ${funcName} deployada com sucesso!`));
|
|
559
|
-
} catch (deployError) {
|
|
560
|
-
console.log(chalk.yellow(` ⚠️ ${funcName} - deploy falhou: ${deployError.message}`));
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// Limpar supabase/functions após deploy
|
|
565
|
-
console.log(chalk.gray(' - Limpando supabase/functions após deploy...'));
|
|
566
|
-
try {
|
|
567
|
-
await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
|
|
568
|
-
} catch (cleanError) {
|
|
569
|
-
// Ignorar erro de limpeza
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
console.log(chalk.green(' ✅ Edge Functions restauradas com sucesso!'));
|
|
573
|
-
|
|
574
|
-
} catch (error) {
|
|
575
|
-
console.error(chalk.red(` ❌ Erro ao restaurar Edge Functions: ${error.message}`));
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Função auxiliar para copiar diretório recursivamente
|
|
580
|
-
async function copyDirectoryRecursive(src, dest) {
|
|
581
|
-
const fs = require('fs').promises;
|
|
582
|
-
|
|
583
|
-
await fs.mkdir(dest, { recursive: true });
|
|
584
|
-
|
|
585
|
-
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
586
|
-
|
|
587
|
-
for (const entry of entries) {
|
|
588
|
-
const srcPath = path.join(src, entry.name);
|
|
589
|
-
const destPath = path.join(dest, entry.name);
|
|
590
|
-
|
|
591
|
-
if (entry.isDirectory()) {
|
|
592
|
-
await copyDirectoryRecursive(srcPath, destPath);
|
|
593
|
-
} else {
|
|
594
|
-
await fs.copyFile(srcPath, destPath);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Restaurar Storage Buckets (interativo - exibir informações)
|
|
600
|
-
async function restoreStorageBuckets(backupPath, targetProject) {
|
|
601
|
-
console.log(chalk.blue('\n📦 Restaurando Storage Buckets...'));
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
const storageDir = path.join(backupPath, 'storage');
|
|
605
|
-
|
|
606
|
-
if (!fs.existsSync(storageDir)) {
|
|
607
|
-
console.log(chalk.yellow(' ⚠️ Nenhum bucket de Storage encontrado no backup'));
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
612
|
-
let manifest = null;
|
|
613
|
-
|
|
614
|
-
if (fs.existsSync(manifestPath)) {
|
|
615
|
-
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const buckets = manifest?.components?.storage?.buckets || [];
|
|
619
|
-
|
|
620
|
-
if (buckets.length === 0) {
|
|
621
|
-
console.log(chalk.gray(' ℹ️ Nenhum bucket para restaurar'));
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
console.log(chalk.green(`\n ✅ ${buckets.length} bucket(s) encontrado(s) no backup`));
|
|
626
|
-
buckets.forEach(bucket => {
|
|
627
|
-
console.log(chalk.gray(` - ${bucket.name} (${bucket.public ? 'público' : 'privado'})`));
|
|
628
|
-
});
|
|
629
|
-
|
|
630
|
-
const colabUrl = 'https://colab.research.google.com/github/PLyn/supabase-storage-migrate/blob/main/Supabase_Storage_migration.ipynb';
|
|
631
|
-
|
|
632
|
-
console.log(chalk.yellow('\n ⚠️ Migração de objetos de Storage requer processo manual'));
|
|
633
|
-
console.log(chalk.cyan(` ℹ️ Use o script do Google Colab: ${colabUrl}`));
|
|
634
|
-
console.log(chalk.gray('\n 📋 Instruções:'));
|
|
635
|
-
console.log(chalk.gray(' 1. Execute o script no Google Colab'));
|
|
636
|
-
console.log(chalk.gray(' 2. Configure as credenciais dos projetos (origem e destino)'));
|
|
637
|
-
console.log(chalk.gray(' 3. Execute a migração'));
|
|
638
|
-
|
|
639
|
-
await inquirer.prompt([{
|
|
640
|
-
type: 'input',
|
|
641
|
-
name: 'continue',
|
|
642
|
-
message: 'Pressione Enter para continuar'
|
|
643
|
-
}]);
|
|
644
|
-
|
|
645
|
-
} catch (error) {
|
|
646
|
-
console.error(chalk.red(` ❌ Erro ao processar Storage: ${error.message}`));
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
// Restaurar Auth Settings (interativo - exibir URL e valores)
|
|
651
|
-
async function restoreAuthSettings(backupPath, targetProject) {
|
|
652
|
-
console.log(chalk.blue('\n🔐 Restaurando Auth Settings...'));
|
|
653
|
-
|
|
654
|
-
try {
|
|
655
|
-
const authSettingsPath = path.join(backupPath, 'auth-settings.json');
|
|
656
|
-
|
|
657
|
-
if (!fs.existsSync(authSettingsPath)) {
|
|
658
|
-
console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Auth encontrada no backup'));
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
const authSettings = JSON.parse(fs.readFileSync(authSettingsPath, 'utf8'));
|
|
663
|
-
const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/auth/url-config`;
|
|
664
|
-
|
|
665
|
-
console.log(chalk.green('\n ✅ URL para configuração manual:'));
|
|
666
|
-
console.log(chalk.cyan(` ${dashboardUrl}`));
|
|
667
|
-
console.log(chalk.yellow('\n 📋 Configure manualmente as seguintes opções:'));
|
|
668
|
-
|
|
669
|
-
if (authSettings.auth_url_config) {
|
|
670
|
-
Object.entries(authSettings.auth_url_config).forEach(([key, value]) => {
|
|
671
|
-
console.log(chalk.gray(` - ${key}: ${value}`));
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
console.log(chalk.yellow('\n ⚠️ Após configurar, pressione Enter para continuar...'));
|
|
676
|
-
|
|
677
|
-
await inquirer.prompt([{
|
|
678
|
-
type: 'input',
|
|
679
|
-
name: 'continue',
|
|
680
|
-
message: 'Pressione Enter para continuar'
|
|
681
|
-
}]);
|
|
682
|
-
|
|
683
|
-
console.log(chalk.green(' ✅ Auth Settings processados'));
|
|
684
|
-
|
|
685
|
-
} catch (error) {
|
|
686
|
-
console.error(chalk.red(` ❌ Erro ao processar Auth Settings: ${error.message}`));
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Restaurar Database Settings (via SQL)
|
|
691
|
-
async function restoreDatabaseSettings(backupPath, targetProject) {
|
|
692
|
-
console.log(chalk.blue('\n🔧 Restaurando Database Settings...'));
|
|
693
|
-
|
|
694
|
-
try {
|
|
695
|
-
const files = fs.readdirSync(backupPath);
|
|
696
|
-
const dbSettingsFile = files.find(f => f.startsWith('database-settings-') && f.endsWith('.json'));
|
|
697
|
-
|
|
698
|
-
if (!dbSettingsFile) {
|
|
699
|
-
console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Database encontrada no backup'));
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const dbSettings = JSON.parse(fs.readFileSync(path.join(backupPath, dbSettingsFile), 'utf8'));
|
|
704
|
-
const { execSync } = require('child_process');
|
|
705
|
-
|
|
706
|
-
if (dbSettings.extensions && dbSettings.extensions.length > 0) {
|
|
707
|
-
console.log(chalk.gray(` - Habilitando ${dbSettings.extensions.length} extension(s)...`));
|
|
708
|
-
|
|
709
|
-
for (const ext of dbSettings.extensions) {
|
|
710
|
-
console.log(chalk.gray(` - ${ext}`));
|
|
711
|
-
|
|
712
|
-
const sqlCommand = `CREATE EXTENSION IF NOT EXISTS ${ext};`;
|
|
713
|
-
|
|
714
|
-
const urlMatch = targetProject.targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
|
|
715
|
-
|
|
716
|
-
if (!urlMatch) {
|
|
717
|
-
console.log(chalk.yellow(` ⚠️ URL inválida para ${ext}`));
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const dockerCmd = [
|
|
722
|
-
'docker run --rm',
|
|
723
|
-
'--network host',
|
|
724
|
-
`-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
|
|
725
|
-
'postgres:17 psql',
|
|
726
|
-
`-d "${targetProject.targetDatabaseUrl}"`,
|
|
727
|
-
`-c "${sqlCommand}"`
|
|
728
|
-
].join(' ');
|
|
729
|
-
|
|
730
|
-
try {
|
|
731
|
-
execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
732
|
-
} catch (sqlError) {
|
|
733
|
-
console.log(chalk.yellow(` ⚠️ ${ext} - extension já existe ou não pode ser habilitada`));
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
console.log(chalk.green(' ✅ Database Settings restaurados com sucesso!'));
|
|
739
|
-
|
|
740
|
-
} catch (error) {
|
|
741
|
-
console.error(chalk.red(` ❌ Erro ao restaurar Database Settings: ${error.message}`));
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Restaurar Realtime Settings (interativo - exibir URL e valores)
|
|
746
|
-
async function restoreRealtimeSettings(backupPath, targetProject) {
|
|
747
|
-
console.log(chalk.blue('\n🔄 Restaurando Realtime Settings...'));
|
|
748
|
-
|
|
749
|
-
try {
|
|
750
|
-
const realtimeSettingsPath = path.join(backupPath, 'realtime-settings.json');
|
|
751
|
-
|
|
752
|
-
if (!fs.existsSync(realtimeSettingsPath)) {
|
|
753
|
-
console.log(chalk.yellow(' ⚠️ Nenhuma configuração de Realtime encontrada no backup'));
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
const realtimeSettings = JSON.parse(fs.readFileSync(realtimeSettingsPath, 'utf8'));
|
|
758
|
-
const dashboardUrl = `https://supabase.com/dashboard/project/${targetProject.targetProjectId}/realtime/settings`;
|
|
759
|
-
|
|
760
|
-
console.log(chalk.green('\n ✅ URL para configuração manual:'));
|
|
761
|
-
console.log(chalk.cyan(` ${dashboardUrl}`));
|
|
762
|
-
console.log(chalk.yellow('\n 📋 Configure manualmente as seguintes opções:'));
|
|
763
|
-
|
|
764
|
-
if (realtimeSettings.realtime_settings?.settings) {
|
|
765
|
-
Object.entries(realtimeSettings.realtime_settings.settings).forEach(([key, setting]) => {
|
|
766
|
-
console.log(chalk.gray(` - ${setting.label}: ${setting.value}`));
|
|
767
|
-
if (setting.description) {
|
|
768
|
-
console.log(chalk.gray(` ${setting.description}`));
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
console.log(chalk.yellow('\n ⚠️ Após configurar, pressione Enter para continuar...'));
|
|
774
|
-
|
|
775
|
-
await inquirer.prompt([{
|
|
776
|
-
type: 'input',
|
|
777
|
-
name: 'continue',
|
|
778
|
-
message: 'Pressione Enter para continuar'
|
|
779
|
-
}]);
|
|
780
|
-
|
|
781
|
-
console.log(chalk.green(' ✅ Realtime Settings processados'));
|
|
782
|
-
|
|
783
|
-
} catch (error) {
|
|
784
|
-
console.error(chalk.red(` ❌ Erro ao processar Realtime Settings: ${error.message}`));
|
|
785
|
-
}
|
|
786
|
-
}
|