smoonb 0.0.47 → 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/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/package.json
CHANGED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { ensureDir, writeJson } = require('../../utils/fsx');
|
|
5
|
+
const { readConfig, validateFor } = require('../../utils/config');
|
|
6
|
+
const { showBetaBanner } = require('../../utils/banner');
|
|
7
|
+
const { getDockerVersion } = require('../../utils/docker');
|
|
8
|
+
const { readEnvFile, writeEnvFile, backupEnvFile } = require('../../utils/env');
|
|
9
|
+
const { saveEnvMap } = require('../../utils/envMap');
|
|
10
|
+
const { mapEnvVariablesInteractively, askComponentsFlags } = require('../../interactive/envMapper');
|
|
11
|
+
|
|
12
|
+
// Importar todas as etapas
|
|
13
|
+
const step00DockerValidation = require('./steps/00-docker-validation');
|
|
14
|
+
const step01Database = require('./steps/01-database');
|
|
15
|
+
const step02DatabaseSeparated = require('./steps/02-database-separated');
|
|
16
|
+
const step03DatabaseSettings = require('./steps/03-database-settings');
|
|
17
|
+
const step04AuthSettings = require('./steps/04-auth-settings');
|
|
18
|
+
const step05RealtimeSettings = require('./steps/05-realtime-settings');
|
|
19
|
+
const step06Storage = require('./steps/06-storage');
|
|
20
|
+
const step07CustomRoles = require('./steps/07-custom-roles');
|
|
21
|
+
const step08EdgeFunctions = require('./steps/08-edge-functions');
|
|
22
|
+
const step09SupabaseTemp = require('./steps/09-supabase-temp');
|
|
23
|
+
const step10Migrations = require('./steps/10-migrations');
|
|
24
|
+
|
|
25
|
+
// Exportar FUNÇÃO em vez de objeto Command
|
|
26
|
+
module.exports = async (options) => {
|
|
27
|
+
showBetaBanner();
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Consentimento para leitura e escrita do .env.local
|
|
31
|
+
console.log(chalk.yellow('⚠️ O smoonb irá ler e escrever o arquivo .env.local localmente.'));
|
|
32
|
+
console.log(chalk.yellow(' Um backup automático do .env.local será criado antes de qualquer alteração.'));
|
|
33
|
+
const consent = await require('inquirer').prompt([{ type: 'confirm', name: 'ok', message: 'Você consente em prosseguir (S/n):', default: true }]);
|
|
34
|
+
if (!consent.ok) {
|
|
35
|
+
console.log(chalk.red('🚫 Operação cancelada pelo usuário.'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Carregar configuração existente apenas para defaults de diretório
|
|
40
|
+
const config = await readConfig().catch(() => ({ backup: { outputDir: './backups' }, supabase: {} }));
|
|
41
|
+
validateFor(config, 'backup');
|
|
42
|
+
|
|
43
|
+
// Pré-passo de ENV: criar diretório de backup com timestamp já no início
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const year = now.getFullYear();
|
|
46
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
47
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
48
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
49
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
50
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
51
|
+
|
|
52
|
+
// Resolver diretório de saída
|
|
53
|
+
const defaultOutput = options.output || config.backup?.outputDir || './backups';
|
|
54
|
+
const backupDir = path.join(defaultOutput, `backup-${year}-${month}-${day}-${hour}-${minute}-${second}`);
|
|
55
|
+
await ensureDir(backupDir);
|
|
56
|
+
|
|
57
|
+
// Backup e mapeamento do .env.local
|
|
58
|
+
const envPath = path.join(process.cwd(), '.env.local');
|
|
59
|
+
const envBackupPath = path.join(backupDir, 'env', '.env.local');
|
|
60
|
+
await ensureDir(path.dirname(envBackupPath));
|
|
61
|
+
await backupEnvFile(envPath, envBackupPath);
|
|
62
|
+
console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
|
|
63
|
+
|
|
64
|
+
const expectedKeys = [
|
|
65
|
+
'NEXT_PUBLIC_SUPABASE_URL',
|
|
66
|
+
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
|
67
|
+
'SUPABASE_SERVICE_ROLE_KEY',
|
|
68
|
+
'SUPABASE_DB_URL',
|
|
69
|
+
'SUPABASE_PROJECT_ID',
|
|
70
|
+
'SUPABASE_ACCESS_TOKEN',
|
|
71
|
+
'SMOONB_OUTPUT_DIR'
|
|
72
|
+
];
|
|
73
|
+
const currentEnv = await readEnvFile(envPath);
|
|
74
|
+
const { finalEnv, dePara } = await mapEnvVariablesInteractively(currentEnv, expectedKeys);
|
|
75
|
+
await writeEnvFile(envPath, finalEnv);
|
|
76
|
+
await saveEnvMap(dePara, path.join(backupDir, 'env', 'env-map.json'));
|
|
77
|
+
console.log(chalk.green('✅ .env.local atualizado com sucesso. Nenhuma chave renomeada; valores sincronizados.'));
|
|
78
|
+
|
|
79
|
+
function getValue(expectedKey) {
|
|
80
|
+
const clientKey = Object.keys(dePara).find(k => dePara[k] === expectedKey);
|
|
81
|
+
return clientKey ? finalEnv[clientKey] : '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Recalcular outputDir a partir do ENV mapeado
|
|
85
|
+
const resolvedOutputDir = options.output || getValue('SMOONB_OUTPUT_DIR') || config.backup?.outputDir || './backups';
|
|
86
|
+
|
|
87
|
+
// Se mudou o outputDir, movemos o backupDir inicial para o novo local mantendo timestamp
|
|
88
|
+
const finalBackupDir = backupDir.startsWith(path.resolve(resolvedOutputDir))
|
|
89
|
+
? backupDir
|
|
90
|
+
: path.join(resolvedOutputDir, path.basename(backupDir));
|
|
91
|
+
if (finalBackupDir !== backupDir) {
|
|
92
|
+
await ensureDir(resolvedOutputDir);
|
|
93
|
+
await fs.rename(backupDir, finalBackupDir);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const projectId = getValue('SUPABASE_PROJECT_ID');
|
|
97
|
+
const accessToken = getValue('SUPABASE_ACCESS_TOKEN');
|
|
98
|
+
const databaseUrl = getValue('SUPABASE_DB_URL');
|
|
99
|
+
|
|
100
|
+
if (!databaseUrl) {
|
|
101
|
+
console.log(chalk.red('❌ DATABASE_URL NÃO CONFIGURADA'));
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
104
|
+
console.log(chalk.yellow(' 1. Configurar SUPABASE_DB_URL no .env.local'));
|
|
105
|
+
console.log(chalk.yellow(' 2. Repetir o comando de backup'));
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(chalk.blue('💡 Exemplo de configuração:'));
|
|
108
|
+
console.log(chalk.gray(' "databaseUrl": "postgresql://postgres:[senha]@db.[projeto].supabase.co:5432/postgres"'));
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(chalk.red('🚫 Backup cancelado - Configuração incompleta'));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!accessToken) {
|
|
115
|
+
console.log(chalk.red('❌ ACCESS_TOKEN NÃO CONFIGURADO'));
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(chalk.yellow('📋 Para fazer backup completo do Supabase, você precisa:'));
|
|
118
|
+
console.log(chalk.yellow(' 1. Obter Personal Access Token do Supabase'));
|
|
119
|
+
console.log(chalk.yellow(' 2. Configurar SUPABASE_ACCESS_TOKEN no .env.local'));
|
|
120
|
+
console.log(chalk.yellow(' 3. Repetir o comando de backup'));
|
|
121
|
+
console.log('');
|
|
122
|
+
console.log(chalk.blue('🔗 Como obter o token:'));
|
|
123
|
+
console.log(chalk.gray(' 1. Acesse: https://supabase.com/dashboard/account/tokens'));
|
|
124
|
+
console.log(chalk.gray(' 2. Clique: "Generate new token"'));
|
|
125
|
+
console.log(chalk.gray(' 3. Copie o token (formato: sbp_...)'));
|
|
126
|
+
console.log('');
|
|
127
|
+
console.log(chalk.red('🚫 Backup cancelado - Token não configurado'));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(chalk.blue(`🚀 Iniciando backup do projeto: ${projectId}`));
|
|
132
|
+
|
|
133
|
+
// Executar validação Docker (etapa 0)
|
|
134
|
+
await step00DockerValidation();
|
|
135
|
+
|
|
136
|
+
// Flags de componentes
|
|
137
|
+
const flags = await askComponentsFlags();
|
|
138
|
+
|
|
139
|
+
// Criar contexto compartilhado para as etapas
|
|
140
|
+
const context = {
|
|
141
|
+
projectId,
|
|
142
|
+
accessToken,
|
|
143
|
+
databaseUrl,
|
|
144
|
+
backupDir: finalBackupDir,
|
|
145
|
+
outputDir: resolvedOutputDir,
|
|
146
|
+
options: { ...options, flags }
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Criar manifest
|
|
150
|
+
const manifest = {
|
|
151
|
+
created_at: new Date().toISOString(),
|
|
152
|
+
project_id: projectId,
|
|
153
|
+
smoonb_version: require('../../../package.json').version,
|
|
154
|
+
backup_type: 'pg_dumpall_docker_dashboard_compatible',
|
|
155
|
+
docker_version: await getDockerVersion(),
|
|
156
|
+
dashboard_compatible: true,
|
|
157
|
+
components: {}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Executar todas as etapas na ordem
|
|
161
|
+
console.log(chalk.blue(`📁 Diretório: ${finalBackupDir}`));
|
|
162
|
+
console.log(chalk.gray(`🐳 Backup via Docker Desktop`));
|
|
163
|
+
|
|
164
|
+
// 1. Backup Database via pg_dumpall Docker
|
|
165
|
+
console.log(chalk.blue('\n📊 1/12 - Backup da Database PostgreSQL via pg_dumpall Docker...'));
|
|
166
|
+
const databaseResult = await step01Database(context);
|
|
167
|
+
manifest.components.database = databaseResult;
|
|
168
|
+
|
|
169
|
+
// 2. Backup Database Separado
|
|
170
|
+
console.log(chalk.blue('\n📊 2/12 - Backup da Database PostgreSQL (arquivos SQL separados)...'));
|
|
171
|
+
const dbSeparatedResult = await step02DatabaseSeparated(context);
|
|
172
|
+
manifest.components.database_separated = {
|
|
173
|
+
success: dbSeparatedResult.success,
|
|
174
|
+
method: 'supabase-cli',
|
|
175
|
+
files: dbSeparatedResult.files || [],
|
|
176
|
+
total_size_kb: dbSeparatedResult.totalSizeKB || '0.0'
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// 3. Backup Database Settings
|
|
180
|
+
console.log(chalk.blue('\n🔧 3/12 - Backup das Database Extensions and Settings via SQL...'));
|
|
181
|
+
const databaseSettingsResult = await step03DatabaseSettings(context);
|
|
182
|
+
manifest.components.database_settings = databaseSettingsResult;
|
|
183
|
+
|
|
184
|
+
// 4. Backup Auth Settings
|
|
185
|
+
if (flags?.includeAuth) {
|
|
186
|
+
console.log(chalk.blue('\n🔐 4/12 - Backup das Auth Settings via API...'));
|
|
187
|
+
const authResult = await step04AuthSettings(context);
|
|
188
|
+
manifest.components.auth_settings = authResult;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 5. Backup Realtime Settings
|
|
192
|
+
if (flags?.includeRealtime) {
|
|
193
|
+
console.log(chalk.blue('\n🔄 5/12 - Backup das Realtime Settings via Captura Interativa...'));
|
|
194
|
+
const realtimeResult = await step05RealtimeSettings(context);
|
|
195
|
+
manifest.components.realtime = realtimeResult;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 6. Backup Storage
|
|
199
|
+
if (flags?.includeStorage) {
|
|
200
|
+
console.log(chalk.blue('\n📦 6/12 - Backup do Storage via API...'));
|
|
201
|
+
const storageResult = await step06Storage(context);
|
|
202
|
+
manifest.components.storage = storageResult;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 7. Backup Custom Roles
|
|
206
|
+
console.log(chalk.blue('\n👥 7/12 - Backup dos Custom Roles via SQL...'));
|
|
207
|
+
const rolesResult = await step07CustomRoles(context);
|
|
208
|
+
manifest.components.custom_roles = rolesResult;
|
|
209
|
+
|
|
210
|
+
// 8. Backup Edge Functions
|
|
211
|
+
if (flags?.includeFunctions) {
|
|
212
|
+
console.log(chalk.blue('\n⚡ 8/12 - Backup das Edge Functions via Docker...'));
|
|
213
|
+
const functionsResult = await step08EdgeFunctions(context);
|
|
214
|
+
manifest.components.edge_functions = functionsResult;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 9. Backup Supabase .temp
|
|
218
|
+
console.log(chalk.blue('\n📁 9/12 - Backup do Supabase .temp...'));
|
|
219
|
+
const supabaseTempResult = await step09SupabaseTemp(context);
|
|
220
|
+
manifest.components.supabase_temp = supabaseTempResult;
|
|
221
|
+
|
|
222
|
+
// 10. Backup Migrations
|
|
223
|
+
console.log(chalk.blue('\n📋 10/12 - Backup das Migrations...'));
|
|
224
|
+
const migrationsResult = await step10Migrations(context);
|
|
225
|
+
manifest.components.migrations = migrationsResult;
|
|
226
|
+
|
|
227
|
+
// Salvar manifest
|
|
228
|
+
await writeJson(path.join(finalBackupDir, 'backup-manifest.json'), manifest);
|
|
229
|
+
|
|
230
|
+
// Exibir resumo final
|
|
231
|
+
console.log(chalk.green('\n🎉 BACKUP COMPLETO FINALIZADO VIA DOCKER!'));
|
|
232
|
+
console.log(chalk.blue(`📁 Localização: ${finalBackupDir}`));
|
|
233
|
+
console.log(chalk.green(`📊 Database: ${databaseResult.fileName} (${databaseResult.size} KB) - Idêntico ao Dashboard`));
|
|
234
|
+
console.log(chalk.green(`📊 Database SQL: ${dbSeparatedResult.files?.length || 0} arquivos separados (${dbSeparatedResult.totalSizeKB} KB) - Para troubleshooting`));
|
|
235
|
+
console.log(chalk.green(`🔧 Database Settings: ${databaseSettingsResult.fileName} (${databaseSettingsResult.size} KB) - Extensions e Configurações`));
|
|
236
|
+
|
|
237
|
+
if (flags?.includeFunctions && manifest.components.edge_functions) {
|
|
238
|
+
const functionsResult = manifest.components.edge_functions;
|
|
239
|
+
console.log(chalk.green(`⚡ Edge Functions: ${functionsResult.success_count || 0}/${functionsResult.functions_count || 0} functions baixadas via Docker`));
|
|
240
|
+
}
|
|
241
|
+
if (flags?.includeAuth && manifest.components.auth_settings) {
|
|
242
|
+
const authResult = manifest.components.auth_settings;
|
|
243
|
+
console.log(chalk.green(`🔐 Auth Settings: ${authResult.success ? 'Exportadas via API' : 'Falharam'}`));
|
|
244
|
+
}
|
|
245
|
+
if (flags?.includeStorage && manifest.components.storage) {
|
|
246
|
+
const storageResult = manifest.components.storage;
|
|
247
|
+
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API`));
|
|
248
|
+
}
|
|
249
|
+
console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
|
|
250
|
+
|
|
251
|
+
// Determinar mensagem correta baseada no método usado
|
|
252
|
+
if (flags?.includeRealtime && manifest.components.realtime) {
|
|
253
|
+
const realtimeResult = manifest.components.realtime;
|
|
254
|
+
let realtimeMessage = 'Falharam';
|
|
255
|
+
if (realtimeResult.success) {
|
|
256
|
+
if (options.skipRealtime) {
|
|
257
|
+
realtimeMessage = 'Configurações copiadas do backup anterior';
|
|
258
|
+
} else {
|
|
259
|
+
realtimeMessage = 'Configurações capturadas interativamente';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
console.log(chalk.green(`🔄 Realtime: ${realtimeMessage}`));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// report.json
|
|
266
|
+
await writeJson(path.join(finalBackupDir, 'report.json'), {
|
|
267
|
+
process: 'backup',
|
|
268
|
+
created_at: manifest.created_at,
|
|
269
|
+
project_id: manifest.project_id,
|
|
270
|
+
assets: {
|
|
271
|
+
env: path.join(finalBackupDir, 'env', '.env.local'),
|
|
272
|
+
env_map: path.join(finalBackupDir, 'env', 'env-map.json'),
|
|
273
|
+
manifest: path.join(finalBackupDir, 'backup-manifest.json')
|
|
274
|
+
},
|
|
275
|
+
components: {
|
|
276
|
+
includeFunctions: !!flags?.includeFunctions,
|
|
277
|
+
includeStorage: !!flags?.includeStorage,
|
|
278
|
+
includeAuth: !!flags?.includeAuth,
|
|
279
|
+
includeRealtime: !!flags?.includeRealtime
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
return { success: true, backupDir: finalBackupDir, manifest };
|
|
284
|
+
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error(chalk.red(`❌ Erro no backup: ${error.message}`));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { canPerformCompleteBackup } = require('../../../utils/docker');
|
|
3
|
+
const { showDockerMessagesAndExit } = require('../utils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Etapa 0: Validação Docker
|
|
7
|
+
* Deve ocorrer antes de tudo
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async () => {
|
|
10
|
+
console.log(chalk.blue('\n🐳 0/12 - Validação Docker...'));
|
|
11
|
+
console.log(chalk.gray('🔍 Verificando dependências Docker...'));
|
|
12
|
+
|
|
13
|
+
const backupCapability = await canPerformCompleteBackup();
|
|
14
|
+
|
|
15
|
+
if (!backupCapability.canBackupComplete) {
|
|
16
|
+
showDockerMessagesAndExit(backupCapability.reason);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(chalk.green('✅ Docker Desktop detectado e funcionando'));
|
|
20
|
+
console.log(chalk.gray(`🐳 Versão: ${backupCapability.dockerStatus.version}`));
|
|
21
|
+
|
|
22
|
+
return { success: true };
|
|
23
|
+
};
|
|
24
|
+
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 1: Backup Database via pg_dumpall Docker (idêntico ao Dashboard)
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ databaseUrl, backupDir }) => {
|
|
10
|
+
try {
|
|
11
|
+
console.log(chalk.gray(' - Criando backup completo via pg_dumpall...'));
|
|
12
|
+
|
|
13
|
+
// Extrair credenciais da databaseUrl
|
|
14
|
+
const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
15
|
+
|
|
16
|
+
if (!urlMatch) {
|
|
17
|
+
throw new Error('Database URL inválida');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const [, username, password, host, port] = urlMatch;
|
|
21
|
+
|
|
22
|
+
// Gerar nome do arquivo igual ao dashboard
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
25
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
26
|
+
const year = now.getFullYear();
|
|
27
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
28
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
29
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
30
|
+
|
|
31
|
+
const fileName = `db_cluster-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.backup`;
|
|
32
|
+
|
|
33
|
+
// Usar caminho absoluto igual às Edge Functions
|
|
34
|
+
const backupDirAbs = path.resolve(backupDir);
|
|
35
|
+
|
|
36
|
+
// Comando pg_dumpall via Docker
|
|
37
|
+
const dockerCmd = [
|
|
38
|
+
'docker run --rm --network host',
|
|
39
|
+
`-v "${backupDirAbs}:/host"`,
|
|
40
|
+
`-e PGPASSWORD="${password}"`,
|
|
41
|
+
'postgres:17 pg_dumpall',
|
|
42
|
+
`-h ${host}`,
|
|
43
|
+
`-p ${port}`,
|
|
44
|
+
`-U ${username}`,
|
|
45
|
+
`-f /host/${fileName}`
|
|
46
|
+
].join(' ');
|
|
47
|
+
|
|
48
|
+
console.log(chalk.gray(' - Executando pg_dumpall via Docker...'));
|
|
49
|
+
execSync(dockerCmd, { stdio: 'pipe' });
|
|
50
|
+
|
|
51
|
+
// Compactar igual ao Supabase Dashboard
|
|
52
|
+
const gzipCmd = [
|
|
53
|
+
'docker run --rm',
|
|
54
|
+
`-v "${backupDirAbs}:/host"`,
|
|
55
|
+
`postgres:17 gzip /host/${fileName}`
|
|
56
|
+
].join(' ');
|
|
57
|
+
|
|
58
|
+
execSync(gzipCmd, { stdio: 'pipe' });
|
|
59
|
+
|
|
60
|
+
const finalFileName = `${fileName}.gz`;
|
|
61
|
+
const stats = await fs.stat(path.join(backupDir, finalFileName));
|
|
62
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
63
|
+
|
|
64
|
+
console.log(chalk.green(` ✅ Database backup: ${finalFileName} (${sizeKB} KB)`));
|
|
65
|
+
|
|
66
|
+
return { success: true, size: sizeKB, fileName: finalFileName };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.log(chalk.yellow(` ⚠️ Erro no backup do database: ${error.message}`));
|
|
69
|
+
return { success: false };
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 2: Backup Database Separado (SQL files para troubleshooting)
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ databaseUrl, backupDir, accessToken }) => {
|
|
10
|
+
try {
|
|
11
|
+
console.log(chalk.gray(' - Criando backups SQL separados via Supabase CLI...'));
|
|
12
|
+
|
|
13
|
+
const dbUrl = databaseUrl;
|
|
14
|
+
const files = [];
|
|
15
|
+
let totalSizeKB = 0;
|
|
16
|
+
|
|
17
|
+
// 1. Backup do Schema
|
|
18
|
+
console.log(chalk.gray(' - Exportando schema...'));
|
|
19
|
+
const schemaFile = path.join(backupDir, 'schema.sql');
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
execSync(`supabase db dump --db-url "${dbUrl}" -f "${schemaFile}"`, {
|
|
23
|
+
stdio: 'pipe',
|
|
24
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' }
|
|
25
|
+
});
|
|
26
|
+
const stats = await fs.stat(schemaFile);
|
|
27
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
28
|
+
files.push({ filename: 'schema.sql', sizeKB });
|
|
29
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
30
|
+
console.log(chalk.green(` ✅ Schema: ${sizeKB} KB`));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.log(chalk.yellow(` ⚠️ Erro no schema: ${error.message}`));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Backup dos Dados
|
|
36
|
+
console.log(chalk.gray(' - Exportando dados...'));
|
|
37
|
+
const dataFile = path.join(backupDir, 'data.sql');
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
execSync(`supabase db dump --db-url "${dbUrl}" --data-only -f "${dataFile}"`, {
|
|
41
|
+
stdio: 'pipe',
|
|
42
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' }
|
|
43
|
+
});
|
|
44
|
+
const stats = await fs.stat(dataFile);
|
|
45
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
46
|
+
files.push({ filename: 'data.sql', sizeKB });
|
|
47
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
48
|
+
console.log(chalk.green(` ✅ Data: ${sizeKB} KB`));
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.log(chalk.yellow(` ⚠️ Erro nos dados: ${error.message}`));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Backup dos Roles
|
|
54
|
+
console.log(chalk.gray(' - Exportando roles...'));
|
|
55
|
+
const rolesFile = path.join(backupDir, 'roles.sql');
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
execSync(`supabase db dump --db-url "${dbUrl}" --role-only -f "${rolesFile}"`, {
|
|
59
|
+
stdio: 'pipe',
|
|
60
|
+
env: { ...process.env, SUPABASE_ACCESS_TOKEN: accessToken || '' }
|
|
61
|
+
});
|
|
62
|
+
const stats = await fs.stat(rolesFile);
|
|
63
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
64
|
+
files.push({ filename: 'roles.sql', sizeKB });
|
|
65
|
+
totalSizeKB += parseFloat(sizeKB);
|
|
66
|
+
console.log(chalk.green(` ✅ Roles: ${sizeKB} KB`));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.log(chalk.yellow(` ⚠️ Erro nos roles: ${error.message}`));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: files.length > 0,
|
|
73
|
+
files,
|
|
74
|
+
totalSizeKB: totalSizeKB.toFixed(1)
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.log(chalk.yellow(` ⚠️ Erro nos backups SQL separados: ${error.message}`));
|
|
79
|
+
return { success: false, files: [], totalSizeKB: '0.0' };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Etapa 3: Backup Database Extensions and Settings via SQL
|
|
8
|
+
*/
|
|
9
|
+
module.exports = async ({ databaseUrl, projectId, backupDir }) => {
|
|
10
|
+
try {
|
|
11
|
+
console.log(chalk.gray(' - Capturando Database Extensions and Settings...'));
|
|
12
|
+
|
|
13
|
+
// Extrair credenciais da databaseUrl
|
|
14
|
+
const urlMatch = databaseUrl.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
|
|
15
|
+
|
|
16
|
+
if (!urlMatch) {
|
|
17
|
+
throw new Error('Database URL inválida');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const [, username, password, host, port, database] = urlMatch;
|
|
21
|
+
|
|
22
|
+
// Gerar nome do arquivo
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
25
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
26
|
+
const year = now.getFullYear();
|
|
27
|
+
const hours = String(now.getHours()).padStart(2, '0');
|
|
28
|
+
const minutes = String(now.getMinutes()).padStart(2, '0');
|
|
29
|
+
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
30
|
+
|
|
31
|
+
const fileName = `database-settings-${day}-${month}-${year}@${hours}-${minutes}-${seconds}.json`;
|
|
32
|
+
|
|
33
|
+
// Usar caminho absoluto igual às outras funções
|
|
34
|
+
const backupDirAbs = path.resolve(backupDir);
|
|
35
|
+
|
|
36
|
+
// Script SQL para capturar todas as configurações
|
|
37
|
+
const sqlScript = `
|
|
38
|
+
-- Database Extensions and Settings Backup
|
|
39
|
+
-- Generated at: ${new Date().toISOString()}
|
|
40
|
+
|
|
41
|
+
-- 1. Capturar extensões instaladas
|
|
42
|
+
SELECT json_agg(
|
|
43
|
+
json_build_object(
|
|
44
|
+
'name', extname,
|
|
45
|
+
'version', extversion,
|
|
46
|
+
'schema', extnamespace::regnamespace
|
|
47
|
+
)
|
|
48
|
+
) as extensions
|
|
49
|
+
FROM pg_extension;
|
|
50
|
+
|
|
51
|
+
-- 2. Capturar configurações PostgreSQL importantes
|
|
52
|
+
SELECT json_agg(
|
|
53
|
+
json_build_object(
|
|
54
|
+
'name', name,
|
|
55
|
+
'setting', setting,
|
|
56
|
+
'unit', unit,
|
|
57
|
+
'context', context,
|
|
58
|
+
'description', short_desc
|
|
59
|
+
)
|
|
60
|
+
) as postgres_settings
|
|
61
|
+
FROM pg_settings
|
|
62
|
+
WHERE name IN (
|
|
63
|
+
'statement_timeout',
|
|
64
|
+
'idle_in_transaction_session_timeout',
|
|
65
|
+
'lock_timeout',
|
|
66
|
+
'shared_buffers',
|
|
67
|
+
'work_mem',
|
|
68
|
+
'maintenance_work_mem',
|
|
69
|
+
'effective_cache_size',
|
|
70
|
+
'max_connections',
|
|
71
|
+
'log_statement',
|
|
72
|
+
'log_min_duration_statement',
|
|
73
|
+
'timezone',
|
|
74
|
+
'log_timezone',
|
|
75
|
+
'default_transaction_isolation',
|
|
76
|
+
'default_transaction_read_only',
|
|
77
|
+
'checkpoint_completion_target',
|
|
78
|
+
'wal_buffers',
|
|
79
|
+
'max_wal_size',
|
|
80
|
+
'min_wal_size'
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
-- 3. Capturar configurações específicas dos roles Supabase
|
|
84
|
+
SELECT json_agg(
|
|
85
|
+
json_build_object(
|
|
86
|
+
'role', rolname,
|
|
87
|
+
'config', rolconfig
|
|
88
|
+
)
|
|
89
|
+
) as role_configurations
|
|
90
|
+
FROM pg_roles
|
|
91
|
+
WHERE rolname IN ('anon', 'authenticated', 'authenticator', 'postgres', 'service_role')
|
|
92
|
+
AND rolconfig IS NOT NULL;
|
|
93
|
+
|
|
94
|
+
-- 4. Capturar configurações de PGAudit (se existir)
|
|
95
|
+
SELECT json_agg(
|
|
96
|
+
json_build_object(
|
|
97
|
+
'role', rolname,
|
|
98
|
+
'config', rolconfig
|
|
99
|
+
)
|
|
100
|
+
) as pgaudit_configurations
|
|
101
|
+
FROM pg_roles
|
|
102
|
+
WHERE rolconfig IS NOT NULL
|
|
103
|
+
AND EXISTS (
|
|
104
|
+
SELECT 1 FROM unnest(rolconfig) AS config
|
|
105
|
+
WHERE config LIKE '%pgaudit%'
|
|
106
|
+
);
|
|
107
|
+
`;
|
|
108
|
+
|
|
109
|
+
// Salvar script SQL temporário
|
|
110
|
+
const sqlFile = path.join(backupDir, 'temp_settings.sql');
|
|
111
|
+
await fs.writeFile(sqlFile, sqlScript);
|
|
112
|
+
|
|
113
|
+
// Executar via Docker
|
|
114
|
+
const dockerCmd = [
|
|
115
|
+
'docker run --rm --network host',
|
|
116
|
+
`-v "${backupDirAbs}:/host"`,
|
|
117
|
+
`-e PGPASSWORD="${password}"`,
|
|
118
|
+
'postgres:17 psql',
|
|
119
|
+
`-h ${host}`,
|
|
120
|
+
`-p ${port}`,
|
|
121
|
+
`-U ${username}`,
|
|
122
|
+
`-d ${database}`,
|
|
123
|
+
'-f /host/temp_settings.sql',
|
|
124
|
+
'-t', // Tuples only
|
|
125
|
+
'-A' // Unaligned output
|
|
126
|
+
].join(' ');
|
|
127
|
+
|
|
128
|
+
console.log(chalk.gray(' - Executando queries de configurações via Docker...'));
|
|
129
|
+
const output = execSync(dockerCmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
130
|
+
|
|
131
|
+
// Processar output e criar JSON estruturado
|
|
132
|
+
const lines = output.trim().split('\n').filter(line => line.trim());
|
|
133
|
+
|
|
134
|
+
const result = {
|
|
135
|
+
database_settings: {
|
|
136
|
+
note: "Configurações específicas do database Supabase capturadas via SQL",
|
|
137
|
+
captured_at: new Date().toISOString(),
|
|
138
|
+
project_id: projectId,
|
|
139
|
+
extensions: lines[0] ? JSON.parse(lines[0]) : [],
|
|
140
|
+
postgres_settings: lines[1] ? JSON.parse(lines[1]) : [],
|
|
141
|
+
role_configurations: lines[2] ? JSON.parse(lines[2]) : [],
|
|
142
|
+
pgaudit_configurations: lines[3] ? JSON.parse(lines[3]) : [],
|
|
143
|
+
restore_instructions: {
|
|
144
|
+
note: "Estas configurações precisam ser aplicadas manualmente após a restauração do database",
|
|
145
|
+
steps: [
|
|
146
|
+
"1. Restaurar o database usando o arquivo .backup.gz",
|
|
147
|
+
"2. Aplicar configurações de Postgres via SQL:",
|
|
148
|
+
" ALTER DATABASE postgres SET setting_name TO 'value';",
|
|
149
|
+
"3. Aplicar configurações de roles via SQL:",
|
|
150
|
+
" ALTER ROLE role_name SET setting_name TO 'value';",
|
|
151
|
+
"4. Habilitar extensões necessárias via Dashboard ou SQL:",
|
|
152
|
+
" CREATE EXTENSION IF NOT EXISTS extension_name;",
|
|
153
|
+
"5. Verificar configurações aplicadas:",
|
|
154
|
+
" SELECT name, setting FROM pg_settings WHERE name IN (...);"
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Salvar arquivo JSON
|
|
161
|
+
const jsonFile = path.join(backupDir, fileName);
|
|
162
|
+
await fs.writeFile(jsonFile, JSON.stringify(result, null, 2));
|
|
163
|
+
|
|
164
|
+
// Limpar arquivo temporário
|
|
165
|
+
await fs.unlink(sqlFile);
|
|
166
|
+
|
|
167
|
+
const stats = await fs.stat(jsonFile);
|
|
168
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
169
|
+
|
|
170
|
+
console.log(chalk.green(` ✅ Database Settings: ${fileName} (${sizeKB} KB)`));
|
|
171
|
+
|
|
172
|
+
return { success: true, size: sizeKB, fileName: fileName };
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.log(chalk.yellow(` ⚠️ Erro no backup das Database Settings: ${error.message}`));
|
|
175
|
+
return { success: false };
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|