import { v4 as uuidv4 } from 'uuid'; import Axios from 'axios'; import { baseURLApi } from '../../config'; function extractExtensionFrom(filename) { if (!filename) { return null; } const regex = /(?:\.([^.]+))?$/; return regex.exec(filename)[1]; } export default class FileUploader { static validate(file, schema) { if (!schema) { return; } if (schema.image) { if (!file.type.startsWith('image')) { throw new Error('You must upload an image'); } } if (schema.size && file.size > schema.size) { throw new Error('File is too big.'); } const extension = extractExtensionFrom(file.name); if (schema.formats && !schema.formats.includes(extension)) { throw new Error('Invalid format'); } } static async upload(path, file, schema) { try { this.validate(file, schema); } catch (error) { return Promise.reject(error); } const extension = extractExtensionFrom(file.name); const id = uuidv4(); const filename = `${id}.${extension}`; const privateUrl = `${path}/${filename}`; const publicUrl = await this.uploadToServer(file, path, filename); return { id: id, name: file.name, sizeInBytes: file.size, privateUrl, publicUrl, new: true, }; } static async uploadChunked(path, file, schema, options = {}) { try { this.validate(file, schema); } catch (error) { return Promise.reject(error); } const chunkSize = Number(options.chunkSize) > 0 ? Number(options.chunkSize) : 5 * 1024 * 1024; const maxRetries = Number(options.maxRetries) > 0 ? Number(options.maxRetries) : 3; const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null; const onStatus = typeof options.onStatus === 'function' ? options.onStatus : null; const signal = options.signal || null; const extension = extractExtensionFrom(file.name); const id = uuidv4(); const filename = extension ? `${id}.${extension}` : id; const privateUrl = `${path}/${filename}`; const totalChunks = Math.max(1, Math.ceil(file.size / chunkSize)); if (signal?.aborted) { throw new Error('Upload aborted'); } const initResponse = await Axios.post( '/file/upload-sessions/init', { folder: path, filename, size: file.size, contentType: file.type || '', totalChunks, }, { signal }, ); const sessionId = initResponse?.data?.sessionId; if (!sessionId) { throw new Error('Upload session was not created'); } for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex += 1) { if (signal?.aborted) { throw new Error('Upload aborted'); } const start = chunkIndex * chunkSize; const end = Math.min(file.size, start + chunkSize); const chunk = file.slice(start, end); if (onStatus) { onStatus('uploading', { chunkIndex, totalChunks, }); } let retry = 0; while (retry <= maxRetries) { try { await Axios.put( `/file/upload-sessions/${sessionId}/chunks/${chunkIndex}`, chunk, { headers: { 'Content-Type': 'application/octet-stream', }, signal, }, ); break; } catch (error) { if (signal?.aborted) { throw new Error('Upload aborted'); } retry += 1; if (retry > maxRetries) { throw error; } await new Promise((resolve) => setTimeout(resolve, 500 * retry)); } } if (onProgress) { const percent = Math.round(((chunkIndex + 1) / totalChunks) * 100); onProgress(percent, { chunkIndex: chunkIndex + 1, totalChunks, }); } } if (signal?.aborted) { throw new Error('Upload aborted'); } if (onStatus) { onStatus('finalizing', null); } const finalizeResponse = await Axios.post( `/file/upload-sessions/${sessionId}/finalize`, null, { signal }, ); const responsePublicUrl = finalizeResponse?.data?.url; const publicUrl = responsePublicUrl ? responsePublicUrl.startsWith('http') ? responsePublicUrl : `${baseURLApi}${responsePublicUrl}` : `${baseURLApi}/file/download?privateUrl=${encodeURIComponent(privateUrl)}`; return { id, name: file.name, sizeInBytes: file.size, privateUrl, publicUrl, new: true, }; } static async uploadToServer(file, path, filename) { const formData = new FormData(); formData.append('file', file); formData.append('filename', filename); const uri = `/file/upload/${path}`; await Axios.post(uri, formData, { headers: { 'Content-Type': 'multipart/form-data', }, }); const privateUrl = `${path}/${filename}`; // Debug logging removed for production builds return `${baseURLApi}/file/download?privateUrl=${privateUrl}`; } }