smoonb 0.0.65 → 0.0.67

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.65",
3
+ "version": "0.0.67",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -31,10 +31,24 @@ 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
+ let storageZipFiles = [];
35
+
36
+ // Verificar se o diretório existe e listar arquivos
37
+ try {
38
+ if (fs.existsSync(backupPath)) {
39
+ const files = fs.readdirSync(backupPath);
40
+ storageZipFiles = files.filter(f => f.endsWith('.storage.zip'));
41
+ }
42
+ } catch {
43
+ // Ignorar erro ao ler diretório
44
+ storageZipFiles = [];
45
+ }
46
+
35
47
  let restoreStorage = false;
48
+ const hasStorageDir = fs.existsSync(storageDir);
49
+ const hasStorageFiles = hasStorageDir && fs.readdirSync(storageDir).length > 0;
36
50
 
37
- if (storageZipFiles.length > 0 || (fs.existsSync(storageDir) && fs.readdirSync(storageDir).length > 0)) {
51
+ if (storageZipFiles.length > 0 || hasStorageFiles) {
38
52
  console.log(chalk.cyan('\n📦 Storage:'));
39
53
  if (storageZipFiles.length > 0) {
40
54
  console.log(chalk.white(` Arquivo .storage.zip encontrado: ${storageZipFiles[0]}`));
@@ -22,11 +22,15 @@ module.exports = async ({ backupPath, targetProject }) => {
22
22
  const manifestPath = path.join(backupPath, 'backup-manifest.json');
23
23
  const bucketMetadata = {};
24
24
  let manifest = null;
25
+ let sourceProjectId = null;
25
26
 
26
27
  try {
27
28
  const manifestContent = await fs.readFile(manifestPath, 'utf8');
28
29
  manifest = JSON.parse(manifestContent);
29
30
 
31
+ // Obter project ID do projeto origem do manifest
32
+ sourceProjectId = manifest?.project_id || null;
33
+
30
34
  // Carregar metadados dos buckets do manifest
31
35
  const buckets = manifest?.components?.storage?.buckets || [];
32
36
  for (const bucket of buckets) {
@@ -107,6 +111,13 @@ module.exports = async ({ backupPath, targetProject }) => {
107
111
  throw new Error('Credenciais do Supabase não configuradas. É necessário NEXT_PUBLIC_SUPABASE_URL e SUPABASE_SERVICE_ROLE_KEY');
108
112
  }
109
113
 
114
+ // 4.1 Obter project ID do projeto origem e validar substituição
115
+ if (sourceProjectId && sourceProjectId !== targetProject.targetProjectId) {
116
+ console.log(chalk.cyan(` 🔄 Substituindo Project ID: ${sourceProjectId} → ${targetProject.targetProjectId}`));
117
+ } else if (!sourceProjectId) {
118
+ console.log(chalk.yellow(' ⚠️ Project ID do projeto origem não encontrado no manifest. Continuando sem substituição...'));
119
+ }
120
+
110
121
  // 5. Extrair arquivo ZIP
111
122
  console.log(chalk.white(' - Extraindo arquivo .storage.zip...'));
112
123
  const extractDir = path.join(backupPath, 'storage_extracted');
@@ -121,20 +132,85 @@ module.exports = async ({ backupPath, targetProject }) => {
121
132
  zip.extractAllTo(extractDir, true);
122
133
  console.log(chalk.green(' ✅ Arquivo extraído com sucesso'));
123
134
 
135
+ // 5.1 Substituir Project ID antigo pelo novo nos diretórios e arquivos extraídos
136
+ // O Supabase pode incluir o project ID nos caminhos dentro do ZIP
137
+ if (sourceProjectId && sourceProjectId !== targetProject.targetProjectId) {
138
+ console.log(chalk.white(' - Substituindo referências ao Project ID antigo nos arquivos extraídos...'));
139
+ await replaceProjectIdInExtractedFiles(extractDir, sourceProjectId, targetProject.targetProjectId);
140
+ console.log(chalk.green(' ✅ Substituição de Project ID concluída'));
141
+ }
142
+
124
143
  // 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
144
+ // O formato do .storage.zip do Supabase pode ter duas estruturas:
145
+ // Estrutura 1 (direta): bucket-name/file1.jpg
146
+ // Estrutura 2 (com Project ID): project-id/bucket-name/file1.jpg
147
+ // Após a substituição do Project ID, a estrutura 2 fica: project-id-novo/bucket-name/file1.jpg
130
148
  const extractedContents = await fs.readdir(extractDir);
131
149
  const bucketDirs = [];
132
150
 
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);
151
+ // Verificar se a pasta raiz é o Project ID do destino (após substituição)
152
+ // Se for, as subpastas são os buckets reais
153
+ let rootDir = null;
154
+ if (extractedContents.length === 1) {
155
+ const firstItem = extractedContents[0];
156
+ const firstItemPath = path.join(extractDir, firstItem);
157
+ const firstItemStats = await fs.stat(firstItemPath);
158
+
159
+ if (firstItemStats.isDirectory()) {
160
+ // Verificar se o nome da pasta raiz corresponde ao Project ID do destino
161
+ // Isso pode acontecer se a pasta raiz original era o Project ID antigo
162
+ // e foi renomeada para o Project ID novo pela função replaceProjectIdInExtractedFiles
163
+ if (firstItem === targetProject.targetProjectId) {
164
+ // Verificar se contém subpastas (buckets reais)
165
+ const subContents = await fs.readdir(firstItemPath);
166
+ const hasSubDirs = subContents.some(item => {
167
+ const itemPath = path.join(firstItemPath, item);
168
+ try {
169
+ const stats = fs.statSync(itemPath);
170
+ return stats.isDirectory();
171
+ } catch {
172
+ return false;
173
+ }
174
+ });
175
+
176
+ if (hasSubDirs) {
177
+ // A pasta raiz é um wrapper do Project ID - buscar buckets nas subpastas
178
+ rootDir = firstItem;
179
+ console.log(chalk.white(` - Detectada pasta raiz com Project ID do destino: ${firstItem}`));
180
+ console.log(chalk.white(` - Buscando buckets nas subpastas...`));
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ if (rootDir) {
187
+ // Estrutura com Project ID: project-id/bucket-name/...
188
+ // Listar subpastas dentro da pasta do Project ID - essas são os buckets reais
189
+ const projectIdPath = path.join(extractDir, rootDir);
190
+ const subContents = await fs.readdir(projectIdPath);
191
+
192
+ for (const item of subContents) {
193
+ const itemPath = path.join(projectIdPath, item);
194
+ const stats = await fs.stat(itemPath);
195
+ if (stats.isDirectory()) {
196
+ bucketDirs.push({
197
+ name: item,
198
+ path: itemPath
199
+ });
200
+ }
201
+ }
202
+ } else {
203
+ // Estrutura direta: bucket-name/...
204
+ // As pastas raiz são os buckets
205
+ for (const item of extractedContents) {
206
+ const itemPath = path.join(extractDir, item);
207
+ const stats = await fs.stat(itemPath);
208
+ if (stats.isDirectory()) {
209
+ bucketDirs.push({
210
+ name: item,
211
+ path: itemPath
212
+ });
213
+ }
138
214
  }
139
215
  }
140
216
 
@@ -152,12 +228,13 @@ module.exports = async ({ backupPath, targetProject }) => {
152
228
  let successCount = 0;
153
229
  let totalFilesUploaded = 0;
154
230
 
155
- for (const bucketName of bucketDirs) {
231
+ for (const bucketInfo of bucketDirs) {
232
+ const bucketName = bucketInfo.name;
233
+ const bucketPath = bucketInfo.path;
234
+
156
235
  try {
157
236
  console.log(chalk.white(`\n - Processando bucket: ${bucketName}`));
158
237
 
159
- const bucketPath = path.join(extractDir, bucketName);
160
-
161
238
  // 8.1 Obter metadados do bucket do backup (se disponíveis)
162
239
  const bucketMeta = bucketMetadata[bucketName] || {
163
240
  public: false,
@@ -249,7 +326,11 @@ module.exports = async ({ backupPath, targetProject }) => {
249
326
 
250
327
  for (const entry of entries) {
251
328
  const fullPath = path.join(dir, entry.name);
252
- const relativePath = path.join(basePath, entry.name).replace(/\\/g, '/');
329
+ // Substituir project ID antigo pelo novo no caminho relativo
330
+ let relativePath = path.join(basePath, entry.name).replace(/\\/g, '/');
331
+ if (sourceProjectId && sourceProjectId !== targetProject.targetProjectId) {
332
+ relativePath = relativePath.replace(new RegExp(sourceProjectId, 'g'), targetProject.targetProjectId);
333
+ }
253
334
 
254
335
  if (entry.isDirectory()) {
255
336
  const subFiles = await getAllFiles(fullPath, relativePath);
@@ -381,6 +462,75 @@ module.exports = async ({ backupPath, targetProject }) => {
381
462
  }
382
463
  };
383
464
 
465
+ /**
466
+ * Substitui o Project ID antigo pelo novo em arquivos extraídos
467
+ * Processa recursivamente todos os arquivos e diretórios
468
+ * IMPORTANTE: Processa primeiro os filhos, depois renomeia o diretório atual
469
+ * para evitar problemas com caminhos que mudam durante o processamento
470
+ */
471
+ async function replaceProjectIdInExtractedFiles(dir, oldProjectId, newProjectId) {
472
+ const entries = await fs.readdir(dir, { withFileTypes: true });
473
+
474
+ // Primeiro, processar recursivamente todos os filhos (arquivos e subdiretórios)
475
+ for (const entry of entries) {
476
+ const entryPath = path.join(dir, entry.name);
477
+
478
+ if (entry.isDirectory()) {
479
+ // Processar recursivamente o subdiretório ANTES de renomeá-lo
480
+ await replaceProjectIdInExtractedFiles(entryPath, oldProjectId, newProjectId);
481
+ } else {
482
+ // Processar arquivos: substituir project ID no conteúdo (se for texto) e no nome
483
+ // Renomear arquivo se contiver o project ID antigo
484
+ if (entry.name.includes(oldProjectId)) {
485
+ const newName = entry.name.replace(new RegExp(oldProjectId, 'g'), newProjectId);
486
+ if (newName !== entry.name) {
487
+ const newPath = path.join(dir, newName);
488
+ try {
489
+ await fs.rename(entryPath, newPath);
490
+ } catch {
491
+ // Ignorar erros de renomeação (pode já ter sido renomeado)
492
+ }
493
+ }
494
+ }
495
+
496
+ // Substituir project ID no conteúdo de arquivos de texto
497
+ // Verificar extensões comuns de arquivos de texto
498
+ const textExtensions = ['.json', '.txt', '.md', '.html', '.css', '.js', '.xml', '.yaml', '.yml'];
499
+ const ext = path.extname(entry.name).toLowerCase();
500
+
501
+ if (textExtensions.includes(ext)) {
502
+ try {
503
+ const content = await fs.readFile(entryPath, 'utf8');
504
+ if (content.includes(oldProjectId)) {
505
+ const newContent = content.replace(new RegExp(oldProjectId, 'g'), newProjectId);
506
+ await fs.writeFile(entryPath, newContent, 'utf8');
507
+ }
508
+ } catch {
509
+ // Ignorar erros ao processar arquivos (pode ser binário ou sem permissão)
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ // Depois de processar todos os filhos, renomear o diretório atual se necessário
516
+ // Ler novamente os diretórios para pegar os nomes atualizados
517
+ const updatedEntries = await fs.readdir(dir, { withFileTypes: true });
518
+ for (const entry of updatedEntries) {
519
+ if (entry.isDirectory() && entry.name.includes(oldProjectId)) {
520
+ const entryPath = path.join(dir, entry.name);
521
+ const newName = entry.name.replace(new RegExp(oldProjectId, 'g'), newProjectId);
522
+ if (newName !== entry.name) {
523
+ const newPath = path.join(dir, newName);
524
+ try {
525
+ await fs.rename(entryPath, newPath);
526
+ } catch {
527
+ // Ignorar erros de renomeação (pode já ter sido renomeado)
528
+ }
529
+ }
530
+ }
531
+ }
532
+ }
533
+
384
534
  /**
385
535
  * Determina o content-type baseado na extensão do arquivo
386
536
  */