import { ChunkUploadReq, FileData, FileUploadRegistration, FinishUploadReq, RegisterUploadReq, RegisterUploadResp } from "../domain/fileUploadService.schema"; import {AxiosInstance, isAxiosError} from "axios"; import {getClient} from "../core/http"; import {InviteToCallReq} from "../domain/callService.schema"; import {GenericErrorBody} from "../domain/http.schema"; import {v4 as uuidv4} from 'uuid'; export class FileUploadService { client: AxiosInstance; cdnClient: AxiosInstance; constructor(token: string, private httpClientOverwrite: AxiosInstance | null, private cdnHttpClientOverwrite: AxiosInstance | null) { this.client = (httpClientOverwrite ?? getClient(false)).create({ headers: { "Authorization": token } }) this.cdnClient = (cdnHttpClientOverwrite ?? getClient(true)).create({ headers: { "Authorization": token } }) } private MIN_CHUNK_SIZE = 25000000 private calculateChunkSize(totalSize: number) { let chunks = Math.floor(totalSize / this.MIN_CHUNK_SIZE); if (chunks === 0) return totalSize; const chunkSize = Math.ceil(totalSize / chunks); return Math.max(chunkSize, this.MIN_CHUNK_SIZE); } /** * Automatically registers the upload, calculates chunksize and uploads each chunk and returns with an uploadId that must be provided when sending the message * @param roomId chatid or channelId * @param userid * @param files */ async uploadFiles(roomId: string, userid: string, files: FileData[]): Promise { let registrations: FileUploadRegistration[] = []; files.forEach(file => { registrations.push({ fileId: uuidv4(), name: file.name, type: file.type, size: file.data.size, }) }) try { const resp = await this.client.post("chat/cdnRegisterUpload", { roomId: roomId, userid: userid, files: registrations, }); for (let filesUploaded = 0; filesUploaded < files.length; filesUploaded++) { await this.uploadFile(resp.data.uploadId, roomId, userid, files[filesUploaded], registrations[filesUploaded]) } await this.finishUpload(roomId, userid, resp.data.uploadId) return resp.data.uploadId } catch (e) { console.log(e) if (isAxiosError(e)) { throw e; } throw new Error("Unexpected error") } } private async finishUpload(roomId: string, userid: string, uploadId: string): Promise { try { await this.cdnClient.post("chat/finishUpload", { roomId: roomId, userid: userid, uploadId: uploadId }); return } catch (e) { console.log(e) if (isAxiosError(e)) { throw e; } throw new Error("Unexpected error") } } private async uploadFile(uploadId: string, roomId: string, userid: string, file: FileData, registration: FileUploadRegistration): Promise { const chunkSize = this.calculateChunkSize(file.data.size); const totalChunks = Math.ceil(file.data.size / chunkSize); const arrayBuffer = await file.data.arrayBuffer(); for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize; const end = Math.min(start + chunkSize, file.data.size); const chunk = new Uint8Array(arrayBuffer.slice(start, end)); const base64 = this.uint8ToBase64(chunk); await this.uploadChunk(uploadId, roomId, userid, registration.fileId, base64); } } private uint8ToBase64(uint8: Uint8Array): string { let binString = ""; for (let i = 0; i < uint8.length; i++) { binString += String.fromCharCode(uint8[i]); } return btoa(binString); } private async uploadChunk(uploadId: string, roomId: string, userid: string, fileId: string, chunk: string): Promise { try { await this.cdnClient.post("chat/uploadChunk", { roomId: roomId, userid: userid, fileId: fileId, chunk: chunk, uploadId: uploadId, }); return } catch (e) { if (isAxiosError(e)) { throw e; } throw new Error("Unexpected error") } } }