@aether-baas/core 3.6.3 → 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.
@@ -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 para upload/download de arquivos.
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 do Aether.
64
+ * Módulo para operações de storage.
12
65
  *
13
66
  * @example
14
67
  * ```typescript
15
- * // Upload
16
- * const { data } = await aether.storage.upload(file, 'avatar.png', 'image/png');
17
- * console.log(data.downloadUrl);
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
- * // Listar
20
- * const files = await aether.storage.list('folder');
74
+ * if (result.success) {
75
+ * console.log('URL:', result.data.downloadUrl);
76
+ * } else {
77
+ * console.error('Erro:', result.error);
78
+ * }
21
79
  *
22
- * // Deletar
23
- * await aether.storage.delete('file-id');
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,223 +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 fileName Nome do arquivo (com extensão)
38
- * @param contentType MIME type do arquivo (ex: 'image/png')
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
- * const { data, error } = await aether.storage.upload(file, 'foto.png', 'image/png');
45
- * if (error) console.error('Falha no upload:', error);
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, fileName, contentType, folder, onProgress) {
126
+ async upload(file, options = {}) {
49
127
  const projectId = this.client.projectId;
50
- const mimeType = contentType || 'application/octet-stream';
51
- // [SEGURANÇA] Validação básica do arquivo
128
+ // =========================================================================
129
+ // VALIDAÇÃO DE ENTRADA
130
+ // =========================================================================
131
+ // [SEGURANÇA] Validação do arquivo
52
132
  if (!file) {
53
- throw new Error('Arquivo não fornecido');
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
- throw new Error('Nome do arquivo inválido');
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 { data: uploadData } = await this.http.post(`/v1/projects/${projectId}/storage/upload-url`, {
63
- fileName,
64
- contentType: mimeType,
65
- folder,
66
- size: file instanceof Blob ? file.size : file.length,
152
+ // =====================================================================
153
+ const presignedResult = await this.getPresignedUrl({
154
+ fileName: sanitizedFileName,
155
+ contentType,
156
+ folder: options.folder,
157
+ size: fileSize,
67
158
  });
68
- const presignedUrl = uploadData.data.uploadUrl || uploadData.data.url;
69
- const fileId = uploadData.data.fileId;
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
- let blob;
80
- let fileSize;
81
- if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
82
- // Node.js Buffer
83
- blob = new Blob([new Uint8Array(file)], { type: mimeType });
84
- fileSize = file.length;
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)');
97
- }
98
- // ================================================================
99
- // ETAPA 3: Upload para S3 com validação rigorosa
100
- // ================================================================
101
- try {
102
- const s3Response = await axios.put(presignedUrl, blob, {
103
- headers: {
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
- });
119
- }
120
- catch (s3Error) {
121
- // [SEGURANÇA] Captura específica de erros do S3
122
- const axiosError = s3Error;
123
- console.error('[Aether SDK] Falha no PUT S3:', {
124
- status: axiosError.response?.status,
125
- statusText: axiosError.response?.statusText,
126
- code: axiosError.code,
127
- message: axiosError.message,
128
- });
129
- if (axiosError.code === 'ERR_NETWORK') {
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.');
137
- }
138
- throw new Error(axiosError.message || 'Falha ao enviar arquivo para storage');
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;
139
173
  }
140
- // ================================================================
174
+ // =====================================================================
141
175
  // ETAPA 4: Confirmar upload no backend
142
- // ================================================================
143
- const { data: fileData } = await this.http.post(`/v1/projects/${projectId}/storage/confirm`, { fileId });
144
- if (!fileData?.data?.id) {
145
- console.error('[Aether SDK] Confirmação falhou: ID ausente', fileData);
146
- throw new Error('Confirmação de upload falhou: ID não retornado pelo servidor');
176
+ // =====================================================================
177
+ const confirmResult = await this.confirmUpload(fileId);
178
+ if (!confirmResult.success) {
179
+ return confirmResult;
147
180
  }
148
- // [FIX] Garante que a URL de download existe. Se o servidor não retornou,
149
- // tenta construir ou falhar.
150
- if (!fileData.data.downloadUrl && !fileData.data.url) {
151
- console.warn('[Aether SDK] URL de download ausente na resposta de confirmação. Tentando fallback...');
152
- // Se tivermos o ID, podemos tentar recuperar via get() logo em seguida como robustez
153
- try {
154
- // Pequeno delay para garantir consistência eventual
155
- await new Promise(r => setTimeout(r, 500));
156
- const { data: refreshed } = await this.http.get(`/v1/projects/${projectId}/storage/${fileData.data.id}`);
157
- if (refreshed?.data?.downloadUrl) {
158
- return refreshed.data;
159
- }
160
- }
161
- catch (e) {
162
- console.warn('[Aether SDK] Fallback falhou:', e);
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;
163
191
  }
192
+ return err('Upload confirmado mas URL do arquivo não foi obtida');
164
193
  }
165
- if (!fileData.data.downloadUrl && !fileData.data.url) {
166
- throw new Error('Upload confirmado, mas URL do arquivo não foi obtida. Tente atualizar a lista.');
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');
167
217
  }
168
- return fileData.data;
169
- }
170
- catch (err) {
171
- // Propaga o erro para que o frontend possa tratar (ex: mostrar toast vermelho)
172
- const error = err;
173
- console.error('[Aether SDK] Erro no upload:', error.message || error);
174
- if (error.response?.status === 429) {
175
- 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');
176
220
  }
177
- if (error.response?.status === 413) {
178
- throw new Error('Arquivo muito grande para upload.');
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');
179
265
  }
180
- // Se já for um erro tratado acima, relança.
181
- // Se for axios error genérico do http.post (upload-url), extrai mensagem.
182
- const msg = error.response?.data?.error ||
183
- error.response?.data?.message ||
184
- error.message ||
185
- 'Erro desconhecido no upload';
186
- throw new Error(msg);
266
+ return ok(data.data);
267
+ }
268
+ catch (error) {
269
+ return err(this.parseHttpError(error, 'Falha ao confirmar upload'));
187
270
  }
188
271
  }
272
+ // ===========================================================================
273
+ // LIST
274
+ // ===========================================================================
189
275
  /**
190
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
191
281
  */
192
- async list(folder, options) {
282
+ async list(folder, options = {}) {
193
283
  const projectId = this.client.projectId;
194
284
  const params = {};
195
285
  if (folder)
196
286
  params.folder = folder;
197
- if (options?.limit)
287
+ if (options.limit)
198
288
  params.limit = options.limit;
199
- if (options?.offset)
289
+ if (options.offset)
200
290
  params.offset = options.offset;
201
- const { data } = await this.http.get(`/v1/projects/${projectId}/storage`, { params });
202
- return data.data;
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
+ }
203
302
  }
303
+ // ===========================================================================
304
+ // GET
305
+ // ===========================================================================
204
306
  /**
205
307
  * Busca um arquivo por ID.
308
+ *
309
+ * @param fileId - ID do arquivo
310
+ * @returns Result com FileObject ou erro
206
311
  */
207
312
  async get(fileId) {
313
+ if (!fileId) {
314
+ return err('ID do arquivo não fornecido');
315
+ }
208
316
  const projectId = this.client.projectId;
209
- const { data } = await this.http.get(`/v1/projects/${projectId}/storage/${fileId}`);
210
- return data.data;
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
+ }
211
327
  }
328
+ // ===========================================================================
329
+ // DELETE
330
+ // ===========================================================================
212
331
  /**
213
332
  * Deleta um arquivo.
333
+ *
334
+ * @param fileId - ID do arquivo a deletar
335
+ * @returns Result com boolean indicando sucesso
214
336
  */
215
337
  async delete(fileId) {
338
+ if (!fileId) {
339
+ return err('ID do arquivo não fornecido');
340
+ }
216
341
  const projectId = this.client.projectId;
217
- await this.http.delete(`/v1/projects/${projectId}/storage/${fileId}`);
218
- return true;
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
+ }
219
349
  }
350
+ // ===========================================================================
351
+ // URL HELPERS
352
+ // ===========================================================================
220
353
  /**
221
- * Gera URL assinada temporária para download.
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
222
359
  */
223
360
  async getSignedUrl(fileId, expiresIn = 3600) {
361
+ if (!fileId) {
362
+ return err('ID do arquivo não fornecido');
363
+ }
224
364
  const projectId = this.client.projectId;
225
- const { data } = await this.http.post(`/v1/projects/${projectId}/storage/${fileId}/signed-url`, { expiresIn });
226
- return data.url;
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
+ }
227
375
  }
228
376
  /**
229
- * Retorna URL pública do arquivo (se bucket for público).
377
+ * Retorna URL pública de um arquivo.
378
+ *
379
+ * @param fileId - ID do arquivo
380
+ * @returns URL pública (síncrono)
230
381
  */
231
382
  getPublicUrl(fileId) {
232
383
  const projectId = this.client.projectId;
233
- return `${this.client.apiUrl}/v1/projects/${projectId}/storage/public/${fileId}`;
384
+ const baseUrl = this.http.defaults.baseURL || '';
385
+ return `${baseUrl}/v1/projects/${projectId}/storage/${fileId}/public-download`;
234
386
  }
387
+ // ===========================================================================
388
+ // UTILITÁRIOS PRIVADOS
389
+ // ===========================================================================
235
390
  /**
236
- * Move arquivo para outra pasta.
391
+ * Obtém tamanho do arquivo em bytes.
237
392
  */
238
- async move(fileId, newFolder) {
239
- const projectId = this.client.projectId;
240
- const { data } = await this.http.put(`/v1/projects/${projectId}/storage/${fileId}/move`, { folder: newFolder });
241
- return data.data;
393
+ getFileSize(file) {
394
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(file)) {
395
+ return file.length;
396
+ }
397
+ return file.size;
242
398
  }
243
399
  /**
244
- * Renomeia arquivo.
400
+ * Gera nome único para o arquivo.
245
401
  */
246
- async rename(fileId, newName) {
247
- const projectId = this.client.projectId;
248
- const { data } = await this.http.put(`/v1/projects/${projectId}/storage/${fileId}`, { name: newName });
249
- return data.data;
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';
250
536
  }
251
537
  }
252
538
  //# sourceMappingURL=storage.js.map