smoonb 0.0.73 → 0.0.75
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 +62 -65
- package/src/commands/backup/steps/00-docker-validation.js +6 -4
- package/src/commands/backup/steps/01-database.js +6 -3
- package/src/commands/backup/steps/02-database-separated.js +11 -8
- package/src/commands/backup/steps/03-database-settings.js +6 -3
- package/src/commands/backup/steps/04-auth-settings.js +6 -3
- package/src/commands/backup/steps/05-realtime-settings.js +5 -2
- package/src/commands/backup/steps/06-storage.js +28 -25
- package/src/commands/backup/steps/07-custom-roles.js +6 -3
- package/src/commands/backup/steps/08-edge-functions.js +14 -10
- package/src/commands/backup/steps/09-supabase-temp.js +9 -6
- package/src/commands/backup/steps/10-migrations.js +13 -9
- package/src/commands/restore/steps/00-backup-selection.js +4 -2
- package/src/commands/restore/steps/01-components-selection.js +30 -28
- package/src/commands/restore/steps/03-database.js +19 -16
- package/src/commands/restore/steps/04-edge-functions.js +16 -13
- package/src/commands/restore/steps/05-auth-settings.js +9 -6
- package/src/commands/restore/steps/06-storage.js +47 -40
- package/src/commands/restore/steps/07-database-settings.js +10 -7
- package/src/commands/restore/steps/08-realtime-settings.js +9 -6
- package/src/i18n/locales/en.json +328 -1
- package/src/i18n/locales/pt-BR.json +327 -1
- package/src/interactive/envMapper.js +30 -25
|
@@ -5,6 +5,7 @@ const AdmZip = require('adm-zip');
|
|
|
5
5
|
const { createClient } = require('@supabase/supabase-js');
|
|
6
6
|
const { ensureDir, writeJson } = require('../../../utils/fsx');
|
|
7
7
|
const { confirm } = require('../../../utils/prompt');
|
|
8
|
+
const { t } = require('../../../i18n');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Etapa 6: Backup Storage via Supabase API
|
|
@@ -12,10 +13,11 @@ const { confirm } = require('../../../utils/prompt');
|
|
|
12
13
|
*/
|
|
13
14
|
module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supabaseServiceKey }) => {
|
|
14
15
|
try {
|
|
16
|
+
const getT = global.smoonbI18n?.t || t;
|
|
15
17
|
const storageDir = path.join(backupDir, 'storage');
|
|
16
18
|
await ensureDir(storageDir);
|
|
17
19
|
|
|
18
|
-
console.log(chalk.white(
|
|
20
|
+
console.log(chalk.white(` - ${getT('backup.steps.storage.listing')}`));
|
|
19
21
|
|
|
20
22
|
// Usar fetch direto para Management API com Personal Access Token
|
|
21
23
|
const storageResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets`, {
|
|
@@ -26,25 +28,25 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
if (!storageResponse.ok) {
|
|
29
|
-
console.log(chalk.yellow(` ⚠️
|
|
31
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.listBucketsError', { status: storageResponse.status, statusText: storageResponse.statusText })}`));
|
|
30
32
|
return { success: false, buckets: [] };
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const buckets = await storageResponse.json();
|
|
34
36
|
|
|
35
37
|
if (!buckets || buckets.length === 0) {
|
|
36
|
-
console.log(chalk.white(
|
|
38
|
+
console.log(chalk.white(` - ${getT('backup.steps.storage.noBuckets')}`));
|
|
37
39
|
await writeJson(path.join(storageDir, 'README.md'), {
|
|
38
40
|
message: 'Nenhum bucket de Storage encontrado neste projeto'
|
|
39
41
|
});
|
|
40
42
|
return { success: true, buckets: [] };
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
console.log(chalk.white(` -
|
|
45
|
+
console.log(chalk.white(` - ${getT('backup.steps.storage.found', { count: buckets.length })}`));
|
|
44
46
|
|
|
45
47
|
// Validar credenciais do Supabase para download de arquivos
|
|
46
48
|
if (!supabaseUrl || !supabaseServiceKey) {
|
|
47
|
-
console.log(chalk.yellow(
|
|
49
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.credentialsNotAvailable')}`));
|
|
48
50
|
return await backupMetadataOnly(buckets, storageDir, projectId, accessToken);
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -64,7 +66,7 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
64
66
|
|
|
65
67
|
for (const bucket of buckets || []) {
|
|
66
68
|
try {
|
|
67
|
-
console.log(chalk.white(` -
|
|
69
|
+
console.log(chalk.white(` - ${getT('backup.steps.storage.processing', { bucketName: bucket.name })}`));
|
|
68
70
|
|
|
69
71
|
// Listar objetos do bucket via Management API com Personal Access Token
|
|
70
72
|
const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, {
|
|
@@ -97,12 +99,12 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
97
99
|
await ensureDir(bucketDir);
|
|
98
100
|
|
|
99
101
|
// Listar todos os arquivos recursivamente usando Supabase client
|
|
100
|
-
console.log(chalk.white(` -
|
|
102
|
+
console.log(chalk.white(` - ${getT('backup.steps.storage.listingFiles', { bucketName: bucket.name })}`));
|
|
101
103
|
const allFiles = await listAllFilesRecursively(supabase, bucket.name, '');
|
|
102
104
|
|
|
103
105
|
let filesDownloaded = 0;
|
|
104
106
|
if (allFiles.length > 0) {
|
|
105
|
-
console.log(chalk.white(` -
|
|
107
|
+
console.log(chalk.white(` - ${getT('backup.steps.storage.downloading', { count: allFiles.length, bucketName: bucket.name })}`));
|
|
106
108
|
|
|
107
109
|
for (const filePath of allFiles) {
|
|
108
110
|
try {
|
|
@@ -112,7 +114,7 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
112
114
|
.download(filePath);
|
|
113
115
|
|
|
114
116
|
if (downloadError) {
|
|
115
|
-
console.log(chalk.yellow(` ⚠️
|
|
117
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.downloadError', { path: filePath, message: downloadError.message })}`));
|
|
116
118
|
continue;
|
|
117
119
|
}
|
|
118
120
|
|
|
@@ -129,10 +131,10 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
129
131
|
|
|
130
132
|
// Mostrar progresso a cada 10 arquivos ou se for o último
|
|
131
133
|
if (filesDownloaded % 10 === 0 || filesDownloaded === allFiles.length) {
|
|
132
|
-
console.log(chalk.white(` -
|
|
134
|
+
console.log(chalk.white(` - ${getT('backup.steps.storage.downloaded', { current: filesDownloaded, total: allFiles.length })}`));
|
|
133
135
|
}
|
|
134
136
|
} catch (fileError) {
|
|
135
|
-
console.log(chalk.yellow(` ⚠️
|
|
137
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.processFileError', { path: filePath, message: fileError.message })}`));
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
}
|
|
@@ -145,14 +147,14 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
145
147
|
totalFiles: allFiles.length
|
|
146
148
|
});
|
|
147
149
|
|
|
148
|
-
console.log(chalk.green(` ✅
|
|
150
|
+
console.log(chalk.green(` ✅ ${getT('backup.steps.storage.bucketDone', { bucketName: bucket.name, downloaded: filesDownloaded, total: allFiles.length })}`));
|
|
149
151
|
} catch (error) {
|
|
150
|
-
console.log(chalk.yellow(` ⚠️
|
|
152
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.processBucketError', { bucketName: bucket.name, message: error.message })}`));
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
// Criar ZIP no padrão do Dashboard: {project-id}.storage.zip
|
|
155
|
-
console.log(chalk.white(
|
|
157
|
+
console.log(chalk.white(`\n - ${getT('backup.steps.storage.creatingZip')}`));
|
|
156
158
|
const zipFileName = `${projectId}.storage.zip`;
|
|
157
159
|
const zipFilePath = path.join(backupDir, zipFileName);
|
|
158
160
|
|
|
@@ -167,25 +169,24 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
167
169
|
const zipStats = await fs.stat(zipFilePath);
|
|
168
170
|
const zipSizeMB = (zipStats.size / (1024 * 1024)).toFixed(2);
|
|
169
171
|
|
|
170
|
-
console.log(chalk.green(` ✅
|
|
172
|
+
console.log(chalk.green(` ✅ ${getT('backup.steps.storage.zipCreated', { fileName: zipFileName, size: zipSizeMB })}`));
|
|
171
173
|
|
|
172
174
|
// Perguntar ao usuário se deseja limpar a estrutura temporária
|
|
173
|
-
const
|
|
174
|
-
const shouldCleanup = await confirm(` Deseja limpar ${tempDirName} após o backup`, false);
|
|
175
|
+
const shouldCleanup = await confirm(` ${getT('backup.steps.storage.cleanup')}`, false);
|
|
175
176
|
|
|
176
177
|
if (shouldCleanup) {
|
|
177
|
-
console.log(chalk.white(` -
|
|
178
|
+
console.log(chalk.white(` - ${getT('backup.steps.storage.cleanupRemoving')}`));
|
|
178
179
|
try {
|
|
179
180
|
await fs.rm(tempStorageDir, { recursive: true, force: true });
|
|
180
|
-
console.log(chalk.green(` ✅
|
|
181
|
+
console.log(chalk.green(` ✅ ${getT('backup.steps.storage.cleanupRemoved')}`));
|
|
181
182
|
} catch (cleanupError) {
|
|
182
|
-
console.log(chalk.yellow(` ⚠️
|
|
183
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.cleanupError', { message: cleanupError.message })}`));
|
|
183
184
|
}
|
|
184
185
|
} else {
|
|
185
|
-
console.log(chalk.white(` ℹ️
|
|
186
|
+
console.log(chalk.white(` ℹ️ ${getT('backup.steps.storage.tempKept', { path: path.relative(process.cwd(), tempStorageDir) })}`));
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
console.log(chalk.green(`✅
|
|
189
|
+
console.log(chalk.green(`✅ ${getT('backup.steps.storage.done', { buckets: processedBuckets.length, files: totalFilesDownloaded })}`));
|
|
189
190
|
return {
|
|
190
191
|
success: true,
|
|
191
192
|
buckets: processedBuckets,
|
|
@@ -195,7 +196,8 @@ module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supaba
|
|
|
195
196
|
tempDirCleaned: shouldCleanup
|
|
196
197
|
};
|
|
197
198
|
} catch (error) {
|
|
198
|
-
|
|
199
|
+
const getT = global.smoonbI18n?.t || t;
|
|
200
|
+
console.log(chalk.yellow(`⚠️ ${getT('backup.steps.storage.error', { message: error.message })}`));
|
|
199
201
|
return { success: false, buckets: [] };
|
|
200
202
|
}
|
|
201
203
|
};
|
|
@@ -254,6 +256,7 @@ async function backupMetadataOnly(buckets, storageDir, projectId, accessToken) {
|
|
|
254
256
|
*/
|
|
255
257
|
async function listAllFilesRecursively(supabase, bucketName, folderPath = '') {
|
|
256
258
|
const allFiles = [];
|
|
259
|
+
const getT = global.smoonbI18n?.t || t;
|
|
257
260
|
|
|
258
261
|
try {
|
|
259
262
|
// Listar arquivos e pastas no caminho atual
|
|
@@ -265,7 +268,7 @@ async function listAllFilesRecursively(supabase, bucketName, folderPath = '') {
|
|
|
265
268
|
});
|
|
266
269
|
|
|
267
270
|
if (error) {
|
|
268
|
-
console.log(chalk.yellow(` ⚠️
|
|
271
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.listError', { path: folderPath || 'raiz', message: error.message })}`));
|
|
269
272
|
return allFiles;
|
|
270
273
|
}
|
|
271
274
|
|
|
@@ -286,7 +289,7 @@ async function listAllFilesRecursively(supabase, bucketName, folderPath = '') {
|
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
291
|
} catch (error) {
|
|
289
|
-
console.log(chalk.yellow(` ⚠️
|
|
292
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.storage.processError', { path: folderPath || 'raiz', message: error.message })}`));
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
return allFiles;
|
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const fs = require('fs').promises;
|
|
4
4
|
const { promisify } = require('util');
|
|
5
5
|
const { exec } = require('child_process');
|
|
6
|
+
const { t } = require('../../../i18n');
|
|
6
7
|
|
|
7
8
|
const execAsync = promisify(exec);
|
|
8
9
|
|
|
@@ -11,7 +12,8 @@ const execAsync = promisify(exec);
|
|
|
11
12
|
*/
|
|
12
13
|
module.exports = async ({ databaseUrl, backupDir, accessToken }) => {
|
|
13
14
|
try {
|
|
14
|
-
|
|
15
|
+
const getT = global.smoonbI18n?.t || t;
|
|
16
|
+
console.log(chalk.white(` - ${getT('backup.steps.roles.exporting')}`));
|
|
15
17
|
|
|
16
18
|
const customRolesFile = path.join(backupDir, 'custom-roles.sql');
|
|
17
19
|
|
|
@@ -28,11 +30,12 @@ module.exports = async ({ databaseUrl, backupDir, accessToken }) => {
|
|
|
28
30
|
|
|
29
31
|
return { success: true, roles: [{ filename: 'custom-roles.sql', sizeKB }] };
|
|
30
32
|
} catch (error) {
|
|
31
|
-
console.log(chalk.yellow(` ⚠️
|
|
33
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.roles.exportError', { message: error.message })}`));
|
|
32
34
|
return { success: false, roles: [] };
|
|
33
35
|
}
|
|
34
36
|
} catch (error) {
|
|
35
|
-
|
|
37
|
+
const getT = global.smoonbI18n?.t || t;
|
|
38
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.roles.error', { message: error.message })}`));
|
|
36
39
|
return { success: false, roles: [] };
|
|
37
40
|
}
|
|
38
41
|
};
|
|
@@ -4,6 +4,7 @@ const fs = require('fs').promises;
|
|
|
4
4
|
const { ensureDir, writeJson } = require('../../../utils/fsx');
|
|
5
5
|
const { extractPasswordFromDbUrl, ensureCleanLink } = require('../../../utils/supabaseLink');
|
|
6
6
|
const { cleanDir } = require('../../../utils/fsExtra');
|
|
7
|
+
const { t } = require('../../../i18n');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Etapa 8: Backup Edge Functions via Docker (reset link + limpeza opcional)
|
|
@@ -42,21 +43,23 @@ module.exports = async (context) => {
|
|
|
42
43
|
|
|
43
44
|
// Se o usuário escolheu limpar APÓS, podemos limpar ANTES também para garantir ambiente limpo
|
|
44
45
|
// Mas se escolheu NÃO limpar, preservamos o que já existe
|
|
46
|
+
const getT = global.smoonbI18n?.t || t;
|
|
47
|
+
|
|
45
48
|
if (shouldCleanAfter) {
|
|
46
49
|
// Limpar antes se o usuário escolheu limpar após (garante ambiente limpo)
|
|
47
50
|
await cleanDir(supabaseFunctionsDir);
|
|
48
|
-
console.log(chalk.white(
|
|
51
|
+
console.log(chalk.white(` - ${getT('backup.steps.functions.cleanBefore')}`));
|
|
49
52
|
} else {
|
|
50
53
|
// Apenas garantir que o diretório existe
|
|
51
54
|
await fs.mkdir(supabaseFunctionsDir, { recursive: true });
|
|
52
55
|
if (existingFunctionsBefore.length > 0) {
|
|
53
|
-
console.log(chalk.white(` -
|
|
56
|
+
console.log(chalk.white(` - ${getT('backup.steps.functions.preserving', { count: existingFunctionsBefore.length })}`));
|
|
54
57
|
} else {
|
|
55
|
-
console.log(chalk.white(
|
|
58
|
+
console.log(chalk.white(` - ${getT('backup.steps.functions.prepared')}`));
|
|
56
59
|
}
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
console.log(chalk.white(
|
|
62
|
+
console.log(chalk.white(` - ${getT('backup.steps.functions.listing')}`));
|
|
60
63
|
|
|
61
64
|
// Usar fetch direto para Management API com Personal Access Token
|
|
62
65
|
const functionsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/functions`, {
|
|
@@ -67,14 +70,14 @@ module.exports = async (context) => {
|
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
if (!functionsResponse.ok) {
|
|
70
|
-
console.log(chalk.yellow(` ⚠️
|
|
73
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.functions.listError', { status: functionsResponse.status, statusText: functionsResponse.statusText })}`));
|
|
71
74
|
return { success: false, reason: 'api_error', functions: [] };
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
const functions = await functionsResponse.json();
|
|
75
78
|
|
|
76
79
|
if (!functions || functions.length === 0) {
|
|
77
|
-
console.log(chalk.white(
|
|
80
|
+
console.log(chalk.white(` - ${getT('backup.steps.functions.noneFound')}`));
|
|
78
81
|
await writeJson(path.join(functionsDir, 'README.md'), {
|
|
79
82
|
message: 'Nenhuma Edge Function encontrada neste projeto'
|
|
80
83
|
});
|
|
@@ -154,7 +157,7 @@ module.exports = async (context) => {
|
|
|
154
157
|
});
|
|
155
158
|
|
|
156
159
|
} catch (error) {
|
|
157
|
-
console.log(chalk.yellow(` ⚠️
|
|
160
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.functions.downloadError', { funcName: func.name, message: error.message })}`));
|
|
158
161
|
errorCount++;
|
|
159
162
|
}
|
|
160
163
|
}
|
|
@@ -167,11 +170,11 @@ module.exports = async (context) => {
|
|
|
167
170
|
// Nota: shouldCleanAfter já foi definido acima
|
|
168
171
|
if (shouldCleanAfter) {
|
|
169
172
|
await cleanDir(supabaseFunctionsDir);
|
|
170
|
-
console.log(chalk.white(
|
|
173
|
+
console.log(chalk.white(` - ${getT('backup.steps.functions.cleanAfter')}`));
|
|
171
174
|
} else {
|
|
172
175
|
// Preservar tudo: tanto as funções que já existiam quanto as que foram baixadas
|
|
173
176
|
// As funções baixadas não foram removidas individualmente (linha acima foi ajustada)
|
|
174
|
-
console.log(chalk.white(
|
|
177
|
+
console.log(chalk.white(` - ${getT('backup.steps.functions.preserved')}`));
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
return {
|
|
@@ -185,7 +188,8 @@ module.exports = async (context) => {
|
|
|
185
188
|
};
|
|
186
189
|
|
|
187
190
|
} catch (error) {
|
|
188
|
-
|
|
191
|
+
const getT = global.smoonbI18n?.t || t;
|
|
192
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.functions.error', { message: error.message })}`));
|
|
189
193
|
console.log('⏭️ Continuando com outros componentes...');
|
|
190
194
|
return { success: false, reason: 'download_error', error: error.message, functions: [] };
|
|
191
195
|
}
|
|
@@ -2,6 +2,7 @@ const chalk = require('chalk');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { copyDirSafe } = require('../../../utils/fsExtra');
|
|
4
4
|
const { cleanDir } = require('../../../utils/fsExtra');
|
|
5
|
+
const { t } = require('../../../i18n');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Etapa 9: Backup Supabase .temp (NOVA ETAPA INDEPENDENTE)
|
|
@@ -9,17 +10,18 @@ const { cleanDir } = require('../../../utils/fsExtra');
|
|
|
9
10
|
module.exports = async (context) => {
|
|
10
11
|
const { backupDir } = context;
|
|
11
12
|
try {
|
|
13
|
+
const getT = global.smoonbI18n?.t || t;
|
|
12
14
|
const tempDir = path.join(process.cwd(), 'supabase', '.temp');
|
|
13
15
|
const backupTempDir = path.join(backupDir, 'supabase-temp');
|
|
14
16
|
|
|
15
17
|
const fileCount = await copyDirSafe(tempDir, backupTempDir);
|
|
16
|
-
|
|
17
|
-
console.log(chalk.white(` -
|
|
18
|
+
const relativePath = path.relative(process.cwd(), backupTempDir);
|
|
19
|
+
console.log(chalk.white(` - ${getT('backup.steps.temp.copying', { path: relativePath, count: fileCount })}`));
|
|
18
20
|
|
|
19
21
|
if (fileCount === 0) {
|
|
20
|
-
console.log(chalk.white(
|
|
22
|
+
console.log(chalk.white(` - ${getT('backup.steps.temp.noFiles')}`));
|
|
21
23
|
} else {
|
|
22
|
-
console.log(chalk.green(` ✅ ${fileCount}
|
|
24
|
+
console.log(chalk.green(` ✅ ${getT('backup.steps.temp.copied', { count: fileCount })}`));
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
// Usar flag de limpeza do contexto (já foi perguntado no início)
|
|
@@ -27,7 +29,7 @@ module.exports = async (context) => {
|
|
|
27
29
|
|
|
28
30
|
if (shouldClean) {
|
|
29
31
|
await cleanDir(tempDir);
|
|
30
|
-
console.log(chalk.white(
|
|
32
|
+
console.log(chalk.white(` - ${getT('backup.steps.temp.cleaned')}`));
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
return {
|
|
@@ -35,7 +37,8 @@ module.exports = async (context) => {
|
|
|
35
37
|
file_count: fileCount
|
|
36
38
|
};
|
|
37
39
|
} catch (error) {
|
|
38
|
-
|
|
40
|
+
const getT = global.smoonbI18n?.t || t;
|
|
41
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.temp.error', { message: error.message })}`));
|
|
39
42
|
return { success: false };
|
|
40
43
|
}
|
|
41
44
|
};
|
|
@@ -3,6 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const { extractPasswordFromDbUrl, ensureCleanLink } = require('../../../utils/supabaseLink');
|
|
5
5
|
const { cleanDir, countFiles, copyDirSafe } = require('../../../utils/fsExtra');
|
|
6
|
+
const { t } = require('../../../i18n');
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Etapa 10: Backup Migrations (NOVA ETAPA INDEPENDENTE)
|
|
@@ -10,6 +11,7 @@ const { cleanDir, countFiles, copyDirSafe } = require('../../../utils/fsExtra');
|
|
|
10
11
|
module.exports = async (context) => {
|
|
11
12
|
const { projectId, accessToken, databaseUrl, backupDir } = context;
|
|
12
13
|
try {
|
|
14
|
+
const getT = global.smoonbI18n?.t || t;
|
|
13
15
|
// Reset de link ao projeto de ORIGEM
|
|
14
16
|
const dbPassword = extractPasswordFromDbUrl(databaseUrl);
|
|
15
17
|
await ensureCleanLink(projectId, accessToken, dbPassword);
|
|
@@ -17,10 +19,10 @@ module.exports = async (context) => {
|
|
|
17
19
|
// Limpar migrations local (opcional, mas recomendado para garantir servidor como fonte da verdade)
|
|
18
20
|
const migrationsDir = path.join(process.cwd(), 'supabase', 'migrations');
|
|
19
21
|
await cleanDir(migrationsDir);
|
|
20
|
-
console.log(chalk.white(
|
|
22
|
+
console.log(chalk.white(` - ${getT('backup.steps.migrations.cleaning')}`));
|
|
21
23
|
|
|
22
24
|
// Baixar todas as migrations do servidor usando migration fetch
|
|
23
|
-
console.log(chalk.white(
|
|
25
|
+
console.log(chalk.white(` - ${getT('backup.steps.migrations.downloading')}`));
|
|
24
26
|
|
|
25
27
|
const env = {
|
|
26
28
|
...process.env,
|
|
@@ -36,22 +38,23 @@ module.exports = async (context) => {
|
|
|
36
38
|
env
|
|
37
39
|
});
|
|
38
40
|
} catch (error) {
|
|
39
|
-
console.log(chalk.yellow(` ⚠️
|
|
40
|
-
console.log(chalk.yellow(
|
|
41
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.migrations.fetchError', { message: error.message })}`));
|
|
42
|
+
console.log(chalk.yellow(` 💡 ${getT('backup.steps.migrations.fetchTip')}`));
|
|
41
43
|
return { success: false };
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// Contar arquivos baixados
|
|
45
47
|
const fileCount = await countFiles(migrationsDir);
|
|
46
|
-
console.log(chalk.white(` -
|
|
48
|
+
console.log(chalk.white(` - ${getT('backup.steps.migrations.downloaded', { count: fileCount })}`));
|
|
47
49
|
|
|
48
50
|
// Copiar migrations para o backup
|
|
49
51
|
const backupMigrationsDir = path.join(backupDir, 'migrations');
|
|
50
52
|
const copiedCount = await copyDirSafe(migrationsDir, backupMigrationsDir);
|
|
51
|
-
|
|
53
|
+
const relativePath = path.relative(process.cwd(), backupMigrationsDir);
|
|
54
|
+
console.log(chalk.white(` - ${getT('backup.steps.migrations.copying', { path: relativePath, count: copiedCount })}`));
|
|
52
55
|
|
|
53
56
|
if (copiedCount > 0) {
|
|
54
|
-
console.log(chalk.green(` ✅ ${copiedCount}
|
|
57
|
+
console.log(chalk.green(` ✅ ${getT('backup.steps.migrations.copied', { count: copiedCount })}`));
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
// Usar flag de limpeza do contexto (já foi perguntado no início)
|
|
@@ -59,7 +62,7 @@ module.exports = async (context) => {
|
|
|
59
62
|
|
|
60
63
|
if (shouldClean) {
|
|
61
64
|
await cleanDir(migrationsDir);
|
|
62
|
-
console.log(chalk.gray(
|
|
65
|
+
console.log(chalk.gray(` - ${getT('backup.steps.migrations.cleaned')}`));
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
return {
|
|
@@ -67,7 +70,8 @@ module.exports = async (context) => {
|
|
|
67
70
|
file_count: copiedCount
|
|
68
71
|
};
|
|
69
72
|
} catch (error) {
|
|
70
|
-
|
|
73
|
+
const getT = global.smoonbI18n?.t || t;
|
|
74
|
+
console.log(chalk.yellow(` ⚠️ ${getT('backup.steps.migrations.error', { message: error.message })}`));
|
|
71
75
|
return { success: false };
|
|
72
76
|
}
|
|
73
77
|
};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const readline = require('readline');
|
|
3
|
+
const { t } = require('../../../i18n');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Etapa 0: Seleção interativa de backup
|
|
6
7
|
*/
|
|
7
8
|
module.exports = async (backups) => {
|
|
8
|
-
|
|
9
|
-
console.log(chalk.blue('
|
|
9
|
+
const getT = global.smoonbI18n?.t || t;
|
|
10
|
+
console.log(chalk.blue(`\n📋 ${getT('restore.steps.backupSelection.title')}`));
|
|
11
|
+
console.log(chalk.blue(getT('restore.steps.backupSelection.separator')));
|
|
10
12
|
|
|
11
13
|
backups.forEach((backup, index) => {
|
|
12
14
|
const date = new Date(backup.created).toLocaleString('pt-BR');
|
|
@@ -2,31 +2,33 @@ const path = require('path');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const { confirm } = require('../../../utils/prompt');
|
|
5
|
+
const { t } = require('../../../i18n');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Etapa 1: Perguntar quais componentes restaurar
|
|
8
9
|
*/
|
|
9
10
|
module.exports = async (backupPath) => {
|
|
11
|
+
const getT = global.smoonbI18n?.t || t;
|
|
10
12
|
// Database (sempre disponível)
|
|
11
|
-
const restoreDatabase = await confirm('
|
|
13
|
+
const restoreDatabase = await confirm(getT('restore.steps.components.database.include'), true);
|
|
12
14
|
|
|
13
15
|
// Edge Functions
|
|
14
16
|
const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
|
|
15
17
|
let restoreEdgeFunctions = false;
|
|
16
18
|
if (fs.existsSync(edgeFunctionsDir) && fs.readdirSync(edgeFunctionsDir).length > 0) {
|
|
17
|
-
console.log(chalk.cyan(
|
|
18
|
-
console.log(chalk.white('
|
|
19
|
-
console.log(chalk.white(
|
|
20
|
-
restoreEdgeFunctions = await confirm('
|
|
19
|
+
console.log(chalk.cyan(`\n⚡ ${getT('restore.steps.components.edgeFunctions.title')}`));
|
|
20
|
+
console.log(chalk.white(` ${getT('restore.steps.components.edgeFunctions.description1')}`));
|
|
21
|
+
console.log(chalk.white(` ${getT('restore.steps.components.edgeFunctions.description2')}\n`));
|
|
22
|
+
restoreEdgeFunctions = await confirm(getT('restore.steps.components.edgeFunctions.include'), true);
|
|
21
23
|
}
|
|
22
|
-
|
|
24
|
+
|
|
23
25
|
// Auth Settings
|
|
24
26
|
let restoreAuthSettings = false;
|
|
25
27
|
if (fs.existsSync(path.join(backupPath, 'auth-settings.json'))) {
|
|
26
|
-
console.log(chalk.cyan(
|
|
27
|
-
console.log(chalk.white('
|
|
28
|
-
console.log(chalk.white(
|
|
29
|
-
restoreAuthSettings = await confirm('
|
|
28
|
+
console.log(chalk.cyan(`\n🔐 ${getT('restore.steps.components.auth.title')}`));
|
|
29
|
+
console.log(chalk.white(` ${getT('restore.steps.components.auth.description1')}`));
|
|
30
|
+
console.log(chalk.white(` ${getT('restore.steps.components.auth.description2')}\n`));
|
|
31
|
+
restoreAuthSettings = await confirm(getT('restore.steps.components.auth.include'), true);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
// Storage Buckets
|
|
@@ -49,37 +51,37 @@ module.exports = async (backupPath) => {
|
|
|
49
51
|
const hasStorageFiles = hasStorageDir && fs.readdirSync(storageDir).length > 0;
|
|
50
52
|
|
|
51
53
|
if (storageZipFiles.length > 0 || hasStorageFiles) {
|
|
52
|
-
console.log(chalk.cyan(
|
|
54
|
+
console.log(chalk.cyan(`\n📦 ${getT('restore.steps.components.storage.title')}`));
|
|
53
55
|
if (storageZipFiles.length > 0) {
|
|
54
|
-
console.log(chalk.white(`
|
|
55
|
-
console.log(chalk.white('
|
|
56
|
-
console.log(chalk.white(
|
|
56
|
+
console.log(chalk.white(` ${getT('restore.steps.components.storage.zipFound', { fileName: storageZipFiles[0] })}`));
|
|
57
|
+
console.log(chalk.white(` ${getT('restore.steps.components.storage.withZip.description1')}`));
|
|
58
|
+
console.log(chalk.white(` ${getT('restore.steps.components.storage.withZip.description2')}\n`));
|
|
57
59
|
} else {
|
|
58
|
-
console.log(chalk.white(
|
|
59
|
-
console.log(chalk.white('
|
|
60
|
-
console.log(chalk.white(
|
|
60
|
+
console.log(chalk.white(` ${getT('restore.steps.components.storage.metadataOnly.description1')}`));
|
|
61
|
+
console.log(chalk.white(` ${getT('restore.steps.components.storage.metadataOnly.description2')}`));
|
|
62
|
+
console.log(chalk.white(` ${getT('restore.steps.components.storage.metadataOnly.description3')}\n`));
|
|
61
63
|
}
|
|
62
|
-
restoreStorage = await confirm('
|
|
64
|
+
restoreStorage = await confirm(getT('restore.steps.components.storage.include'), true);
|
|
63
65
|
}
|
|
64
|
-
|
|
66
|
+
|
|
65
67
|
// Database Extensions and Settings
|
|
66
68
|
const dbSettingsFiles = fs.readdirSync(backupPath)
|
|
67
69
|
.filter(file => file.startsWith('database-settings-') && file.endsWith('.json'));
|
|
68
70
|
let restoreDatabaseSettings = false;
|
|
69
71
|
if (dbSettingsFiles.length > 0) {
|
|
70
|
-
console.log(chalk.cyan(
|
|
71
|
-
console.log(chalk.white('
|
|
72
|
-
console.log(chalk.white(
|
|
73
|
-
restoreDatabaseSettings = await confirm('
|
|
72
|
+
console.log(chalk.cyan(`\n🔧 ${getT('restore.steps.components.databaseSettings.title')}`));
|
|
73
|
+
console.log(chalk.white(` ${getT('restore.steps.components.databaseSettings.description1')}`));
|
|
74
|
+
console.log(chalk.white(` ${getT('restore.steps.components.databaseSettings.description2')}\n`));
|
|
75
|
+
restoreDatabaseSettings = await confirm(getT('restore.steps.components.databaseSettings.include'), true);
|
|
74
76
|
}
|
|
75
|
-
|
|
77
|
+
|
|
76
78
|
// Realtime Settings
|
|
77
79
|
let restoreRealtimeSettings = false;
|
|
78
80
|
if (fs.existsSync(path.join(backupPath, 'realtime-settings.json'))) {
|
|
79
|
-
console.log(chalk.cyan(
|
|
80
|
-
console.log(chalk.white('
|
|
81
|
-
console.log(chalk.white(
|
|
82
|
-
restoreRealtimeSettings = await confirm('
|
|
81
|
+
console.log(chalk.cyan(`\n🔄 ${getT('restore.steps.components.realtime.title')}`));
|
|
82
|
+
console.log(chalk.white(` ${getT('restore.steps.components.realtime.description1')}`));
|
|
83
|
+
console.log(chalk.white(` ${getT('restore.steps.components.realtime.description2')}\n`));
|
|
84
|
+
restoreRealtimeSettings = await confirm(getT('restore.steps.components.realtime.include'), true);
|
|
83
85
|
}
|
|
84
86
|
|
|
85
87
|
return {
|
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
|
+
const { t } = require('../../../i18n');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Etapa 3: Restaurar Database via psql
|
|
7
8
|
*/
|
|
8
9
|
module.exports = async ({ backupFilePath, targetDatabaseUrl }) => {
|
|
9
10
|
try {
|
|
11
|
+
const getT = global.smoonbI18n?.t || t;
|
|
10
12
|
const backupDirAbs = path.resolve(path.dirname(backupFilePath));
|
|
11
13
|
const fileName = path.basename(backupFilePath);
|
|
12
14
|
let uncompressedFile = fileName;
|
|
13
15
|
|
|
14
16
|
// Verificar se é arquivo .backup.gz (compactado) ou .backup (descompactado)
|
|
15
17
|
if (fileName.endsWith('.backup.gz')) {
|
|
16
|
-
console.log(chalk.white(
|
|
17
|
-
console.log(chalk.white(
|
|
18
|
+
console.log(chalk.white(` - ${getT('restore.steps.database.detectedGz')}`));
|
|
19
|
+
console.log(chalk.white(` - ${getT('restore.steps.database.extractingGz')}`));
|
|
18
20
|
|
|
19
21
|
const unzipCmd = [
|
|
20
22
|
'docker run --rm',
|
|
@@ -24,10 +26,10 @@ module.exports = async ({ backupFilePath, targetDatabaseUrl }) => {
|
|
|
24
26
|
|
|
25
27
|
execSync(unzipCmd, { stdio: 'pipe' });
|
|
26
28
|
uncompressedFile = fileName.replace('.gz', '');
|
|
27
|
-
console.log(chalk.white(
|
|
29
|
+
console.log(chalk.white(` - ${getT('restore.steps.database.uncompressed', { file: uncompressedFile })}`));
|
|
28
30
|
} else if (fileName.endsWith('.backup')) {
|
|
29
|
-
console.log(chalk.white(
|
|
30
|
-
console.log(chalk.white(
|
|
31
|
+
console.log(chalk.white(` - ${getT('restore.steps.database.detectedBackup')}`));
|
|
32
|
+
console.log(chalk.white(` - ${getT('restore.steps.database.proceeding')}`));
|
|
31
33
|
} else {
|
|
32
34
|
throw new Error(`Formato de arquivo inválido. Esperado .backup.gz ou .backup, recebido: ${fileName}`);
|
|
33
35
|
}
|
|
@@ -49,29 +51,30 @@ module.exports = async ({ backupFilePath, targetDatabaseUrl }) => {
|
|
|
49
51
|
`-f /host/${uncompressedFile}`
|
|
50
52
|
].join(' ');
|
|
51
53
|
|
|
52
|
-
console.log(chalk.cyan(
|
|
53
|
-
console.log(chalk.cyan(
|
|
54
|
-
console.log(chalk.yellow(
|
|
55
|
-
console.log(chalk.yellow(
|
|
56
|
-
console.log(chalk.yellow(
|
|
54
|
+
console.log(chalk.cyan(` - ${getT('restore.steps.database.executing')}`));
|
|
55
|
+
console.log(chalk.cyan(` ℹ️ ${getT('restore.steps.database.followingDocs')}`));
|
|
56
|
+
console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warning')}`));
|
|
57
|
+
console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warningReason1')}`));
|
|
58
|
+
console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warningReason2')}`));
|
|
57
59
|
|
|
58
60
|
// Executar comando de restauração
|
|
59
61
|
execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
|
|
60
62
|
|
|
61
|
-
console.log(chalk.green(
|
|
62
|
-
console.log(chalk.gray(
|
|
63
|
+
console.log(chalk.green(` ✅ ${getT('restore.steps.database.success')}`));
|
|
64
|
+
console.log(chalk.gray(` ℹ️ ${getT('restore.steps.database.normalErrors')}`));
|
|
63
65
|
|
|
64
66
|
} catch (error) {
|
|
65
67
|
// Erros esperados conforme documentação oficial Supabase
|
|
68
|
+
const getT = global.smoonbI18n?.t || t;
|
|
66
69
|
if (error.message.includes('already exists') ||
|
|
67
70
|
error.message.includes('constraint') ||
|
|
68
71
|
error.message.includes('duplicate') ||
|
|
69
72
|
error.stdout?.includes('already exists')) {
|
|
70
|
-
console.log(chalk.yellow(
|
|
71
|
-
console.log(chalk.green(
|
|
72
|
-
console.log(chalk.gray(
|
|
73
|
+
console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.expectedErrors')}`));
|
|
74
|
+
console.log(chalk.green(` ✅ ${getT('restore.steps.database.success')}`));
|
|
75
|
+
console.log(chalk.gray(` ℹ️ ${getT('restore.steps.database.errorsIgnored')}`));
|
|
73
76
|
} else {
|
|
74
|
-
console.error(chalk.red(` ❌
|
|
77
|
+
console.error(chalk.red(` ❌ ${getT('restore.steps.database.error', { message: error.message })}`));
|
|
75
78
|
throw error;
|
|
76
79
|
}
|
|
77
80
|
}
|