smoonb 0.0.69 → 0.0.71
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/README.md
CHANGED
|
@@ -23,7 +23,7 @@ O smoonb faz backup completo de todos os componentes do seu projeto Supabase:
|
|
|
23
23
|
- ✅ **Custom Roles** (roles personalizados do PostgreSQL)
|
|
24
24
|
- ✅ **Edge Functions** (download automático do servidor)
|
|
25
25
|
- ✅ **Auth Settings** (configurações de autenticação via Management API)
|
|
26
|
-
- ✅ **Storage Buckets** (metadados e
|
|
26
|
+
- ✅ **Storage Buckets** (backup completo: metadados, configurações e todos os arquivos via Management API + Supabase Client, cria ZIP no padrão do Dashboard)
|
|
27
27
|
- ✅ **Realtime Settings** (7 parâmetros capturados interativamente)
|
|
28
28
|
- ✅ **Supabase .temp** (arquivos temporários do Supabase CLI)
|
|
29
29
|
- ✅ **Migrations** (todas as migrations do projeto via `supabase migration fetch`)
|
|
@@ -169,7 +169,7 @@ npx smoonb backup
|
|
|
169
169
|
4. **Backup do .env.local** - Cria backup automático antes de alterações
|
|
170
170
|
5. **Seleção de Componentes** - Pergunta quais componentes incluir:
|
|
171
171
|
- ⚡ Edge Functions (explicação sobre reset de link e download)
|
|
172
|
-
- 📦 Storage (explicação sobre
|
|
172
|
+
- 📦 Storage (explicação sobre backup completo: download de arquivos + ZIP no padrão do Dashboard)
|
|
173
173
|
- 🔐 Auth Settings (explicação sobre configurações)
|
|
174
174
|
- 🔄 Realtime Settings (explicação sobre captura interativa de 7 parâmetros)
|
|
175
175
|
- 🗑️ Opções de limpeza (functions, .temp, migrations após backup)
|
|
@@ -181,7 +181,7 @@ npx smoonb backup
|
|
|
181
181
|
- 🔧 3/10 - Backup Database Extensions and Settings
|
|
182
182
|
- 🔐 4/10 - Backup Auth Settings (se selecionado)
|
|
183
183
|
- 🔄 5/10 - Backup Realtime Settings (se selecionado) - 7 parâmetros capturados interativamente
|
|
184
|
-
- 📦 6/10 - Backup Storage (se selecionado)
|
|
184
|
+
- 📦 6/10 - Backup Storage (se selecionado) - Download completo de arquivos + ZIP no padrão do Dashboard
|
|
185
185
|
- 👥 7/10 - Backup Custom Roles
|
|
186
186
|
- ⚡ 8/10 - Backup Edge Functions (se selecionado)
|
|
187
187
|
- 📁 9/10 - Backup Supabase .temp (se selecionado)
|
|
@@ -199,6 +199,11 @@ backups/backup-2025-10-31-09-37-54/
|
|
|
199
199
|
├── auth-settings.json # Configurações de Auth
|
|
200
200
|
├── realtime-settings.json # Configurações de Realtime
|
|
201
201
|
├── storage/ # Metadados de Storage
|
|
202
|
+
│ └── [bucket-name].json # Metadados de cada bucket
|
|
203
|
+
├── [project-id].storage.zip # Backup completo de Storage (padrão Dashboard)
|
|
204
|
+
├── storage_temp/ # Estrutura temporária (opcional, pode ser removida)
|
|
205
|
+
│ └── [project-id]/ # Estrutura de arquivos baixados
|
|
206
|
+
│ └── [bucket-name]/ # Arquivos de cada bucket
|
|
202
207
|
├── edge-functions/ # Edge Functions baixadas
|
|
203
208
|
│ └── [nome-da-function]/
|
|
204
209
|
├── supabase-temp/ # Arquivos .temp do Supabase CLI
|
|
@@ -247,7 +252,7 @@ npx smoonb restore --file "backup.backup.gz" --storage "meu-projeto.storage.zip"
|
|
|
247
252
|
- 📊 Database - Restaura via `psql` (suporta `.backup.gz` e `.backup`)
|
|
248
253
|
- ⚡ Edge Functions - Copia e faz deploy no projeto destino
|
|
249
254
|
- 🔐 Auth Settings - Exibe configurações para aplicação manual
|
|
250
|
-
- 📦 Storage -
|
|
255
|
+
- 📦 Storage - Restaura buckets e arquivos do ZIP (se disponível) ou exibe informações para migração manual
|
|
251
256
|
- 🔧 Database Settings - Restaura extensões e configurações via SQL
|
|
252
257
|
- 🔄 Realtime Settings - Exibe configurações para aplicação manual
|
|
253
258
|
|
|
@@ -432,7 +437,17 @@ restore/
|
|
|
432
437
|
- **Reset de Link**: Garante link limpo com o projeto
|
|
433
438
|
- **Backup Completo**: Todas as migrations do servidor
|
|
434
439
|
|
|
435
|
-
####
|
|
440
|
+
#### Storage
|
|
441
|
+
- **Backup Completo**: Download de todos os arquivos de todos os buckets
|
|
442
|
+
- **Estrutura Temporária**: Cria `storage_temp/project-id/bucket-name/arquivos...` dentro do backupDir
|
|
443
|
+
- **ZIP no Padrão Dashboard**: Cria `{project-id}.storage.zip` com estrutura `project-id/bucket-name/arquivos...`
|
|
444
|
+
- **Compatível com Restore**: O ZIP criado é compatível com o processo de restore (mesmo formato do Dashboard)
|
|
445
|
+
- **Pergunta Interativa**: Após criar o ZIP, pergunta se deseja limpar a estrutura temporária
|
|
446
|
+
- **Fallback**: Se não houver credenciais do Supabase, faz backup apenas de metadados
|
|
447
|
+
- **Management API**: Usa Personal Access Token para listar buckets e objetos
|
|
448
|
+
- **Supabase Client**: Usa Service Role Key para download de arquivos
|
|
449
|
+
|
|
450
|
+
#### Auth, Realtime
|
|
436
451
|
- **Management API**: Usa Personal Access Token
|
|
437
452
|
- **JSON Export**: Configurações exportadas como JSON
|
|
438
453
|
- **Realtime Settings**: Captura interativa de 7 parâmetros:
|
|
@@ -460,7 +475,8 @@ restore/
|
|
|
460
475
|
|
|
461
476
|
#### Outros Componentes
|
|
462
477
|
- **Database Settings**: Restaura via SQL
|
|
463
|
-
- **
|
|
478
|
+
- **Storage**: Restaura buckets e arquivos do ZIP (se disponível) ou exibe informações para configuração manual
|
|
479
|
+
- **Auth/Realtime**: Exibe informações para configuração manual no Dashboard
|
|
464
480
|
|
|
465
481
|
### Multiplataforma
|
|
466
482
|
|
|
@@ -495,10 +511,10 @@ npx smoonb restore
|
|
|
495
511
|
# 6. Verificar integridade
|
|
496
512
|
npx smoonb check
|
|
497
513
|
|
|
498
|
-
# 7. Aplicar configurações manuais
|
|
514
|
+
# 7. Aplicar configurações manuais (se necessário)
|
|
499
515
|
# - Auth Settings: Dashboard → Authentication → Settings
|
|
500
|
-
# - Storage: Dashboard → Storage → Buckets
|
|
501
516
|
# - Realtime: Dashboard → Database → Replication
|
|
517
|
+
# Nota: Storage é restaurado automaticamente do ZIP se disponível
|
|
502
518
|
```
|
|
503
519
|
|
|
504
520
|
## 🎨 Experiência do Usuário
|
package/package.json
CHANGED
|
@@ -119,6 +119,8 @@ module.exports = async (options) => {
|
|
|
119
119
|
const projectId = getValue('SUPABASE_PROJECT_ID');
|
|
120
120
|
const accessToken = getValue('SUPABASE_ACCESS_TOKEN');
|
|
121
121
|
const databaseUrl = getValue('SUPABASE_DB_URL');
|
|
122
|
+
const supabaseUrl = getValue('NEXT_PUBLIC_SUPABASE_URL');
|
|
123
|
+
const supabaseServiceKey = getValue('SUPABASE_SERVICE_ROLE_KEY');
|
|
122
124
|
|
|
123
125
|
if (!databaseUrl) {
|
|
124
126
|
console.log(chalk.red('❌ DATABASE_URL NÃO CONFIGURADA'));
|
|
@@ -187,6 +189,8 @@ module.exports = async (options) => {
|
|
|
187
189
|
projectId,
|
|
188
190
|
accessToken,
|
|
189
191
|
databaseUrl,
|
|
192
|
+
supabaseUrl,
|
|
193
|
+
supabaseServiceKey,
|
|
190
194
|
backupDir: finalBackupDir,
|
|
191
195
|
outputDir: resolvedOutputDir,
|
|
192
196
|
options: { ...options, flags },
|
|
@@ -326,7 +330,11 @@ module.exports = async (options) => {
|
|
|
326
330
|
|
|
327
331
|
if (flags?.includeStorage && manifest.components.storage) {
|
|
328
332
|
const storageResult = manifest.components.storage;
|
|
329
|
-
|
|
333
|
+
if (storageResult.zipFile) {
|
|
334
|
+
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets, ${storageResult.totalFiles || 0} arquivo(s) baixado(s), ZIP: ${storageResult.zipFile} (${storageResult.zipSizeMB || 0} MB)`));
|
|
335
|
+
} else {
|
|
336
|
+
console.log(chalk.green(`📦 Storage: ${storageResult.buckets?.length || 0} buckets verificados via API (apenas metadados)`));
|
|
337
|
+
}
|
|
330
338
|
}
|
|
331
339
|
|
|
332
340
|
console.log(chalk.green(`👥 Custom Roles: ${rolesResult.roles?.length || 0} roles exportados via SQL`));
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const fs = require('fs').promises;
|
|
4
|
+
const AdmZip = require('adm-zip');
|
|
5
|
+
const { createClient } = require('@supabase/supabase-js');
|
|
3
6
|
const { ensureDir, writeJson } = require('../../../utils/fsx');
|
|
7
|
+
const { confirm } = require('../../../utils/prompt');
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Etapa 6: Backup Storage via Supabase API
|
|
11
|
+
* Agora faz backup completo: metadados + download de todos os arquivos + ZIP no padrão do Dashboard
|
|
7
12
|
*/
|
|
8
|
-
module.exports = async ({ projectId, accessToken, backupDir }) => {
|
|
13
|
+
module.exports = async ({ projectId, accessToken, backupDir, supabaseUrl, supabaseServiceKey }) => {
|
|
9
14
|
try {
|
|
10
15
|
const storageDir = path.join(backupDir, 'storage');
|
|
11
16
|
await ensureDir(storageDir);
|
|
@@ -37,7 +42,25 @@ module.exports = async ({ projectId, accessToken, backupDir }) => {
|
|
|
37
42
|
|
|
38
43
|
console.log(chalk.white(` - Encontrados ${buckets.length} buckets`));
|
|
39
44
|
|
|
45
|
+
// Validar credenciais do Supabase para download de arquivos
|
|
46
|
+
if (!supabaseUrl || !supabaseServiceKey) {
|
|
47
|
+
console.log(chalk.yellow(' ⚠️ Credenciais do Supabase não disponíveis. Fazendo backup apenas de metadados...'));
|
|
48
|
+
return await backupMetadataOnly(buckets, storageDir, projectId, accessToken);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Criar cliente Supabase para download de arquivos
|
|
52
|
+
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
53
|
+
|
|
54
|
+
// Criar estrutura temporária para armazenar arquivos baixados
|
|
55
|
+
const tempStorageDir = path.join(backupDir, 'storage_temp');
|
|
56
|
+
await ensureDir(tempStorageDir);
|
|
57
|
+
|
|
58
|
+
// Criar estrutura: storage_temp/project-id/bucket-name/arquivos...
|
|
59
|
+
const projectStorageDir = path.join(tempStorageDir, projectId);
|
|
60
|
+
await ensureDir(projectStorageDir);
|
|
61
|
+
|
|
40
62
|
const processedBuckets = [];
|
|
63
|
+
let totalFilesDownloaded = 0;
|
|
41
64
|
|
|
42
65
|
for (const bucket of buckets || []) {
|
|
43
66
|
try {
|
|
@@ -69,22 +92,222 @@ module.exports = async ({ projectId, accessToken, backupDir }) => {
|
|
|
69
92
|
const bucketPath = path.join(storageDir, `${bucket.name}.json`);
|
|
70
93
|
await writeJson(bucketPath, bucketInfo);
|
|
71
94
|
|
|
95
|
+
// Baixar todos os arquivos do bucket
|
|
96
|
+
const bucketDir = path.join(projectStorageDir, bucket.name);
|
|
97
|
+
await ensureDir(bucketDir);
|
|
98
|
+
|
|
99
|
+
// Listar todos os arquivos recursivamente usando Supabase client
|
|
100
|
+
console.log(chalk.white(` - Listando arquivos do bucket ${bucket.name}...`));
|
|
101
|
+
const allFiles = await listAllFilesRecursively(supabase, bucket.name, '');
|
|
102
|
+
|
|
103
|
+
let filesDownloaded = 0;
|
|
104
|
+
if (allFiles.length > 0) {
|
|
105
|
+
console.log(chalk.white(` - Baixando ${allFiles.length} arquivo(s) do bucket ${bucket.name}...`));
|
|
106
|
+
|
|
107
|
+
for (const filePath of allFiles) {
|
|
108
|
+
try {
|
|
109
|
+
// Baixar arquivo do Storage
|
|
110
|
+
const { data: fileData, error: downloadError } = await supabase.storage
|
|
111
|
+
.from(bucket.name)
|
|
112
|
+
.download(filePath);
|
|
113
|
+
|
|
114
|
+
if (downloadError) {
|
|
115
|
+
console.log(chalk.yellow(` ⚠️ Erro ao baixar ${filePath}: ${downloadError.message}`));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Criar estrutura de pastas local se necessário
|
|
120
|
+
const localFilePath = path.join(bucketDir, filePath);
|
|
121
|
+
const localFileDir = path.dirname(localFilePath);
|
|
122
|
+
await ensureDir(localFileDir);
|
|
123
|
+
|
|
124
|
+
// Salvar arquivo localmente
|
|
125
|
+
const arrayBuffer = await fileData.arrayBuffer();
|
|
126
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
127
|
+
await fs.writeFile(localFilePath, buffer);
|
|
128
|
+
filesDownloaded++;
|
|
129
|
+
|
|
130
|
+
// Mostrar progresso a cada 10 arquivos ou se for o último
|
|
131
|
+
if (filesDownloaded % 10 === 0 || filesDownloaded === allFiles.length) {
|
|
132
|
+
console.log(chalk.white(` - Baixados ${filesDownloaded}/${allFiles.length} arquivo(s)...`));
|
|
133
|
+
}
|
|
134
|
+
} catch (fileError) {
|
|
135
|
+
console.log(chalk.yellow(` ⚠️ Erro ao processar arquivo ${filePath}: ${fileError.message}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
totalFilesDownloaded += filesDownloaded;
|
|
72
141
|
processedBuckets.push({
|
|
73
142
|
name: bucket.name,
|
|
74
|
-
objectCount: objects?.length || 0
|
|
143
|
+
objectCount: objects?.length || 0,
|
|
144
|
+
filesDownloaded: filesDownloaded,
|
|
145
|
+
totalFiles: allFiles.length
|
|
75
146
|
});
|
|
76
147
|
|
|
77
|
-
console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${
|
|
148
|
+
console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${filesDownloaded}/${allFiles.length} arquivo(s) baixado(s)`));
|
|
78
149
|
} catch (error) {
|
|
79
150
|
console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
|
|
80
151
|
}
|
|
81
152
|
}
|
|
82
153
|
|
|
83
|
-
|
|
84
|
-
|
|
154
|
+
// Criar ZIP no padrão do Dashboard: {project-id}.storage.zip
|
|
155
|
+
console.log(chalk.white('\n - Criando arquivo ZIP no padrão do Dashboard...'));
|
|
156
|
+
const zipFileName = `${projectId}.storage.zip`;
|
|
157
|
+
const zipFilePath = path.join(backupDir, zipFileName);
|
|
158
|
+
|
|
159
|
+
const zip = new AdmZip();
|
|
160
|
+
|
|
161
|
+
// Adicionar toda a estrutura de pastas ao ZIP
|
|
162
|
+
// Estrutura: project-id/bucket-name/arquivos...
|
|
163
|
+
await addDirectoryToZip(zip, projectStorageDir, projectId);
|
|
164
|
+
|
|
165
|
+
// Salvar ZIP
|
|
166
|
+
zip.writeZip(zipFilePath);
|
|
167
|
+
const zipStats = await fs.stat(zipFilePath);
|
|
168
|
+
const zipSizeMB = (zipStats.size / (1024 * 1024)).toFixed(2);
|
|
169
|
+
|
|
170
|
+
console.log(chalk.green(` ✅ Arquivo ZIP criado: ${zipFileName} (${zipSizeMB} MB)`));
|
|
171
|
+
|
|
172
|
+
// Perguntar ao usuário se deseja limpar a estrutura temporária
|
|
173
|
+
const tempDirName = path.basename(tempStorageDir);
|
|
174
|
+
const shouldCleanup = await confirm(` Deseja limpar ${tempDirName} após o backup`, false);
|
|
175
|
+
|
|
176
|
+
if (shouldCleanup) {
|
|
177
|
+
console.log(chalk.white(` - Limpando estrutura temporária...`));
|
|
178
|
+
try {
|
|
179
|
+
await fs.rm(tempStorageDir, { recursive: true, force: true });
|
|
180
|
+
console.log(chalk.green(` ✅ Estrutura temporária removida`));
|
|
181
|
+
} catch (cleanupError) {
|
|
182
|
+
console.log(chalk.yellow(` ⚠️ Erro ao limpar estrutura temporária: ${cleanupError.message}`));
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
console.log(chalk.white(` ℹ️ Estrutura temporária mantida em: ${path.relative(process.cwd(), tempStorageDir)}`));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(chalk.green(`✅ Storage backupado: ${processedBuckets.length} buckets, ${totalFilesDownloaded} arquivo(s) baixado(s)`));
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
buckets: processedBuckets,
|
|
192
|
+
zipFile: zipFileName,
|
|
193
|
+
zipSizeMB: zipSizeMB,
|
|
194
|
+
totalFiles: totalFilesDownloaded,
|
|
195
|
+
tempDirCleaned: shouldCleanup
|
|
196
|
+
};
|
|
85
197
|
} catch (error) {
|
|
86
198
|
console.log(chalk.yellow(`⚠️ Erro no backup do Storage: ${error.message}`));
|
|
87
199
|
return { success: false, buckets: [] };
|
|
88
200
|
}
|
|
89
201
|
};
|
|
90
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Backup apenas de metadados (fallback quando não há credenciais do Supabase)
|
|
205
|
+
*/
|
|
206
|
+
async function backupMetadataOnly(buckets, storageDir, projectId, accessToken) {
|
|
207
|
+
const processedBuckets = [];
|
|
208
|
+
|
|
209
|
+
for (const bucket of buckets || []) {
|
|
210
|
+
try {
|
|
211
|
+
console.log(chalk.white(` - Processando bucket: ${bucket.name}`));
|
|
212
|
+
|
|
213
|
+
const objectsResponse = await fetch(`https://api.supabase.com/v1/projects/${projectId}/storage/buckets/${bucket.name}/objects`, {
|
|
214
|
+
headers: {
|
|
215
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
216
|
+
'Content-Type': 'application/json'
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let objects = [];
|
|
221
|
+
if (objectsResponse.ok) {
|
|
222
|
+
objects = await objectsResponse.json();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const bucketInfo = {
|
|
226
|
+
id: bucket.id,
|
|
227
|
+
name: bucket.name,
|
|
228
|
+
public: bucket.public,
|
|
229
|
+
file_size_limit: bucket.file_size_limit,
|
|
230
|
+
allowed_mime_types: bucket.allowed_mime_types,
|
|
231
|
+
objects: objects || []
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const bucketPath = path.join(storageDir, `${bucket.name}.json`);
|
|
235
|
+
await writeJson(bucketPath, bucketInfo);
|
|
236
|
+
|
|
237
|
+
processedBuckets.push({
|
|
238
|
+
name: bucket.name,
|
|
239
|
+
objectCount: objects?.length || 0
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
console.log(chalk.green(` ✅ Bucket ${bucket.name}: ${objects?.length || 0} objetos`));
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.log(chalk.yellow(` ⚠️ Erro ao processar bucket ${bucket.name}: ${error.message}`));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log(chalk.green(`✅ Storage backupado (apenas metadados): ${processedBuckets.length} buckets`));
|
|
249
|
+
return { success: true, buckets: processedBuckets };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Lista todos os arquivos recursivamente de um bucket do Storage
|
|
254
|
+
*/
|
|
255
|
+
async function listAllFilesRecursively(supabase, bucketName, folderPath = '') {
|
|
256
|
+
const allFiles = [];
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
// Listar arquivos e pastas no caminho atual
|
|
260
|
+
const { data: items, error } = await supabase.storage
|
|
261
|
+
.from(bucketName)
|
|
262
|
+
.list(folderPath, {
|
|
263
|
+
limit: 1000,
|
|
264
|
+
sortBy: { column: 'name', order: 'asc' }
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (error) {
|
|
268
|
+
console.log(chalk.yellow(` ⚠️ Erro ao listar ${folderPath || 'raiz'}: ${error.message}`));
|
|
269
|
+
return allFiles;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!items || items.length === 0) {
|
|
273
|
+
return allFiles;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
for (const item of items) {
|
|
277
|
+
const itemPath = folderPath ? `${folderPath}/${item.name}` : item.name;
|
|
278
|
+
|
|
279
|
+
if (item.id === null) {
|
|
280
|
+
// É uma pasta, listar recursivamente
|
|
281
|
+
const subFiles = await listAllFilesRecursively(supabase, bucketName, itemPath);
|
|
282
|
+
allFiles.push(...subFiles);
|
|
283
|
+
} else {
|
|
284
|
+
// É um arquivo
|
|
285
|
+
allFiles.push(itemPath);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.log(chalk.yellow(` ⚠️ Erro ao processar ${folderPath || 'raiz'}: ${error.message}`));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return allFiles;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Adiciona um diretório recursivamente ao ZIP mantendo a estrutura de pastas
|
|
297
|
+
*/
|
|
298
|
+
async function addDirectoryToZip(zip, dirPath, basePath = '') {
|
|
299
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
300
|
+
|
|
301
|
+
for (const entry of entries) {
|
|
302
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
303
|
+
const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
304
|
+
|
|
305
|
+
if (entry.isDirectory()) {
|
|
306
|
+
await addDirectoryToZip(zip, fullPath, zipPath);
|
|
307
|
+
} else {
|
|
308
|
+
const fileContent = await fs.readFile(fullPath);
|
|
309
|
+
zip.addFile(zipPath, fileContent);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
@@ -100,6 +100,7 @@ module.exports = async ({ backupPath, targetProject }) => {
|
|
|
100
100
|
|
|
101
101
|
// 3. Selecionar o primeiro arquivo .storage.zip encontrado
|
|
102
102
|
const storageZipFile = path.join(backupPath, storageZipFiles[0]);
|
|
103
|
+
const storageZipBaseName = path.basename(storageZipFiles[0], '.storage.zip');
|
|
103
104
|
console.log(chalk.white(` - Arquivo de storage encontrado: ${storageZipFiles[0]}`));
|
|
104
105
|
|
|
105
106
|
// 4. Validar credenciais do projeto destino
|
|
@@ -155,17 +156,30 @@ module.exports = async ({ backupPath, targetProject }) => {
|
|
|
155
156
|
const firstItem = extractedContents[0];
|
|
156
157
|
const firstItemPath = path.join(extractDir, firstItem);
|
|
157
158
|
const firstItemStats = await fs.stat(firstItemPath);
|
|
158
|
-
|
|
159
|
+
|
|
159
160
|
if (firstItemStats.isDirectory()) {
|
|
160
161
|
// Verificar se o nome da pasta raiz corresponde ao Project ID antigo OU novo
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
const matchesSourceProjectId = sourceProjectId && firstItem === sourceProjectId;
|
|
163
|
+
const matchesTargetProjectId = firstItem === targetProject.targetProjectId;
|
|
164
|
+
const matchesZipFileName = firstItem === storageZipBaseName;
|
|
165
|
+
const matchesProjectIdPattern = isLikelyProjectId(firstItem);
|
|
166
|
+
|
|
167
|
+
const isProjectId =
|
|
168
|
+
matchesSourceProjectId ||
|
|
169
|
+
matchesTargetProjectId ||
|
|
170
|
+
matchesZipFileName ||
|
|
171
|
+
matchesProjectIdPattern;
|
|
172
|
+
|
|
165
173
|
if (isProjectId) {
|
|
166
174
|
// A pasta raiz é um wrapper do Project ID - SEMPRE buscar buckets nas subpastas
|
|
167
175
|
rootDir = firstItem;
|
|
168
|
-
|
|
176
|
+
const reasons = [];
|
|
177
|
+
if (matchesSourceProjectId) reasons.push('manifest');
|
|
178
|
+
if (matchesTargetProjectId) reasons.push('projeto destino');
|
|
179
|
+
if (matchesZipFileName) reasons.push('nome do arquivo ZIP');
|
|
180
|
+
if (matchesProjectIdPattern) reasons.push('formato de Project ID');
|
|
181
|
+
const reasonText = reasons.length ? ` (${reasons.join(', ')})` : '';
|
|
182
|
+
console.log(chalk.white(` - Detectada pasta raiz com Project ID: ${firstItem}${reasonText}`));
|
|
169
183
|
console.log(chalk.white(` - Buscando buckets nas subpastas...`));
|
|
170
184
|
}
|
|
171
185
|
}
|
|
@@ -551,3 +565,12 @@ function getContentType(fileName) {
|
|
|
551
565
|
|
|
552
566
|
return contentTypes[ext] || 'application/octet-stream';
|
|
553
567
|
}
|
|
568
|
+
|
|
569
|
+
function isLikelyProjectId(name) {
|
|
570
|
+
if (!name || typeof name !== 'string') {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Project IDs do Supabase são normalmente strings de 20 caracteres alfanuméricos minúsculos
|
|
575
|
+
return /^[a-z0-9]{20}$/.test(name);
|
|
576
|
+
}
|