smoonb 0.0.66 → 0.0.68

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.66",
3
+ "version": "0.0.68",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -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,89 @@ 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 = [];
150
+
151
+ // Verificar se a pasta raiz é o Project ID (antigo ou novo)
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 antigo OU novo
161
+ // Isso pode acontecer se a pasta raiz original era o Project ID antigo
162
+ // e pode ou não ter sido renomeada para o Project ID novo pela função replaceProjectIdInExtractedFiles
163
+ const isProjectId =
164
+ (sourceProjectId && firstItem === sourceProjectId) ||
165
+ (firstItem === targetProject.targetProjectId);
166
+
167
+ if (isProjectId) {
168
+ // Verificar se contém subpastas (buckets reais)
169
+ const subContents = await fs.readdir(firstItemPath);
170
+ const hasSubDirs = subContents.some(item => {
171
+ const itemPath = path.join(firstItemPath, item);
172
+ try {
173
+ const stats = fs.statSync(itemPath);
174
+ return stats.isDirectory();
175
+ } catch {
176
+ return false;
177
+ }
178
+ });
179
+
180
+ if (hasSubDirs) {
181
+ // A pasta raiz é um wrapper do Project ID - buscar buckets nas subpastas
182
+ rootDir = firstItem;
183
+ console.log(chalk.white(` - Detectada pasta raiz com Project ID: ${firstItem}`));
184
+ console.log(chalk.white(` - Buscando buckets nas subpastas...`));
185
+ }
186
+ }
187
+ }
188
+ }
132
189
 
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);
190
+ if (rootDir) {
191
+ // Estrutura com Project ID: project-id/bucket-name/...
192
+ // Listar subpastas dentro da pasta do Project ID - essas são os buckets reais
193
+ const projectIdPath = path.join(extractDir, rootDir);
194
+ const subContents = await fs.readdir(projectIdPath);
195
+
196
+ for (const item of subContents) {
197
+ const itemPath = path.join(projectIdPath, item);
198
+ const stats = await fs.stat(itemPath);
199
+ if (stats.isDirectory()) {
200
+ bucketDirs.push({
201
+ name: item,
202
+ path: itemPath
203
+ });
204
+ }
205
+ }
206
+ } else {
207
+ // Estrutura direta: bucket-name/...
208
+ // As pastas raiz são os buckets
209
+ for (const item of extractedContents) {
210
+ const itemPath = path.join(extractDir, item);
211
+ const stats = await fs.stat(itemPath);
212
+ if (stats.isDirectory()) {
213
+ bucketDirs.push({
214
+ name: item,
215
+ path: itemPath
216
+ });
217
+ }
138
218
  }
139
219
  }
140
220
 
