smoonb 0.0.63 → 0.0.65
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 +2 -1
- package/src/commands/backup/index.js +10 -2
- package/src/commands/restore/index.js +20 -4
- package/src/commands/restore/steps/01-components-selection.js +13 -4
- package/src/commands/restore/steps/06-storage.js +376 -31
- package/src/commands/restore/utils.js +7 -1
- package/src/interactive/envMapper.js +25 -13
- package/src/utils/env.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoonb",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.65",
|
|
4
4
|
"description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
|
|
5
5
|
"preferGlobal": false,
|
|
6
6
|
"preventGlobalInstall": true,
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@supabase/supabase-js": "^2.38.0",
|
|
37
|
+
"adm-zip": "^0.5.16",
|
|
37
38
|
"chalk": "^4.1.2",
|
|
38
39
|
"commander": "^11.1.0",
|
|
39
40
|
"inquirer": "^8.2.7"
|
|
@@ -73,8 +73,16 @@ module.exports = async (options) => {
|
|
|
73
73
|
const envPath = path.join(process.cwd(), '.env.local');
|
|
74
74
|
const envBackupPath = path.join(backupDir, 'env', '.env.local');
|
|
75
75
|
await ensureDir(path.dirname(envBackupPath));
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
|
|
77
|
+
// Verificar se o arquivo existe antes de fazer backup
|
|
78
|
+
try {
|
|
79
|
+
await fs.access(envPath);
|
|
80
|
+
await backupEnvFile(envPath, envBackupPath);
|
|
81
|
+
console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
|
|
82
|
+
} catch {
|
|
83
|
+
// Arquivo não existe, não fazer backup
|
|
84
|
+
console.log(chalk.yellow('⚠️ Arquivo .env.local não encontrado. Será criado durante o mapeamento.'));
|
|
85
|
+
}
|
|
78
86
|
|
|
79
87
|
const expectedKeys = [
|
|
80
88
|
'SUPABASE_PROJECT_ID',
|
|
@@ -135,8 +135,16 @@ module.exports = async (options) => {
|
|
|
135
135
|
// Backup do .env.local
|
|
136
136
|
const envPath = path.join(process.cwd(), '.env.local');
|
|
137
137
|
const envBackupPath = path.join(processDir, 'env', '.env.local');
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
|
|
139
|
+
// Verificar se o arquivo existe antes de fazer backup
|
|
140
|
+
try {
|
|
141
|
+
await fsPromises.access(envPath);
|
|
142
|
+
await backupEnvFile(envPath, envBackupPath);
|
|
143
|
+
console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
|
|
144
|
+
} catch {
|
|
145
|
+
// Arquivo não existe, não fazer backup
|
|
146
|
+
console.log(chalk.yellow('⚠️ Arquivo .env.local não encontrado. Será criado durante o mapeamento.'));
|
|
147
|
+
}
|
|
140
148
|
|
|
141
149
|
// Leitura e mapeamento interativo
|
|
142
150
|
const currentEnv = await readEnvFile(envPath);
|
|
@@ -290,7 +298,8 @@ module.exports = async (options) => {
|
|
|
290
298
|
stepNumber++;
|
|
291
299
|
console.log(chalk.blue(`\n📦 ${stepNumber}/${totalSteps} - Restaurando Storage Buckets...`));
|
|
292
300
|
const storageResult = await step06Storage({
|
|
293
|
-
backupPath: selectedBackup.path
|
|
301
|
+
backupPath: selectedBackup.path,
|
|
302
|
+
targetProject
|
|
294
303
|
});
|
|
295
304
|
restoreResults.storage = storageResult || { success: true };
|
|
296
305
|
}
|
|
@@ -358,7 +367,14 @@ module.exports = async (options) => {
|
|
|
358
367
|
|
|
359
368
|
if (restoreResults.storage) {
|
|
360
369
|
const bucketCount = restoreResults.storage.buckets_count || 0;
|
|
361
|
-
|
|
370
|
+
const filesRestored = restoreResults.storage.files_restored;
|
|
371
|
+
const totalFiles = restoreResults.storage.total_files || 0;
|
|
372
|
+
|
|
373
|
+
if (filesRestored) {
|
|
374
|
+
console.log(chalk.green(`📦 Storage: ${bucketCount} bucket(s) restaurado(s), ${totalFiles} arquivo(s) enviado(s)`));
|
|
375
|
+
} else {
|
|
376
|
+
console.log(chalk.green(`📦 Storage: ${bucketCount} bucket(s) encontrado(s) - apenas metadados (arquivo .storage.zip não encontrado)`));
|
|
377
|
+
}
|
|
362
378
|
}
|
|
363
379
|
|
|
364
380
|
if (restoreResults.databaseSettings) {
|
|
@@ -31,12 +31,21 @@ module.exports = async (backupPath) => {
|
|
|
31
31
|
|
|
32
32
|
// Storage Buckets
|
|
33
33
|
const storageDir = path.join(backupPath, 'storage');
|
|
34
|
+
const storageZipFiles = fs.readdirSync(backupPath).filter(f => f.endsWith('.storage.zip'));
|
|
34
35
|
let restoreStorage = false;
|
|
35
|
-
|
|
36
|
+
|
|
37
|
+
if (storageZipFiles.length > 0 || (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0)) {
|
|
36
38
|
console.log(chalk.cyan('\n📦 Storage:'));
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
if (storageZipFiles.length > 0) {
|
|
40
|
+
console.log(chalk.white(` Arquivo .storage.zip encontrado: ${storageZipFiles[0]}`));
|
|
41
|
+
console.log(chalk.white(' Os buckets e arquivos serão restaurados automaticamente no projeto destino.'));
|
|
42
|
+
console.log(chalk.white(' O arquivo ZIP será extraído, buckets criados e arquivos enviados via API.\n'));
|
|
43
|
+
} else {
|
|
44
|
+
console.log(chalk.white(' Apenas metadados dos buckets encontrados (pasta storage).'));
|
|
45
|
+
console.log(chalk.white(' Para restaurar os arquivos, é necessário o arquivo .storage.zip do Dashboard.'));
|
|
46
|
+
console.log(chalk.white(' Apenas informações dos buckets serão exibidas.\n'));
|
|
47
|
+
}
|
|
48
|
+
restoreStorage = await confirm('Deseja restaurar Storage Buckets', true);
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
// Database Extensions and Settings
|
|
@@ -1,58 +1,378 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const AdmZip = require('adm-zip');
|
|
5
|
+
const { createClient } = require('@supabase/supabase-js');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Etapa 6: Restaurar Storage Buckets
|
|
8
|
+
* Etapa 6: Restaurar Storage Buckets e arquivos
|
|
9
|
+
* Processo baseado no script oficial do Supabase Storage Migration
|
|
10
|
+
* Segue exatamente o processo descrito no notebook oficial do Supabase
|
|
8
11
|
*/
|
|
9
|
-
module.exports = async ({ backupPath }) => {
|
|
12
|
+
module.exports = async ({ backupPath, targetProject }) => {
|
|
10
13
|
|
|
11
14
|
try {
|
|
15
|
+
// 1. Verificar se existe arquivo .storage.zip na pasta do backup
|
|
16
|
+
const storageZipFiles = await fs.readdir(backupPath).then(files =>
|
|
17
|
+
files.filter(f => f.endsWith('.storage.zip'))
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
// 2. Carregar metadados dos buckets do backup (se existirem)
|
|
12
21
|
const storageDir = path.join(backupPath, 'storage');
|
|
22
|
+
const manifestPath = path.join(backupPath, 'backup-manifest.json');
|
|
23
|
+
const bucketMetadata = {};
|
|
24
|
+
let manifest = null;
|
|
13
25
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
try {
|
|
27
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf8');
|
|
28
|
+
manifest = JSON.parse(manifestContent);
|
|
29
|
+
|
|
30
|
+
// Carregar metadados dos buckets do manifest
|
|
31
|
+
const buckets = manifest?.components?.storage?.buckets || [];
|
|
32
|
+
for (const bucket of buckets) {
|
|
33
|
+
bucketMetadata[bucket.name] = {
|
|
34
|
+
public: bucket.public || false,
|
|
35
|
+
file_size_limit: bucket.file_size_limit || null,
|
|
36
|
+
allowed_mime_types: bucket.allowed_mime_types || null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Também tentar carregar dos arquivos JSON individuais (backup mais detalhado)
|
|
41
|
+
try {
|
|
42
|
+
const storageFiles = await fs.readdir(storageDir);
|
|
43
|
+
for (const file of storageFiles) {
|
|
44
|
+
if (file.endsWith('.json') && file !== 'README.md') {
|
|
45
|
+
const bucketName = file.replace('.json', '');
|
|
46
|
+
try {
|
|
47
|
+
const bucketInfoPath = path.join(storageDir, file);
|
|
48
|
+
const bucketInfoContent = await fs.readFile(bucketInfoPath, 'utf8');
|
|
49
|
+
const bucketInfo = JSON.parse(bucketInfoContent);
|
|
50
|
+
|
|
51
|
+
// Usar metadados dos arquivos JSON se disponíveis (mais completo)
|
|
52
|
+
// Incluir objetos para preservar metadados dos arquivos (contentType, cacheControl)
|
|
53
|
+
bucketMetadata[bucketName] = {
|
|
54
|
+
public: bucketInfo.public !== undefined ? bucketInfo.public : false,
|
|
55
|
+
file_size_limit: bucketInfo.file_size_limit || null,
|
|
56
|
+
allowed_mime_types: bucketInfo.allowed_mime_types || null,
|
|
57
|
+
objects: bucketInfo.objects || [] // Metadados dos objetos para preservar contentType e cacheControl
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
// Ignorar arquivos JSON inválidos
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// Pasta storage pode não existir, continuar
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Manifest pode não existir, continuar sem metadados
|
|
17
69
|
}
|
|
18
70
|
|
|
19
|
-
|
|
20
|
-
|
|
71
|
+
if (storageZipFiles.length === 0) {
|
|
72
|
+
// Verificar se existe pasta storage com metadados (backup antigo)
|
|
73
|
+
try {
|
|
74
|
+
await fs.access(storageDir);
|
|
75
|
+
const buckets = manifest?.components?.storage?.buckets || [];
|
|
76
|
+
|
|
77
|
+
if (buckets.length === 0) {
|
|
78
|
+
console.log(chalk.yellow(' ⚠️ Nenhum bucket de Storage encontrado no backup'));
|
|
79
|
+
return { success: false, buckets_count: 0 };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(chalk.yellow('\n ⚠️ Arquivo .storage.zip não encontrado'));
|
|
83
|
+
console.log(chalk.white(' ℹ️ Apenas metadados dos buckets foram encontrados'));
|
|
84
|
+
console.log(chalk.white(' ℹ️ Para restaurar os arquivos, é necessário o arquivo .storage.zip do Dashboard'));
|
|
85
|
+
console.log(chalk.green(`\n ✅ ${buckets.length} bucket(s) encontrado(s) no backup (apenas metadados)`));
|
|
86
|
+
buckets.forEach(bucket => {
|
|
87
|
+
console.log(chalk.white(` - ${bucket.name} (${bucket.public ? 'público' : 'privado'})`));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { success: true, buckets_count: buckets.length, files_restored: false };
|
|
91
|
+
} catch {
|
|
92
|
+
console.log(chalk.yellow(' ⚠️ Nenhum bucket de Storage encontrado no backup'));
|
|
93
|
+
return { success: false, buckets_count: 0 };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 3. Selecionar o primeiro arquivo .storage.zip encontrado
|
|
98
|
+
const storageZipFile = path.join(backupPath, storageZipFiles[0]);
|
|
99
|
+
console.log(chalk.white(` - Arquivo de storage encontrado: ${storageZipFiles[0]}`));
|
|
100
|
+
|
|
101
|
+
// 4. Validar credenciais do projeto destino
|
|
102
|
+
if (!targetProject.targetProjectId || !targetProject.targetAccessToken) {
|
|
103
|
+
throw new Error('Credenciais do projeto destino não configuradas. É necessário SUPABASE_PROJECT_ID e SUPABASE_ACCESS_TOKEN');
|
|
104
|
+
}
|
|
21
105
|
|
|
22
|
-
if (
|
|
23
|
-
|
|
106
|
+
if (!targetProject.targetUrl || !targetProject.targetServiceKey) {
|
|
107
|
+
throw new Error('Credenciais do Supabase não configuradas. É necessário NEXT_PUBLIC_SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY');
|
|
24
108
|
}
|
|
25
109
|
|
|
26
|
-
|
|
110
|
+
// 5. Extrair arquivo ZIP
|
|
111
|
+
console.log(chalk.white(' - Extraindo arquivo .storage.zip...'));
|
|
112
|
+
const extractDir = path.join(backupPath, 'storage_extracted');
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
116
|
+
} catch {
|
|
117
|
+
// Diretório pode já existir
|
|
118
|
+
}
|
|
27
119
|
|
|
28
|
-
|
|
29
|
-
|
|
120
|
+
const zip = new AdmZip(storageZipFile);
|
|
121
|
+
zip.extractAllTo(extractDir, true);
|
|
122
|
+
console.log(chalk.green(' ✅ Arquivo extraído com sucesso'));
|
|
123
|
+
|
|
124
|
+
// 6. Ler estrutura de diretórios extraídos
|
|
125
|
+
// O formato do .storage.zip do Supabase tem a estrutura:
|
|
126
|
+
// bucket-name/
|
|
127
|
+
// file1.jpg
|
|
128
|
+
// folder/
|
|
129
|
+
// file2.png
|
|
130
|
+
const extractedContents = await fs.readdir(extractDir);
|
|
131
|
+
const bucketDirs = [];
|
|
132
|
+
|
|
133
|
+
for (const item of extractedContents) {
|
|
134
|
+
const itemPath = path.join(extractDir, item);
|
|
135
|
+
const stats = await fs.stat(itemPath);
|
|
136
|
+
if (stats.isDirectory()) {
|
|
137
|
+
bucketDirs.push(item);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (bucketDirs.length === 0) {
|
|
142
|
+
console.log(chalk.yellow(' ⚠️ Nenhum bucket encontrado no arquivo .storage.zip'));
|
|
30
143
|
return { success: false, buckets_count: 0 };
|
|
31
144
|
}
|
|
32
145
|
|
|
33
|
-
console.log(chalk.
|
|
34
|
-
buckets.forEach(bucket => {
|
|
35
|
-
console.log(chalk.white(` - ${bucket.name} (${bucket.public ? 'público' : 'privado'})`));
|
|
36
|
-
});
|
|
146
|
+
console.log(chalk.white(` - Encontrados ${bucketDirs.length} bucket(s) no arquivo ZIP`));
|
|
37
147
|
|
|
38
|
-
|
|
148
|
+
// 7. Criar cliente Supabase para o projeto destino
|
|
149
|
+
const supabase = createClient(targetProject.targetUrl, targetProject.targetServiceKey);
|
|
39
150
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
console.log(chalk.white(' 1. Execute o script no Google Colab'));
|
|
44
|
-
console.log(chalk.white(' 2. Configure as credenciais dos projetos (origem e destino)'));
|
|
45
|
-
console.log(chalk.white(' 3. Execute a migração'));
|
|
151
|
+
// 8. Processar cada bucket
|
|
152
|
+
let successCount = 0;
|
|
153
|
+
let totalFilesUploaded = 0;
|
|
46
154
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
155
|
+
for (const bucketName of bucketDirs) {
|
|
156
|
+
try {
|
|
157
|
+
console.log(chalk.white(`\n - Processando bucket: ${bucketName}`));
|
|
158
|
+
|
|
159
|
+
const bucketPath = path.join(extractDir, bucketName);
|
|
160
|
+
|
|
161
|
+
// 8.1 Obter metadados do bucket do backup (se disponíveis)
|
|
162
|
+
const bucketMeta = bucketMetadata[bucketName] || {
|
|
163
|
+
public: false,
|
|
164
|
+
file_size_limit: null,
|
|
165
|
+
allowed_mime_types: null
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// 8.2 Verificar se bucket já existe, se não, criar com configurações corretas
|
|
169
|
+
const { data: existingBuckets, error: listError } = await supabase.storage.listBuckets();
|
|
170
|
+
|
|
171
|
+
if (listError) {
|
|
172
|
+
throw new Error(`Erro ao listar buckets: ${listError.message}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const existingBucket = existingBuckets?.find(b => b.name === bucketName);
|
|
176
|
+
const bucketExists = !!existingBucket;
|
|
177
|
+
|
|
178
|
+
if (!bucketExists) {
|
|
179
|
+
// Criar bucket via Management API com configurações do backup
|
|
180
|
+
console.log(chalk.white(` - Criando bucket ${bucketName}...`));
|
|
181
|
+
console.log(chalk.white(` Configurações: ${bucketMeta.public ? 'público' : 'privado'}, limite: ${bucketMeta.file_size_limit || 'sem limite'}, tipos: ${bucketMeta.allowed_mime_types?.join(', ') || 'todos'}`));
|
|
182
|
+
|
|
183
|
+
const createResponse = await fetch(
|
|
184
|
+
`https://api.supabase.com/v1/projects/${targetProject.targetProjectId}/storage/buckets`,
|
|
185
|
+
{
|
|
186
|
+
method: 'POST',
|
|
187
|
+
headers: {
|
|
188
|
+
'Authorization': `Bearer ${targetProject.targetAccessToken}`,
|
|
189
|
+
'Content-Type': 'application/json'
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
name: bucketName,
|
|
193
|
+
public: bucketMeta.public,
|
|
194
|
+
file_size_limit: bucketMeta.file_size_limit,
|
|
195
|
+
allowed_mime_types: bucketMeta.allowed_mime_types
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
if (!createResponse.ok) {
|
|
201
|
+
const errorText = await createResponse.text();
|
|
202
|
+
throw new Error(`Erro ao criar bucket: ${createResponse.status} ${errorText}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(chalk.green(` ✅ Bucket ${bucketName} criado com configurações do backup`));
|
|
206
|
+
} else {
|
|
207
|
+
// Bucket já existe - verificar e atualizar configurações se necessário
|
|
208
|
+
console.log(chalk.white(` - Bucket ${bucketName} já existe`));
|
|
209
|
+
|
|
210
|
+
const needsUpdate =
|
|
211
|
+
existingBucket.public !== bucketMeta.public ||
|
|
212
|
+
existingBucket.file_size_limit !== bucketMeta.file_size_limit ||
|
|
213
|
+
JSON.stringify(existingBucket.allowed_mime_types || []) !== JSON.stringify(bucketMeta.allowed_mime_types || []);
|
|
214
|
+
|
|
215
|
+
if (needsUpdate) {
|
|
216
|
+
console.log(chalk.white(` - Atualizando configurações do bucket para corresponder ao backup...`));
|
|
217
|
+
|
|
218
|
+
const updateResponse = await fetch(
|
|
219
|
+
`https://api.supabase.com/v1/projects/${targetProject.targetProjectId}/storage/buckets/${bucketName}`,
|
|
220
|
+
{
|
|
221
|
+
method: 'PATCH',
|
|
222
|
+
headers: {
|
|
223
|
+
'Authorization': `Bearer ${targetProject.targetAccessToken}`,
|
|
224
|
+
'Content-Type': 'application/json'
|
|
225
|
+
},
|
|
226
|
+
body: JSON.stringify({
|
|
227
|
+
public: bucketMeta.public,
|
|
228
|
+
file_size_limit: bucketMeta.file_size_limit,
|
|
229
|
+
allowed_mime_types: bucketMeta.allowed_mime_types
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (!updateResponse.ok) {
|
|
235
|
+
const errorText = await updateResponse.text();
|
|
236
|
+
console.log(chalk.yellow(` ⚠️ Não foi possível atualizar configurações: ${errorText}`));
|
|
237
|
+
} else {
|
|
238
|
+
console.log(chalk.green(` ✅ Configurações do bucket atualizadas`));
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
console.log(chalk.white(` - Configurações do bucket já estão corretas`));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 8.3 Listar todos os arquivos do bucket recursivamente
|
|
246
|
+
async function getAllFiles(dir, basePath = '') {
|
|
247
|
+
const files = [];
|
|
248
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
249
|
+
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
const fullPath = path.join(dir, entry.name);
|
|
252
|
+
const relativePath = path.join(basePath, entry.name).replace(/\\/g, '/');
|
|
253
|
+
|
|
254
|
+
if (entry.isDirectory()) {
|
|
255
|
+
const subFiles = await getAllFiles(fullPath, relativePath);
|
|
256
|
+
files.push(...subFiles);
|
|
257
|
+
} else {
|
|
258
|
+
files.push({
|
|
259
|
+
localPath: fullPath,
|
|
260
|
+
storagePath: relativePath
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return files;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const filesToUpload = await getAllFiles(bucketPath);
|
|
269
|
+
console.log(chalk.white(` - Encontrados ${filesToUpload.length} arquivo(s) para upload`));
|
|
270
|
+
|
|
271
|
+
// 8.4 Fazer upload de cada arquivo
|
|
272
|
+
// Criar mapa de metadados dos objetos do backup (se disponível)
|
|
273
|
+
// A Management API pode retornar objetos com diferentes estruturas
|
|
274
|
+
const objectsMetadata = {};
|
|
275
|
+
if (bucketMeta.objects && Array.isArray(bucketMeta.objects)) {
|
|
276
|
+
for (const obj of bucketMeta.objects) {
|
|
277
|
+
// Normalizar o caminho para comparação (remover barras iniciais e normalizar)
|
|
278
|
+
// A API pode retornar 'name' ou 'path' ou 'id'
|
|
279
|
+
const objPath = obj.name || obj.path || obj.id || '';
|
|
280
|
+
const normalizedPath = objPath.replace(/^\/+/, '').replace(/\\/g, '/');
|
|
281
|
+
if (normalizedPath) {
|
|
282
|
+
objectsMetadata[normalizedPath] = obj;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let filesUploaded = 0;
|
|
288
|
+
for (const file of filesToUpload) {
|
|
289
|
+
try {
|
|
290
|
+
const fileContent = await fs.readFile(file.localPath);
|
|
291
|
+
const fileName = path.basename(file.localPath);
|
|
292
|
+
|
|
293
|
+
// Usar o caminho relativo como path no storage
|
|
294
|
+
const storagePath = file.storagePath;
|
|
295
|
+
|
|
296
|
+
// Buscar metadados do objeto no backup (se disponível)
|
|
297
|
+
// Normalizar caminho para comparação (remover barras iniciais e normalizar separadores)
|
|
298
|
+
const normalizedStoragePath = storagePath.replace(/^\/+/, '').replace(/\\/g, '/');
|
|
299
|
+
const objectMeta = objectsMetadata[normalizedStoragePath];
|
|
300
|
+
|
|
301
|
+
// Extrair metadados do objeto (a estrutura pode variar)
|
|
302
|
+
// A Management API pode retornar metadata.mimetype, metadata.contentType, ou metadata diretamente
|
|
303
|
+
let contentType = getContentType(fileName);
|
|
304
|
+
let cacheControl = undefined;
|
|
305
|
+
|
|
306
|
+
if (objectMeta) {
|
|
307
|
+
// Tentar diferentes estruturas possíveis de metadados
|
|
308
|
+
const metadata = objectMeta.metadata || objectMeta;
|
|
309
|
+
contentType = metadata?.mimetype ||
|
|
310
|
+
metadata?.contentType ||
|
|
311
|
+
metadata?.mime_type ||
|
|
312
|
+
objectMeta.mimetype ||
|
|
313
|
+
objectMeta.contentType ||
|
|
314
|
+
getContentType(fileName);
|
|
315
|
+
|
|
316
|
+
cacheControl = metadata?.cacheControl ||
|
|
317
|
+
metadata?.cache_control ||
|
|
318
|
+
objectMeta.cacheControl ||
|
|
319
|
+
objectMeta.cache_control ||
|
|
320
|
+
undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Preparar opções de upload seguindo o script oficial do Supabase
|
|
324
|
+
// Preservar contentType e cacheControl quando disponíveis
|
|
325
|
+
const uploadOptions = {
|
|
326
|
+
upsert: true, // Sobrescrever se já existir
|
|
327
|
+
contentType: contentType
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// Adicionar cacheControl apenas se estiver definido
|
|
331
|
+
if (cacheControl) {
|
|
332
|
+
uploadOptions.cacheControl = cacheControl;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const { error: uploadError } = await supabase.storage
|
|
336
|
+
.from(bucketName)
|
|
337
|
+
.upload(storagePath, fileContent, uploadOptions);
|
|
338
|
+
|
|
339
|
+
if (uploadError) {
|
|
340
|
+
console.log(chalk.yellow(` ⚠️ Erro ao fazer upload de ${storagePath}: ${uploadError.message}`));
|
|
341
|
+
} else {
|
|
342
|
+
filesUploaded++;
|
|
343
|
+
if (filesToUpload.length <= 10 || filesUploaded % Math.ceil(filesToUpload.length / 10) === 0) {
|
|
344
|
+
const metaInfo = objectMeta ? ' (metadados preservados)' : '';
|
|
345
|
+
console.log(chalk.white(` - Upload: ${storagePath}${metaInfo}`));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} catch (fileError) {
|
|
349
|
+
console.log(chalk.yellow(` ⚠️ Erro ao processar arquivo ${file.storagePath}: ${fileError.message}`));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log(chalk.green(` ✅ Bucket ${bucketName}: ${filesUploaded}/${filesToUpload.length} arquivo(s) enviado(s)`));
|
|
354
|
+
successCount++;
|
|
355
|
+
totalFilesUploaded += filesUploaded;
|
|
356
|
+
|
|
357
|
+
} catch (bucketError) {
|
|
358
|
+
console.log(chalk.red(` ❌ Erro ao processar bucket ${bucketName}: ${bucketError.message}`));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 9. Limpar diretório de extração
|
|
363
|
+
try {
|
|
364
|
+
await fs.rm(extractDir, { recursive: true, force: true });
|
|
365
|
+
} catch {
|
|
366
|
+
// Ignorar erro de limpeza
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
console.log(chalk.green(`\n ✅ Restauração de Storage concluída: ${successCount}/${bucketDirs.length} bucket(s) processado(s), ${totalFilesUploaded} arquivo(s) enviado(s)`));
|
|
52
370
|
|
|
53
371
|
return {
|
|
54
372
|
success: true,
|
|
55
|
-
buckets_count:
|
|
373
|
+
buckets_count: successCount,
|
|
374
|
+
files_restored: true,
|
|
375
|
+
total_files: totalFilesUploaded
|
|
56
376
|
};
|
|
57
377
|
|
|
58
378
|
} catch (error) {
|
|
@@ -61,3 +381,28 @@ module.exports = async ({ backupPath }) => {
|
|
|
61
381
|
}
|
|
62
382
|
};
|
|
63
383
|
|
|
384
|
+
/**
|
|
385
|
+
* Determina o content-type baseado na extensão do arquivo
|
|
386
|
+
*/
|
|
387
|
+
function getContentType(fileName) {
|
|
388
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
389
|
+
const contentTypes = {
|
|
390
|
+
'.jpg': 'image/jpeg',
|
|
391
|
+
'.jpeg': 'image/jpeg',
|
|
392
|
+
'.png': 'image/png',
|
|
393
|
+
'.gif': 'image/gif',
|
|
394
|
+
'.webp': 'image/webp',
|
|
395
|
+
'.svg': 'image/svg+xml',
|
|
396
|
+
'.pdf': 'application/pdf',
|
|
397
|
+
'.json': 'application/json',
|
|
398
|
+
'.txt': 'text/plain',
|
|
399
|
+
'.html': 'text/html',
|
|
400
|
+
'.css': 'text/css',
|
|
401
|
+
'.js': 'application/javascript',
|
|
402
|
+
'.zip': 'application/zip',
|
|
403
|
+
'.mp4': 'video/mp4',
|
|
404
|
+
'.mp3': 'audio/mpeg'
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
return contentTypes[ext] || 'application/octet-stream';
|
|
408
|
+
}
|
|
@@ -94,7 +94,13 @@ function showRestoreSummary(backup, components, targetProject) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (components.storage) {
|
|
97
|
-
|
|
97
|
+
const backupPath = backup.path;
|
|
98
|
+
const storageZipFiles = fs.readdirSync(backupPath).filter(f => f.endsWith('.storage.zip'));
|
|
99
|
+
if (storageZipFiles.length > 0) {
|
|
100
|
+
console.log('📦 Storage Buckets: Restauração automática de buckets e arquivos via API');
|
|
101
|
+
} else {
|
|
102
|
+
console.log('📦 Storage Buckets: Exibir informações (apenas metadados - arquivo .storage.zip não encontrado)');
|
|
103
|
+
}
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
if (components.databaseSettings) {
|
|
@@ -38,22 +38,22 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
38
38
|
'NEXT_PUBLIC_SUPABASE_URL': {
|
|
39
39
|
notFound: 'Não foi encontrada uma entrada para a variável NEXT_PUBLIC_SUPABASE_URL.',
|
|
40
40
|
help: currentProjectId
|
|
41
|
-
? `
|
|
42
|
-
: '
|
|
41
|
+
? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api`
|
|
42
|
+
: 'Dashboard -> Project Settings -> API -> Project URL',
|
|
43
43
|
link: currentProjectId ? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api` : null
|
|
44
44
|
},
|
|
45
45
|
'NEXT_PUBLIC_SUPABASE_ANON_KEY': {
|
|
46
46
|
notFound: 'Não foi encontrada uma entrada para a variável NEXT_PUBLIC_SUPABASE_ANON_KEY.',
|
|
47
47
|
help: currentProjectId
|
|
48
|
-
? `
|
|
49
|
-
: '
|
|
48
|
+
? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys`
|
|
49
|
+
: 'Dashboard -> Project Settings -> API -> API Keys -> anon public',
|
|
50
50
|
link: currentProjectId ? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys` : null
|
|
51
51
|
},
|
|
52
52
|
'SUPABASE_SERVICE_ROLE_KEY': {
|
|
53
53
|
notFound: 'Não foi encontrada uma entrada para a variável SUPABASE_SERVICE_ROLE_KEY.',
|
|
54
54
|
help: currentProjectId
|
|
55
|
-
? `
|
|
56
|
-
: '
|
|
55
|
+
? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys`
|
|
56
|
+
: 'Dashboard -> Project Settings -> API -> API Keys -> service_role secret',
|
|
57
57
|
link: currentProjectId ? `https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys` : null
|
|
58
58
|
},
|
|
59
59
|
'SUPABASE_DB_URL': {
|
|
@@ -65,7 +65,7 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
65
65
|
},
|
|
66
66
|
'SUPABASE_ACCESS_TOKEN': {
|
|
67
67
|
notFound: 'Não foi encontrada uma entrada para a variável SUPABASE_ACCESS_TOKEN.',
|
|
68
|
-
help: '
|
|
68
|
+
help: 'https://supabase.com/dashboard/account/tokens',
|
|
69
69
|
link: 'https://supabase.com/dashboard/account/tokens'
|
|
70
70
|
},
|
|
71
71
|
'SMOONB_OUTPUT_DIR': {
|
|
@@ -91,8 +91,8 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
91
91
|
// Se existir chave exatamente igual, pular seleção e ir direto para confirmação
|
|
92
92
|
if (Object.prototype.hasOwnProperty.call(finalEnv, expected)) {
|
|
93
93
|
clientKey = expected;
|
|
94
|
-
} else {
|
|
95
|
-
//
|
|
94
|
+
} else if (allKeys.length > 0) {
|
|
95
|
+
// Só mostrar seleção se houver chaves disponíveis no env
|
|
96
96
|
const choices = [
|
|
97
97
|
...allKeys.map((k, idx) => ({ name: `${idx + 1}. ${k}`, value: k })),
|
|
98
98
|
new inquirer.Separator(),
|
|
@@ -119,19 +119,31 @@ async function mapEnvVariablesInteractively(env, expectedKeys) {
|
|
|
119
119
|
}
|
|
120
120
|
finalEnv[clientKey] = '';
|
|
121
121
|
}
|
|
122
|
+
} else {
|
|
123
|
+
// Se não há chaves no env, criar nova chave diretamente
|
|
124
|
+
clientKey = expected;
|
|
125
|
+
finalEnv[clientKey] = '';
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
const currentValue = finalEnv[clientKey] ?? '';
|
|
125
129
|
const currentProjectId = getProjectId();
|
|
126
130
|
const instructions = getVariableInstructions(expected, currentProjectId);
|
|
127
131
|
|
|
128
|
-
// Se não tem valor, mostrar mensagem específica
|
|
132
|
+
// Se não tem valor, mostrar mensagem específica e pular confirmação
|
|
129
133
|
if (!currentValue) {
|
|
130
134
|
console.log(chalk.yellow(instructions.notFound));
|
|
131
135
|
if (instructions.help) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
// Se o help contém link (https://), mostrar como link
|
|
137
|
+
if (instructions.help.includes('https://')) {
|
|
138
|
+
console.log(chalk.cyan(`🔗 ${instructions.help}`));
|
|
139
|
+
} else {
|
|
140
|
+
console.log(chalk.white(instructions.help));
|
|
141
|
+
// Se há link separado e o help não contém link, mostrar link separado
|
|
142
|
+
if (instructions.link && !instructions.help.includes('https://')) {
|
|
143
|
+
console.log(chalk.cyan(`🔗 ${instructions.link}`));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else if (instructions.link) {
|
|
135
147
|
console.log(chalk.cyan(`🔗 ${instructions.link}`));
|
|
136
148
|
}
|
|
137
149
|
} else {
|
package/src/utils/env.js
CHANGED
|
@@ -81,10 +81,12 @@ async function writeEnvFile(filePath, entries, _options = {}) {
|
|
|
81
81
|
async function backupEnvFile(srcPath, destPath) {
|
|
82
82
|
await fsp.mkdir(path.dirname(destPath), { recursive: true });
|
|
83
83
|
try {
|
|
84
|
+
// Verificar se o arquivo existe antes de fazer backup
|
|
85
|
+
await fsp.access(srcPath);
|
|
84
86
|
await fsp.copyFile(srcPath, destPath);
|
|
85
87
|
} catch (e) {
|
|
86
88
|
if (e.code === 'ENOENT') {
|
|
87
|
-
|
|
89
|
+
// Arquivo não existe, não fazer backup de arquivo vazio
|
|
88
90
|
return;
|
|
89
91
|
}
|
|
90
92
|
throw e;
|