Finished implementing FileUploadService + added test workflow
This commit is contained in:
40
src/domain/fileUploadService.schema.ts
Normal file
40
src/domain/fileUploadService.schema.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Request schemas
|
||||
export interface RegisterUploadReq {
|
||||
roomId: string
|
||||
userid: string
|
||||
files: FileUploadRegistration[]
|
||||
}
|
||||
|
||||
export interface ChunkUploadReq {
|
||||
uploadId: string
|
||||
fileId: string
|
||||
chunk: string
|
||||
roomId: string
|
||||
userid: string
|
||||
}
|
||||
|
||||
export interface FinishUploadReq {
|
||||
uploadId: string
|
||||
roomId: string
|
||||
userid: string
|
||||
}
|
||||
|
||||
// Response schemas
|
||||
export interface RegisterUploadResp {
|
||||
uploadId: string
|
||||
}
|
||||
|
||||
// Types
|
||||
export interface FileUploadRegistration {
|
||||
size: number
|
||||
type: string
|
||||
name: string
|
||||
fileId: string
|
||||
}
|
||||
|
||||
export interface FileData {
|
||||
name: string
|
||||
extension: string
|
||||
type: string
|
||||
data: File
|
||||
}
|
||||
19
src/mocks/handlers/fUpl.http.ts
Normal file
19
src/mocks/handlers/fUpl.http.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {http, HttpResponse} from "msw";
|
||||
import {GetRTCAccessResp} from "../../domain/callService.schema";
|
||||
import {RegisterUploadResp} from "../../domain/fileUploadService.schema";
|
||||
|
||||
export const fileUploadHandlers = [
|
||||
http.post('*/chat/cdnRegisterUpload', () => {
|
||||
return HttpResponse.json(<RegisterUploadResp>{
|
||||
uploadId: "MockUploadId"
|
||||
})
|
||||
}),
|
||||
|
||||
http.post('*/chat/uploadChunk', () => {
|
||||
return HttpResponse.json()
|
||||
}),
|
||||
|
||||
http.post('*/chat/finishUpload', () => {
|
||||
return HttpResponse.json()
|
||||
}),
|
||||
]
|
||||
@@ -2,10 +2,12 @@ import {networkHandlers} from "./handlers/auth.http";
|
||||
import {authHandlers} from "./handlers/network.http";
|
||||
import {pictureHandlers} from "./handlers/picture.http";
|
||||
import {callHandlers} from "./handlers/call.http";
|
||||
import {fileUploadHandlers} from "./handlers/fUpl.http";
|
||||
|
||||
export const allHandlers = [
|
||||
...authHandlers,
|
||||
...networkHandlers,
|
||||
...pictureHandlers,
|
||||
...callHandlers
|
||||
...callHandlers,
|
||||
...fileUploadHandlers
|
||||
]
|
||||
10
src/services/fileUploadService.test.ts
Normal file
10
src/services/fileUploadService.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {describe, expect, it} from "vitest";
|
||||
import {FileUploadService} from "./fileUploadService";
|
||||
|
||||
describe("fileUploadService", () => {
|
||||
it('should upload files', async () => {
|
||||
const service = new FileUploadService("", null, null);
|
||||
const uploadId = await service.uploadFiles("", "", [])
|
||||
expect(uploadId).toBe("MockUploadId")
|
||||
});
|
||||
})
|
||||
135
src/services/fileUploadService.ts
Normal file
135
src/services/fileUploadService.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
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<string> {
|
||||
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<RegisterUploadResp>("chat/cdnRegisterUpload", <RegisterUploadReq>{
|
||||
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<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
private async finishUpload(roomId: string, userid: string, uploadId: string): Promise<void> {
|
||||
try {
|
||||
await this.cdnClient.post("chat/finishUpload", <FinishUploadReq>{
|
||||
roomId: roomId,
|
||||
userid: userid,
|
||||
uploadId: uploadId
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadFile(uploadId: string, roomId: string, userid: string, file: FileData, registration: FileUploadRegistration): Promise<void> {
|
||||
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<void> {
|
||||
try {
|
||||
await this.cdnClient.post<RegisterUploadResp>("chat/uploadChunk", <ChunkUploadReq>{
|
||||
roomId: roomId,
|
||||
userid: userid,
|
||||
fileId: fileId,
|
||||
chunk: chunk,
|
||||
uploadId: uploadId,
|
||||
});
|
||||
return
|
||||
} catch (e) {
|
||||
if (isAxiosError<GenericErrorBody>(e)) {
|
||||
throw e;
|
||||
}
|
||||
throw new Error("Unexpected error")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user