@@ -152,12 +232,13 @@ module.exports = async ({ backupPath, targetProject }) => {
152
232
  let successCount = 0;
153
233
  let totalFilesUploaded = 0;
154
234
 
155
- for (const bucketName of bucketDirs) {
235
+ for (const bucketInfo of bucketDirs) {
236
+ const bucketName = bucketInfo.name;
237
+ const bucketPath = bucketInfo.path;
238
+
156
239
  try {
157
240
  console.log(chalk.white(`\n - Processando bucket: ${bucketName}`));
158
241
 
159
- const bucketPath = path.join(extractDir, bucketName);
160
-
161
242
  // 8.1 Obter metadados do bucket do backup (se disponíveis)
162
243
  const bucketMeta = bucketMetadata[bucketName] || {
163
244
  public: false,
@@ -249,7 +330,11 @@ module.exports = async ({ backupPath, targetProject }) => {
249
330
 
250
331
  for (const entry of entries) {
251
332
  const fullPath = path.join(dir, entry.name);
252
- const relativePath = path.join(basePath, entry.name).replace(/\\/g, '/');
333
+ // Substituir project ID antigo pelo novo no caminho relativo
334
+ let relativePath = path.join(basePath, entry.name).replace(/\\/g, '/');
335
+ if (sourceProjectId && sourceProjectId !== targetProject.targetProjectId) {
336
+ relativePath = relativePath.replace(new RegExp(sourceProjectId, 'g'), targetProject.targetProjectId);
337
+ }
253
338
 
254
339
  if (entry.isDirectory()) {
255
340
  const subFiles = await getAllFiles(fullPath, relativePath);
@@ -381,6 +466,75 @@ module.exports = async ({ backupPath, targetProject }) => {
381
466
  }
382
467
  };
383
468
 
469
+ /**
470
+ * Substitui o Project ID antigo pelo novo em arquivos extraídos
471
+ * Processa recursivamente todos os arquivos e diretórios
472
+ * IMPORTANTE: Processa primeiro os filhos, depois renomeia o diretório atual
473
+ * para evitar problemas com caminhos que mudam durante o processamento
474
+ */
475
+ async function replaceProjectIdInExtractedFiles(dir, oldProjectId, newProjectId) {
476
+ const entries = await fs.readdir(dir, { withFileTypes: true });
477
+
478
+ // Primeiro, processar recursivamente todos os filhos (arquivos e subdiretórios)
479
+ for (const entry of entries) {
480
+ const entryPath = path.join(dir, entry.name);
481
+
482
+ if (entry.isDirectory()) {
483
+ // Processar recursivamente o subdiretório ANTES de renomeá-lo
484
+ await replaceProjectIdInExtractedFiles(entryPath, oldProjectId, newProjectId);
485
+ } else {
486
+ // Processar arquivos: substituir project ID no conteúdo (se for texto) e no nome
487
+ // Renomear arquivo se contiver o project ID antigo
488
+ if (entry.name.includes(oldProjectId)) {
489
+ const newName = entry.name.replace(new RegExp(oldProjectId, 'g'), newProjectId);
490
+ if (newName !== entry.name) {
491
+ const newPath = path.join(dir, newName);
492
+ try {
493
+ await fs.rename(entryPath, newPath);
494
+ } catch {
495
+ // Ignorar erros de renomeação (pode já ter sido renomeado)
496
+ }
497
+ }
498
+ }
499
+
500
+ // Substituir project ID no conteúdo de arquivos de texto
501
+ // Verificar extensões comuns de arquivos de texto
502
+ const textExtensions = ['.json', '.txt', '.md', '.html', '.css', '.js', '.xml', '.yaml', '.yml'];
503
+ const ext = path.extname(entry.name).toLowerCase();
504
+
505
+ if (textExtensions.includes(ext)) {
506
+ try {
507
+ const content = await fs.readFile(entryPath, 'utf8');
508
+ if (content.includes(oldProjectId)) {
509
+ const newContent = content.replace(new RegExp(oldProjectId, 'g'), newProjectId);
510
+ await fs.writeFile(entryPath, newContent, 'utf8');
511
+ }
512
+ } catch {
513
+ // Ignorar erros ao processar arquivos (pode ser binário ou sem permissão)
514
+ }
515
+ }
516
+ }
517
+ }
518
+
519
+ // Depois de processar todos os filhos, renomear o diretório atual se necessário
520
+ // Ler novamente os diretórios para pegar os nomes atualizados
521
+ const updatedEntries = await fs.readdir(dir, { withFileTypes: true });
522
+ for (const entry of updatedEntries) {
523
+ if (entry.isDirectory() && entry.name.includes(oldProjectId)) {
524
+ const entryPath = path.join(dir, entry.name);
525
+ const newName = entry.name.replace(new RegExp(oldProjectId, 'g'), newProjectId);
526
+ if (newName !== entry.name) {
527
+ const newPath = path.join(dir, newName);
528
+ try {
529
+ await fs.rename(entryPath, newPath);
530
+ } catch {
531
+ // Ignorar erros de renomeação (pode já ter sido renomeado)
532
+ }
533
+ }
534
+ }
535
+ }
536
+ }
537
+
384
538
  /**
385
539
  * Determina o content-type baseado na extensão do arquivo
386
540
  */