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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.63",
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
- await backupEnvFile(envPath, envBackupPath);
77
- console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
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
- await backupEnvFile(envPath, envBackupPath);
139
- console.log(chalk.blue(`📁 Backup do .env.local: ${path.relative(process.cwd(), envBackupPath)}`));
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
- console.log(chalk.green(`📦 Storage: ${bucketCount} bucket(s) encontrado(s) - migração manual necessária`));
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
- if (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0) {
36
+
37
+ if (storageZipFiles.length > 0 || (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0)) {
36
38
  console.log(chalk.cyan('\n📦 Storage:'));
37
- console.log(chalk.white(' As informações dos buckets de Storage serão exibidas para migração manual.'));
38
- console.log(chalk.white(' Os arquivos precisam ser migrados manualmente usando as ferramentas do Supabase.\n'));
39
- restoreStorage = await confirm('Deseja ver informações de Storage Buckets', true);
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 inquirer = require('inquirer');
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 (interativo - exibir informações)
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
- if (!fs.existsSync(storageDir)) {
15
- console.log(chalk.yellow(' ⚠️ Nenhum bucket de Storage encontrado no backup'));
16
- return { success: false, buckets_count: 0 };
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
- const manifestPath = path.join(backupPath, 'backup-manifest.json');
20
- let manifest = null;
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 (fs.existsSync(manifestPath)) {
23
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
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
- const buckets = manifest?.components?.storage?.buckets || [];
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
- if (buckets.length === 0) {
29
- console.log(chalk.white(' ℹ️ Nenhum bucket para restaurar'));
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.green(`\n ${buckets.length} bucket(s) encontrado(s) no backup`));
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
- const colabUrl = 'https://colab.research.google.com/github/PLyn/supabase-storage-migrate/blob/main/Supabase_Storage_migration.ipynb';
148
+ // 7. Criar cliente Supabase para o projeto destino
149
+ const supabase = createClient(targetProject.targetUrl, targetProject.targetServiceKey);
39
150
 
40
- console.log(chalk.yellow('\n ⚠️ Migração de objetos de Storage requer processo manual'));
41
- console.log(chalk.cyan(` ℹ️ Use o script do Google Colab: ${colabUrl}`));
42
- console.log(chalk.white('\n 📋 Instruções:'));
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
- await inquirer.prompt([{
48
- type: 'input',
49
- name: 'continue',
50
- message: 'Pressione Enter para continuar'
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: buckets.length
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
- console.log('📦 Storage Buckets: Exibir informações e instruções do Google Colab');
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
- ? `Acesse: https://supabase.com/dashboard/project/${currentProjectId}/settings/api`
42
- : 'Acesse: Dashboard -> Project Settings -> API -> Project URL',
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
- ? `Acesse: https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys`
49
- : 'Acesse: Dashboard -> Project Settings -> API -> API Keys -> anon public',
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
- ? `Acesse: https://supabase.com/dashboard/project/${currentProjectId}/settings/api-keys`
56
- : 'Acesse: Dashboard -> Project Settings -> API -> API Keys -> service_role secret',
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: 'Acesse: https://supabase.com/dashboard/account/tokens',
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
- // Opção explícita para adicionar nova chave
94
+ } else if (allKeys.length > 0) {
95
+ // 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
- console.log(chalk.white(instructions.help));
133
- }
134
- if (instructions.link) {
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
- await fsp.writeFile(destPath, '', 'utf8');
89
+ // Arquivo não existe, não fazer backup de arquivo vazio
88
90
  return;
89
91
  }
90
92
  throw e;