@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.
- package/dist/aether.d.ts.map +1 -1
- package/dist/aether.js +14 -6
- package/dist/aether.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 +449 -163
- package/dist/modules/storage.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 +1 -1
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,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
|
|
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)');
|
|
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
|
|
144
|
-
if (!
|
|
145
|
-
|
|
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
|
-
//
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
287
|
+
if (options.limit)
|
|
198
288
|
params.limit = options.limit;
|
|
199
|
-
if (options
|
|
289
|
+
if (options.offset)
|
|
200
290
|
params.offset = options.offset;
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
|
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
|
-
|
|
226
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
391
|
+
* Obtém tamanho do arquivo em bytes.
|
|
237
392
|
*/
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
*
|
|
400
|
+
* Gera nome único para o arquivo.
|
|
245
401
|
*/
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
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
|