@aether-baas/core 3.6.1 → 3.6.4
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/dist/aether.d.ts.map +1 -1
- package/dist/aether.js +14 -6
- package/dist/aether.js.map +1 -1
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +18 -2
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/storage.d.ts +190 -30
- package/dist/modules/storage.d.ts.map +1 -1
- package/dist/modules/storage.js +450 -143
- package/dist/modules/storage.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/result.d.ts +204 -0
- package/dist/types/result.d.ts.map +1 -0
- package/dist/types/result.js +166 -0
- package/dist/types/result.js.map +1 -0
- package/package.json +9 -8
package/dist/modules/storage.js
CHANGED
|
@@ -1,26 +1,87 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// @aether/core - Storage Module
|
|
2
|
+
// @aether-baas/core - Storage Module (CORRIGIDO)
|
|
3
3
|
// =============================================================================
|
|
4
|
-
// Módulo de storage
|
|
4
|
+
// Módulo para operações de storage (upload, download, delete).
|
|
5
|
+
//
|
|
6
|
+
// CORREÇÕES APLICADAS:
|
|
7
|
+
// 1. Padronizado retorno para Result<T> em todos os métodos
|
|
8
|
+
// 2. Assinatura unificada com options object
|
|
9
|
+
// 3. Validações robustas de entrada
|
|
10
|
+
// 4. Tratamento de erros consistente
|
|
11
|
+
// 5. Documentação JSDoc completa
|
|
5
12
|
// =============================================================================
|
|
6
13
|
import axios from 'axios';
|
|
14
|
+
import { ok, err } from '../types/result.js';
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// CONSTANTES
|
|
17
|
+
// =============================================================================
|
|
18
|
+
/** Timeout padrão para uploads (5 minutos) */
|
|
19
|
+
const UPLOAD_TIMEOUT = 300000;
|
|
20
|
+
/** Mapeamento de extensões para MIME types */
|
|
21
|
+
const MIME_TYPES = {
|
|
22
|
+
// Imagens
|
|
23
|
+
jpg: 'image/jpeg',
|
|
24
|
+
jpeg: 'image/jpeg',
|
|
25
|
+
png: 'image/png',
|
|
26
|
+
gif: 'image/gif',
|
|
27
|
+
webp: 'image/webp',
|
|
28
|
+
svg: 'image/svg+xml',
|
|
29
|
+
ico: 'image/x-icon',
|
|
30
|
+
// Documentos
|
|
31
|
+
pdf: 'application/pdf',
|
|
32
|
+
doc: 'application/msword',
|
|
33
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
34
|
+
xls: 'application/vnd.ms-excel',
|
|
35
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
36
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
37
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
38
|
+
// Texto
|
|
39
|
+
txt: 'text/plain',
|
|
40
|
+
csv: 'text/csv',
|
|
41
|
+
json: 'application/json',
|
|
42
|
+
xml: 'application/xml',
|
|
43
|
+
html: 'text/html',
|
|
44
|
+
css: 'text/css',
|
|
45
|
+
js: 'application/javascript',
|
|
46
|
+
// Áudio
|
|
47
|
+
mp3: 'audio/mpeg',
|
|
48
|
+
wav: 'audio/wav',
|
|
49
|
+
ogg: 'audio/ogg',
|
|
50
|
+
// Vídeo
|
|
51
|
+
mp4: 'video/mp4',
|
|
52
|
+
webm: 'video/webm',
|
|
53
|
+
avi: 'video/x-msvideo',
|
|
54
|
+
// Arquivos
|
|
55
|
+
zip: 'application/zip',
|
|
56
|
+
rar: 'application/vnd.rar',
|
|
57
|
+
tar: 'application/x-tar',
|
|
58
|
+
gz: 'application/gzip',
|
|
59
|
+
};
|
|
7
60
|
// =============================================================================
|
|
8
61
|
// STORAGE MODULE
|
|
9
62
|
// =============================================================================
|
|
10
63
|
/**
|
|
11
|
-
* Módulo de storage
|
|
64
|
+
* Módulo para operações de storage.
|
|
12
65
|
*
|
|
13
66
|
* @example
|
|
14
67
|
* ```typescript
|
|
15
|
-
* // Upload
|
|
16
|
-
* const
|
|
17
|
-
*
|
|
68
|
+
* // Upload de arquivo
|
|
69
|
+
* const result = await aether.storage.upload(file, {
|
|
70
|
+
* folder: 'avatars',
|
|
71
|
+
* onProgress: (p) => console.log(`${p.percent}%`)
|
|
72
|
+
* });
|
|
18
73
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
74
|
+
* if (result.success) {
|
|
75
|
+
* console.log('URL:', result.data.downloadUrl);
|
|
76
|
+
* } else {
|
|
77
|
+
* console.error('Erro:', result.error);
|
|
78
|
+
* }
|
|
21
79
|
*
|
|
22
|
-
* //
|
|
23
|
-
* await aether.storage.
|
|
80
|
+
* // Listar arquivos
|
|
81
|
+
* const files = await aether.storage.list('avatars');
|
|
82
|
+
*
|
|
83
|
+
* // Deletar arquivo
|
|
84
|
+
* await aether.storage.delete(fileId);
|
|
24
85
|
* ```
|
|
25
86
|
*/
|
|
26
87
|
export class StorageModule {
|
|
@@ -30,202 +91,448 @@ export class StorageModule {
|
|
|
30
91
|
this.client = client;
|
|
31
92
|
this.http = http;
|
|
32
93
|
}
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
// UPLOAD
|
|
96
|
+
// ===========================================================================
|
|
33
97
|
/**
|
|
34
|
-
* Faz upload de um arquivo.
|
|
98
|
+
* Faz upload de um arquivo para o storage.
|
|
35
99
|
*
|
|
36
|
-
* @param file Arquivo a ser enviado (File, Blob ou Buffer)
|
|
37
|
-
* @param
|
|
38
|
-
* @
|
|
39
|
-
* @param folder Pasta de destino (opcional)
|
|
40
|
-
* @param onProgress Callback de progresso (opcional)
|
|
100
|
+
* @param file - Arquivo a ser enviado (File, Blob ou Buffer)
|
|
101
|
+
* @param options - Opções de upload (fileName, contentType, folder, onProgress)
|
|
102
|
+
* @returns Result com FileObject em caso de sucesso, ou erro em caso de falha
|
|
41
103
|
*
|
|
42
104
|
* @example
|
|
43
105
|
* ```typescript
|
|
44
|
-
*
|
|
45
|
-
*
|
|
106
|
+
* // Upload básico
|
|
107
|
+
* const result = await storage.upload(file);
|
|
108
|
+
*
|
|
109
|
+
* // Upload com opções
|
|
110
|
+
* const result = await storage.upload(file, {
|
|
111
|
+
* fileName: 'avatar.jpg',
|
|
112
|
+
* folder: 'users/123',
|
|
113
|
+
* contentType: 'image/jpeg',
|
|
114
|
+
* onProgress: (p) => setProgress(p.percent)
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* // Tratamento do resultado
|
|
118
|
+
* if (result.success) {
|
|
119
|
+
* const url = result.data.downloadUrl;
|
|
120
|
+
* console.log('Arquivo disponível em:', url);
|
|
121
|
+
* } else {
|
|
122
|
+
* console.error('Falha no upload:', result.error);
|
|
123
|
+
* }
|
|
46
124
|
* ```
|
|
47
125
|
*/
|
|
48
|
-
async upload(file,
|
|
126
|
+
async upload(file, options = {}) {
|
|
49
127
|
const projectId = this.client.projectId;
|
|
50
|
-
|
|
51
|
-
//
|
|
128
|
+
// =========================================================================
|
|
129
|
+
// VALIDAÇÃO DE ENTRADA
|
|
130
|
+
// =========================================================================
|
|
131
|
+
// [SEGURANÇA] Validação do arquivo
|
|
52
132
|
if (!file) {
|
|
53
|
-
|
|
133
|
+
return err('Arquivo não fornecido');
|
|
134
|
+
}
|
|
135
|
+
// Obtém tamanho do arquivo
|
|
136
|
+
const fileSize = this.getFileSize(file);
|
|
137
|
+
if (fileSize === 0) {
|
|
138
|
+
return err('Arquivo está vazio (0 bytes)');
|
|
54
139
|
}
|
|
140
|
+
// Gera nome do arquivo se não fornecido
|
|
141
|
+
const fileName = options.fileName || this.generateFileName(file);
|
|
55
142
|
if (!fileName || typeof fileName !== 'string') {
|
|
56
|
-
|
|
143
|
+
return err('Nome do arquivo inválido');
|
|
57
144
|
}
|
|
145
|
+
// [SEGURANÇA] Sanitiza nome do arquivo
|
|
146
|
+
const sanitizedFileName = this.sanitizeFileName(fileName);
|
|
147
|
+
// Infere MIME type se não fornecido
|
|
148
|
+
const contentType = options.contentType || this.inferContentType(file, sanitizedFileName);
|
|
58
149
|
try {
|
|
59
|
-
//
|
|
150
|
+
// =====================================================================
|
|
60
151
|
// ETAPA 1: Obter URL presigned do backend
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
fileName,
|
|
64
|
-
contentType
|
|
65
|
-
folder,
|
|
66
|
-
size:
|
|
152
|
+
// =====================================================================
|
|
153
|
+
const presignedResult = await this.getPresignedUrl({
|
|
154
|
+
fileName: sanitizedFileName,
|
|
155
|
+
contentType,
|
|
156
|
+
folder: options.folder,
|
|
157
|
+
size: fileSize,
|
|
67
158
|
});
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!presignedUrl) {
|
|
71
|
-
throw new Error('Servidor não retornou URL de upload válida');
|
|
72
|
-
}
|
|
73
|
-
if (!fileId) {
|
|
74
|
-
throw new Error('Servidor não retornou ID do arquivo');
|
|
159
|
+
if (!presignedResult.success) {
|
|
160
|
+
return presignedResult;
|
|
75
161
|
}
|
|
76
|
-
|
|
162
|
+
const { uploadUrl, fileId } = presignedResult.data;
|
|
163
|
+
// =====================================================================
|
|
77
164
|
// ETAPA 2: Preparar blob para upload
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
else if (file instanceof Blob) {
|
|
87
|
-
// Browser File/Blob
|
|
88
|
-
blob = file;
|
|
89
|
-
fileSize = file.size;
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
throw new Error('Tipo de arquivo não suportado (apenas Blob, Buffer ou File)');
|
|
93
|
-
}
|
|
94
|
-
// [SEGURANÇA] Validação de tamanho zero (arquivo vazio/corrompido)
|
|
95
|
-
if (fileSize === 0) {
|
|
96
|
-
throw new Error('Arquivo está vazio (0 bytes)');
|
|
165
|
+
// =====================================================================
|
|
166
|
+
const blob = this.prepareBlob(file, contentType);
|
|
167
|
+
// =====================================================================
|
|
168
|
+
// ETAPA 3: Upload para S3
|
|
169
|
+
// =====================================================================
|
|
170
|
+
const s3Result = await this.uploadToS3(uploadUrl, blob, contentType, options.onProgress);
|
|
171
|
+
if (!s3Result.success) {
|
|
172
|
+
return s3Result;
|
|
97
173
|
}
|
|
98
|
-
//
|
|
99
|
-
// ETAPA
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
'Content-Type': mimeType,
|
|
105
|
-
},
|
|
106
|
-
validateStatus: (status) => status >= 200 && status < 300,
|
|
107
|
-
onUploadProgress: (event) => {
|
|
108
|
-
if (onProgress && event.total) {
|
|
109
|
-
onProgress({
|
|
110
|
-
loaded: event.loaded,
|
|
111
|
-
total: event.total,
|
|
112
|
-
percent: Math.round((event.loaded / event.total) * 100),
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
timeout: 300000,
|
|
117
|
-
transformRequest: [(data) => data],
|
|
118
|
-
});
|
|
174
|
+
// =====================================================================
|
|
175
|
+
// ETAPA 4: Confirmar upload no backend
|
|
176
|
+
// =====================================================================
|
|
177
|
+
const confirmResult = await this.confirmUpload(fileId);
|
|
178
|
+
if (!confirmResult.success) {
|
|
179
|
+
return confirmResult;
|
|
119
180
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
throw new Error('Falha de rede ao enviar arquivo. Verifique sua conexão.');
|
|
131
|
-
}
|
|
132
|
-
if (axiosError.response?.status === 403) {
|
|
133
|
-
throw new Error('Permissão negada (403): O upload foi rejeitado pelo storage.');
|
|
134
|
-
}
|
|
135
|
-
if (axiosError.response?.status === 400) {
|
|
136
|
-
throw new Error('Arquivo rejeitado (400): Verifique o tipo e tamanho.');
|
|
181
|
+
// =====================================================================
|
|
182
|
+
// ETAPA 5: Validar e retornar resultado
|
|
183
|
+
// =====================================================================
|
|
184
|
+
const fileObject = confirmResult.data;
|
|
185
|
+
// [ROBUSTEZ] Garante que temos uma URL válida
|
|
186
|
+
if (!fileObject.downloadUrl && !fileObject.publicUrl && !fileObject.url) {
|
|
187
|
+
// Tenta recuperar URL via GET
|
|
188
|
+
const refreshResult = await this.get(fileObject.id);
|
|
189
|
+
if (refreshResult.success && refreshResult.data.downloadUrl) {
|
|
190
|
+
return refreshResult;
|
|
137
191
|
}
|
|
138
|
-
|
|
192
|
+
return err('Upload confirmado mas URL do arquivo não foi obtida');
|
|
139
193
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
194
|
+
return ok(fileObject);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
// [TRATAMENTO DE ERRO] Captura erros não tratados
|
|
198
|
+
const message = this.parseError(error);
|
|
199
|
+
console.error('[Aether Storage] Erro não tratado no upload:', message);
|
|
200
|
+
return err(message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ===========================================================================
|
|
204
|
+
// MÉTODOS AUXILIARES DE UPLOAD
|
|
205
|
+
// ===========================================================================
|
|
206
|
+
/**
|
|
207
|
+
* Obtém URL presigned para upload.
|
|
208
|
+
*/
|
|
209
|
+
async getPresignedUrl(params) {
|
|
210
|
+
const projectId = this.client.projectId;
|
|
211
|
+
try {
|
|
212
|
+
const { data } = await this.http.post(`/v1/projects/${projectId}/storage/upload-url`, params);
|
|
213
|
+
const uploadUrl = data.data.uploadUrl || data.data.url;
|
|
214
|
+
const fileId = data.data.fileId;
|
|
215
|
+
if (!uploadUrl) {
|
|
216
|
+
return err('Servidor não retornou URL de upload válida');
|
|
146
217
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
catch (err) {
|
|
150
|
-
// Propaga o erro para que o frontend possa tratar (ex: mostrar toast vermelho)
|
|
151
|
-
const error = err;
|
|
152
|
-
console.error('[Aether SDK] Erro no upload:', error.message);
|
|
153
|
-
if (error.response?.status === 429) {
|
|
154
|
-
throw new Error('Limite de uploads atingido. Tente novamente em alguns minutos.');
|
|
218
|
+
if (!fileId) {
|
|
219
|
+
return err('Servidor não retornou ID do arquivo');
|
|
155
220
|
}
|
|
156
|
-
|
|
157
|
-
|
|
221
|
+
return ok({ uploadUrl, fileId });
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
return err(this.parseHttpError(error, 'Falha ao obter URL de upload'));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Faz upload do blob para S3.
|
|
229
|
+
*/
|
|
230
|
+
async uploadToS3(presignedUrl, blob, contentType, onProgress) {
|
|
231
|
+
try {
|
|
232
|
+
await axios.put(presignedUrl, blob, {
|
|
233
|
+
headers: {
|
|
234
|
+
'Content-Type': contentType,
|
|
235
|
+
},
|
|
236
|
+
validateStatus: (status) => status >= 200 && status < 300,
|
|
237
|
+
onUploadProgress: (event) => {
|
|
238
|
+
if (onProgress && event.total) {
|
|
239
|
+
onProgress({
|
|
240
|
+
loaded: event.loaded,
|
|
241
|
+
total: event.total,
|
|
242
|
+
percent: Math.round((event.loaded / event.total) * 100),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
timeout: UPLOAD_TIMEOUT,
|
|
247
|
+
// [IMPORTANTE] Não transformar o blob
|
|
248
|
+
transformRequest: [(data) => data],
|
|
249
|
+
});
|
|
250
|
+
return ok(undefined);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
return err(this.parseS3Error(error));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Confirma upload no backend.
|
|
258
|
+
*/
|
|
259
|
+
async confirmUpload(fileId) {
|
|
260
|
+
const projectId = this.client.projectId;
|
|
261
|
+
try {
|
|
262
|
+
const { data } = await this.http.post(`/v1/projects/${projectId}/storage/confirm`, { fileId });
|
|
263
|
+
if (!data?.data?.id) {
|
|
264
|
+
return err('Confirmação de upload falhou: ID não retornado');
|
|
158
265
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
error.message ||
|
|
164
|
-
'Erro desconhecido no upload';
|
|
165
|
-
throw new Error(msg);
|
|
266
|
+
return ok(data.data);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
return err(this.parseHttpError(error, 'Falha ao confirmar upload'));
|
|
166
270
|
}
|
|
167
271
|
}
|
|
272
|
+
// ===========================================================================
|
|
273
|
+
// LIST
|
|
274
|
+
// ===========================================================================
|
|
168
275
|
/**
|
|
169
276
|
* Lista arquivos de uma pasta.
|
|
277
|
+
*
|
|
278
|
+
* @param folder - Pasta para listar (opcional)
|
|
279
|
+
* @param options - Opções de paginação
|
|
280
|
+
* @returns Result com lista de FileObject
|
|
170
281
|
*/
|
|
171
|
-
async list(folder, options) {
|
|
282
|
+
async list(folder, options = {}) {
|
|
172
283
|
const projectId = this.client.projectId;
|
|
173
284
|
const params = {};
|
|
174
285
|
if (folder)
|
|
175
286
|
params.folder = folder;
|
|
176
|
-
if (options
|
|
287
|
+
if (options.limit)
|
|
177
288
|
params.limit = options.limit;
|
|
178
|
-
if (options
|
|
289
|
+
if (options.offset)
|
|
179
290
|
params.offset = options.offset;
|
|
180
|
-
|
|
181
|
-
|
|
291
|
+
if (options.order)
|
|
292
|
+
params.order = options.order;
|
|
293
|
+
if (options.orderBy)
|
|
294
|
+
params.orderBy = options.orderBy;
|
|
295
|
+
try {
|
|
296
|
+
const { data } = await this.http.get(`/v1/projects/${projectId}/storage`, { params });
|
|
297
|
+
return ok(data.data || []);
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
return err(this.parseHttpError(error, 'Falha ao listar arquivos'));
|
|
301
|
+
}
|
|
182
302
|
}
|
|
303
|
+
// ===========================================================================
|
|
304
|
+
// GET
|
|
305
|
+
// ===========================================================================
|
|
183
306
|
/**
|
|
184
307
|
* Busca um arquivo por ID.
|
|
308
|
+
*
|
|
309
|
+
* @param fileId - ID do arquivo
|
|
310
|
+
* @returns Result com FileObject ou erro
|
|
185
311
|
*/
|
|
186
312
|
async get(fileId) {
|
|
313
|
+
if (!fileId) {
|
|
314
|
+
return err('ID do arquivo não fornecido');
|
|
315
|
+
}
|
|
187
316
|
const projectId = this.client.projectId;
|
|
188
|
-
|
|
189
|
-
|
|
317
|
+
try {
|
|
318
|
+
const { data } = await this.http.get(`/v1/projects/${projectId}/storage/${fileId}`);
|
|
319
|
+
if (!data?.data) {
|
|
320
|
+
return err('Arquivo não encontrado');
|
|
321
|
+
}
|
|
322
|
+
return ok(data.data);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
return err(this.parseHttpError(error, 'Falha ao buscar arquivo'));
|
|
326
|
+
}
|
|
190
327
|
}
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
// DELETE
|
|
330
|
+
// ===========================================================================
|
|
191
331
|
/**
|
|
192
332
|
* Deleta um arquivo.
|
|
333
|
+
*
|
|
334
|
+
* @param fileId - ID do arquivo a deletar
|
|
335
|
+
* @returns Result com boolean indicando sucesso
|
|
193
336
|
*/
|
|
194
337
|
async delete(fileId) {
|
|
338
|
+
if (!fileId) {
|
|
339
|
+
return err('ID do arquivo não fornecido');
|
|
340
|
+
}
|
|
195
341
|
const projectId = this.client.projectId;
|
|
196
|
-
|
|
197
|
-
|
|
342
|
+
try {
|
|
343
|
+
await this.http.delete(`/v1/projects/${projectId}/storage/${fileId}`);
|
|
344
|
+
return ok(true);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
return err(this.parseHttpError(error, 'Falha ao deletar arquivo'));
|
|
348
|
+
}
|
|
198
349
|
}
|
|
350
|
+
// ===========================================================================
|
|
351
|
+
// URL HELPERS
|
|
352
|
+
// ===========================================================================
|
|
199
353
|
/**
|
|
200
|
-
* Gera URL assinada temporária para
|
|
354
|
+
* Gera URL assinada temporária para um arquivo.
|
|
355
|
+
*
|
|
356
|
+
* @param fileId - ID do arquivo
|
|
357
|
+
* @param expiresIn - Tempo de expiração em segundos (padrão: 3600)
|
|
358
|
+
* @returns Result com URL assinada
|
|
201
359
|
*/
|
|
202
360
|
async getSignedUrl(fileId, expiresIn = 3600) {
|
|
361
|
+
if (!fileId) {
|
|
362
|
+
return err('ID do arquivo não fornecido');
|
|
363
|
+
}
|
|
203
364
|
const projectId = this.client.projectId;
|
|
204
|
-
|
|
205
|
-
|
|
365
|
+
try {
|
|
366
|
+
const { data } = await this.http.get(`/v1/projects/${projectId}/storage/${fileId}/signed-url`, { params: { expiresIn } });
|
|
367
|
+
if (!data?.data?.signedUrl) {
|
|
368
|
+
return err('Servidor não retornou URL assinada');
|
|
369
|
+
}
|
|
370
|
+
return ok(data.data.signedUrl);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
return err(this.parseHttpError(error, 'Falha ao gerar URL assinada'));
|
|
374
|
+
}
|
|
206
375
|
}
|
|
207
376
|
/**
|
|
208
|
-
* Retorna URL pública
|
|
377
|
+
* Retorna URL pública de um arquivo.
|
|
378
|
+
*
|
|
379
|
+
* @param fileId - ID do arquivo
|
|
380
|
+
* @returns URL pública (síncrono)
|
|
209
381
|
*/
|
|
210
382
|
getPublicUrl(fileId) {
|
|
211
383
|
const projectId = this.client.projectId;
|
|
212
|
-
|
|
384
|
+
const baseUrl = this.http.defaults.baseURL || '';
|
|
385
|
+
return `${baseUrl}/v1/projects/${projectId}/storage/${fileId}/public-download`;
|
|
213
386
|
}
|
|
387
|
+
// ===========================================================================
|
|
388
|
+
// UTILITÁRIOS PRIVADOS
|
|
389
|
+
// ===========================================================================
|
|
214
390
|
/**
|
|
215
|
-
*
|
|
391
|
+
* Obtém tamanho do arquivo em bytes.
|
|
216
392
|
*/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
393
|
+
getFileSize(file) {
|
|
394
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
|
|
395
|
+
return file.length;
|
|
396
|
+
}
|
|
397
|
+
return file.size;
|
|
221
398
|
}
|
|
222
399
|
/**
|
|
223
|
-
*
|
|
400
|
+
* Gera nome único para o arquivo.
|
|
224
401
|
*/
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
402
|
+
generateFileName(file) {
|
|
403
|
+
const timestamp = Date.now();
|
|
404
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
405
|
+
// Tenta obter extensão do arquivo original
|
|
406
|
+
let ext = 'bin';
|
|
407
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
|
|
408
|
+
// Buffer: tenta inferir do magic bytes se possível, mas aqui vamos de bin default
|
|
409
|
+
// Poderíamos passar contentType como dica se tivéssemos acesso aqui
|
|
410
|
+
ext = 'bin';
|
|
411
|
+
}
|
|
412
|
+
else if (file instanceof File && file.name) {
|
|
413
|
+
const parts = file.name.split('.');
|
|
414
|
+
if (parts.length > 1) {
|
|
415
|
+
ext = parts.pop().toLowerCase();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else if (file instanceof Blob && file.type) {
|
|
419
|
+
// Infere extensão do MIME type
|
|
420
|
+
const mimeExt = Object.entries(MIME_TYPES).find(([_, mime]) => mime === file.type);
|
|
421
|
+
if (mimeExt) {
|
|
422
|
+
ext = mimeExt[0];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return `file_${timestamp}_${random}.${ext}`;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Sanitiza nome do arquivo removendo caracteres perigosos.
|
|
429
|
+
*/
|
|
430
|
+
sanitizeFileName(fileName) {
|
|
431
|
+
return fileName
|
|
432
|
+
// Remove path traversal
|
|
433
|
+
.replace(/\.\./g, '')
|
|
434
|
+
// Remove caracteres especiais perigosos
|
|
435
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_')
|
|
436
|
+
// Limita tamanho
|
|
437
|
+
.substring(0, 255)
|
|
438
|
+
// Remove espaços do início e fim
|
|
439
|
+
.trim();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Infere MIME type do arquivo.
|
|
443
|
+
*/
|
|
444
|
+
inferContentType(file, fileName) {
|
|
445
|
+
// 1. Tenta do arquivo (se for File/Blob)
|
|
446
|
+
if (typeof Buffer === 'undefined' || !Buffer.isBuffer(file)) {
|
|
447
|
+
if (file instanceof File && file.type) {
|
|
448
|
+
return file.type;
|
|
449
|
+
}
|
|
450
|
+
if (file instanceof Blob && file.type) {
|
|
451
|
+
return file.type;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// 2. Infere da extensão
|
|
455
|
+
const ext = fileName.split('.').pop()?.toLowerCase();
|
|
456
|
+
if (ext && MIME_TYPES[ext]) {
|
|
457
|
+
return MIME_TYPES[ext];
|
|
458
|
+
}
|
|
459
|
+
// 3. Fallback
|
|
460
|
+
return 'application/octet-stream';
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Prepara blob para upload.
|
|
464
|
+
*/
|
|
465
|
+
prepareBlob(file, contentType) {
|
|
466
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
|
|
467
|
+
return new Blob([new Uint8Array(file)], { type: contentType });
|
|
468
|
+
}
|
|
469
|
+
return file;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Parseia erro genérico.
|
|
473
|
+
*/
|
|
474
|
+
parseError(error) {
|
|
475
|
+
if (error instanceof Error) {
|
|
476
|
+
return error.message;
|
|
477
|
+
}
|
|
478
|
+
if (typeof error === 'string') {
|
|
479
|
+
return error;
|
|
480
|
+
}
|
|
481
|
+
return 'Erro desconhecido';
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Parseia erro de requisição HTTP.
|
|
485
|
+
*/
|
|
486
|
+
parseHttpError(error, fallback) {
|
|
487
|
+
const axiosError = error;
|
|
488
|
+
// Erro de rede
|
|
489
|
+
if (axiosError.code === 'ERR_NETWORK') {
|
|
490
|
+
return 'Falha de conexão. Verifique sua internet.';
|
|
491
|
+
}
|
|
492
|
+
// Erros HTTP específicos
|
|
493
|
+
if (axiosError.response?.status === 401) {
|
|
494
|
+
return 'Sessão expirada. Faça login novamente.';
|
|
495
|
+
}
|
|
496
|
+
if (axiosError.response?.status === 403) {
|
|
497
|
+
return 'Permissão negada para esta operação.';
|
|
498
|
+
}
|
|
499
|
+
if (axiosError.response?.status === 404) {
|
|
500
|
+
return 'Recurso não encontrado.';
|
|
501
|
+
}
|
|
502
|
+
if (axiosError.response?.status === 413) {
|
|
503
|
+
return 'Arquivo muito grande.';
|
|
504
|
+
}
|
|
505
|
+
if (axiosError.response?.status === 429) {
|
|
506
|
+
return 'Muitas requisições. Tente novamente em alguns minutos.';
|
|
507
|
+
}
|
|
508
|
+
// Mensagem do servidor
|
|
509
|
+
if (axiosError.response?.data?.message) {
|
|
510
|
+
return axiosError.response.data.message;
|
|
511
|
+
}
|
|
512
|
+
if (axiosError.response?.data?.error) {
|
|
513
|
+
return axiosError.response.data.error;
|
|
514
|
+
}
|
|
515
|
+
// Mensagem do axios
|
|
516
|
+
if (axiosError.message) {
|
|
517
|
+
return axiosError.message;
|
|
518
|
+
}
|
|
519
|
+
return fallback;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Parseia erro específico do S3.
|
|
523
|
+
*/
|
|
524
|
+
parseS3Error(error) {
|
|
525
|
+
const axiosError = error;
|
|
526
|
+
if (axiosError.code === 'ERR_NETWORK') {
|
|
527
|
+
return 'Falha de rede ao enviar arquivo. Verifique sua conexão.';
|
|
528
|
+
}
|
|
529
|
+
if (axiosError.response?.status === 403) {
|
|
530
|
+
return 'Upload rejeitado pelo storage (403). URL pode ter expirado.';
|
|
531
|
+
}
|
|
532
|
+
if (axiosError.response?.status === 400) {
|
|
533
|
+
return 'Arquivo rejeitado (400). Verifique tipo e tamanho.';
|
|
534
|
+
}
|
|
535
|
+
return axiosError.message || 'Falha ao enviar arquivo para storage';
|
|
229
536
|
}
|
|
230
537
|
}
|
|
231
538
|
//# sourceMappingURL=storage.js.map
|