smoonb 0.0.64 → 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.
|
|
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"
|
|
@@ -298,7 +298,8 @@ module.exports = async (options) => {
|
|
|
298
298
|
stepNumber++;
|
|
299
299
|
console.log(chalk.blue(`\n📦 ${stepNumber}/${totalSteps} - Restaurando Storage Buckets...`));
|
|
300
300
|
const storageResult = await step06Storage({
|
|
301
|
-
backupPath: selectedBackup.path
|
|
301
|
+
backupPath: selectedBackup.path,
|
|
302
|
+
targetProject
|
|
302
303
|
});
|
|
303
304
|
restoreResults.storage = storageResult || { success: true };
|
|
304
305
|
}
|
|
@@ -366,7 +367,14 @@ module.exports = async (options) => {
|
|
|
366
367
|
|
|
367
368
|
if (restoreResults.storage) {
|
|
368
369
|
const bucketCount = restoreResults.storage.buckets_count || 0;
|
|
369
|
-
|
|
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
|
+
}
|
|
370
378
|
}
|
|
371
379
|
|
|
372
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) {
|