import { Injectable } from '@angular/core'; import { ok, err, Result } from 'neverthrow'; import { ObjectMergeNotification } from 'src/app/services/socket-connection-mcr.service'; import { CMAPIService } from "src/app/shared/repository/CMAPI/cmapi.service" import { DomSanitizer } from '@angular/platform-browser'; import { v4 as uuidv4 } from 'uuid' export enum UploadError { noConnection = 'noConnection', slow = 'slow' } export type IOUploadError = "noConnection" | "slow" @Injectable({ providedIn: 'root' }) export class UploadStreamingService { constructor( private CMAPIService: CMAPIService, private sanitizer: DomSanitizer ) { this.CMAPIService window["sanitizer"] = this.sanitizer } } export class UploadFileUseCase { CMAPIService: CMAPIService = window["CMAPIAPIRepository"] constructor() {} async execute(PublicationAttachmentEntity: PublicationAttachmentEntity): Promise> { return new Promise(async (resolve, reject) => { let path: string; const length = PublicationAttachmentEntity.chucksManager.chunks.totalChunks.toString() const readAndUploadChunk = async(index: number) => { const base64 = await PublicationAttachmentEntity.chucksManager.chunks.getChunks(index) const uploadRequest = this.CMAPIService.FileContent({length, path: PublicationAttachmentEntity.chucksManager.path, index, base64}) uploadRequest.then((uploadRequest) => { if(uploadRequest.isOk()) { PublicationAttachmentEntity.chucksManager.setResponse(index, uploadRequest) } }) return uploadRequest; } if(!PublicationAttachmentEntity.chucksManager.hasPath()) { const guidRequest = await this.CMAPIService.RequestUpload() if(guidRequest.isOk()) { path = guidRequest.value+".mp4" PublicationAttachmentEntity.chucksManager.setPath(path) } else { const pingRequest = await this.CMAPIService.ping() if( pingRequest.isErr()) { return resolve(err(UploadError.noConnection)) } else { return resolve(err(UploadError.slow)) } } } const allRequest: Promise[] = [] let connection = true let errorMessage: UploadError.noConnection | UploadError.slow for (let index = 1; ( (index <= PublicationAttachmentEntity.chucksManager.chunks.totalChunks) && connection ); index++) { const needUpload = PublicationAttachmentEntity.chucksManager.needToUploadChunkIndex(index) if(needUpload) { // // upload every chunk at onces // const request = readAndUploadChunk(index).then(async(uploadRequest) => { // if(uploadRequest.isErr()) { // if(connection) { // connection = false // const pingRequest = await this.CMAPIService.ping() // if( pingRequest.isErr()) { // errorMessage = UploadError.noConnection // } else { // errorMessage = UploadError.slow // } // } // } // }) // allRequest.push(request) // one by one chunk upload const request = readAndUploadChunk(index) allRequest.push(request) const uploadRequest = await request if(uploadRequest.isErr()) { const pingRequest = await this.CMAPIService.ping() if( pingRequest.isErr()) { return resolve(err(UploadError.noConnection)) } else { return resolve(err(UploadError.slow)) } } } } await Promise.all(allRequest) if(!connection) { return resolve(err(errorMessage)) } else { return resolve(ok(true)) } }) } } export class PublicationAttachmentEntity { url: string FileExtension: string FileType: 'image' | 'video' OriginalFileName: string blobFile?: File toUpload = false; chucksManager : ChucksManager Base64: string constructor({base64, extension, blobFile, OriginalFileName, FileType}:PublicationAttachmentEntityParams) { this.Base64 = base64; this.FileExtension = extension; this.blobFile = blobFile this.OriginalFileName = OriginalFileName this.FileType = FileType this.fixFileBase64(); } fixFileBase64() { const sanitizer : DomSanitizer = window["sanitizer"] if(this.FileType == 'image' ) { if(!this.Base64.includes('data:')) { this.url = 'data:image/jpg;base64,' + this.Base64 // this.url = sanitizer.bypassSecurityTrustUrl('data:image/jpg;base64,' + this.Base64) as any } else { this.url = this.Base64 } } else if (this.FileType == 'video' ) { if(!this.Base64.includes('data:') && !this.Base64.startsWith('http')) { this.url = 'data:video/mp4;base64,' + this.Base64 // this.url = sanitizer.bypassSecurityTrustUrl('data:video/mp4;base64,' + this.Base64) as any } else { this.url = this.Base64 } } } needUpload() { this.toUpload = true } setChunkManger (chunks: Chunks | ChunksBase64) { this.chucksManager = new ChucksManager({chunks}) } get hasChunkManger() { return this.chucksManager?.chunks } get hasChunkManager() { return this.chucksManager != null } get hasBlob() { return this.blobFile } } interface IPublicationFormModelEntity { DateIndex: any DocumentId: any ProcessId: any Title: any Message: any DatePublication: any Files?: PublicationAttachmentEntity[] } interface PublicationAttachmentEntityParams { base64: string, blobFile?: File extension: string, OriginalFileName: string, FileType: 'image' | 'video' } export class PublicationFormModel implements IPublicationFormModelEntity { constructor() {} DateIndex: any = new Date() DocumentId: any = null ProcessId: any = null Title: any; Message: any; DatePublication = new Date() OriginalFileName: string; Files: PublicationAttachmentEntity[] = [] hasSet = false send = false setData(data: IPublicationFormModelEntity) { if(data.Files) { data.Files = [] } if(!this.hasSet) { Object.assign(this, data) } this.hasSet = true } } export class Chunks { chunkSize: number private file: File constructor({chunkSize}) { this.chunkSize = chunkSize * 1024 } get totalChunks () { return Math.ceil(this.file.size / this.chunkSize); } setFile(file: File) { this.file = file } // Function to read a chunk of the file readChunk(start: number, end: number): any { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (event: any) => resolve(event.target.result.split(',')[1]); reader.onerror = (error) => reject(error); reader.readAsDataURL(this.file.slice(start, end)); }); } async getChunks(i: number): Promise { i-- if(i < this.totalChunks) { const start = i * this.chunkSize; const end = Math.min(start + this.chunkSize, this.file.size); const chunk = await this.readChunk(start, end); return chunk } } } export class ChunksBase64 { chunkSize: number private base64: string bytes: Uint8Array constructor({chunkSize}) { this.chunkSize = chunkSize * 1024 } get totalChunks () { return Math.ceil(this.bytes.length / this.chunkSize); } setFile(base64: string) { this.base64 = base64 let utf8Encoder = new TextEncoder(); this.bytes = utf8Encoder.encode(base64); } // Function to read a chunk of the file async readChunk(start: number, end: number) { // Slice the last 1MB of bytes let slicedBytes = this.bytes.slice(start, end); // Convert the sliced bytes back to a string let text = new TextDecoder().decode(slicedBytes); return text } async getChunks(i: number): Promise { i-- if(i < this.totalChunks) { const start = i * this.chunkSize; const end = Math.min(start + this.chunkSize, this.bytes.length); const chunk = await this.readChunk(start, end); return chunk } } } interface IUploadResponse { result: Result attemp: number } export class ChucksManager { chunks: Chunks uploads: {[key: string]: IUploadResponse } = {} path: string = undefined uploadPercentage: string = "1%" merging = false onSetPath: Function[] = [] onSetLastChunk: Function[] = [] contentReady = false manualRetry = false isUploading = false needToCommit = true subscribeToUseCaseResponse: Function[] = [] updateTotalPercentageTrigger = () => {} getUploadPercentage() { return this.uploadPercentage } get uploadsCount() { return Object.entries(this.uploads).length } get uploadWithSuccessCount() { const uploadWithSuccess = Object.entries(this.uploads).filter(([index, data])=> data.result.isOk()) return uploadWithSuccess.length } get doneUpload() { return this.chunks.totalChunks == this.uploadWithSuccessCount } uploadFunc: Function constructor({chunks}) { this.chunks = chunks } calculatePercentage(): number { /** * Calculate the percentage based on the total and current values. * * @param total - The total value. * @param current - The current value. * @returns The percentage calculated as (current / total) * 100. */ const total = this.chunks.totalChunks const current = this.uploadWithSuccessCount if (total === 0) { return 0; // To avoid division by zero error } const percentage: number = (current / total) * 100; return percentage; } setManualRetry() { this.manualRetry = true } clearManualRetry() { this.manualRetry = false } setUploading() { this.isUploading = true } clearUploading() { this.isUploading = false } setPercentage() { const percentage: number = this.calculatePercentage() console.log({percentage}) this.updateTotalPercentageTrigger() this.uploadPercentage = percentage.toString()+"%" } setPath(path: string) { this.path = path this.onSetPath.forEach(callback => callback()); } registerOnSetPath(a: Function) { this.onSetPath.push(a) } registerOnLastChunk(a: Function) { this.onSetLastChunk.push(a) } registerToUseCaseResponse(a: Function) { this.subscribeToUseCaseResponse.push(a) } hasPath() { return this.path != undefined } isIndexRegistered(index) { if(!this.uploads[index]) { return false } return true } needToUploadChunkIndex(index) { return !this.uploads?.[index]?.result?.isOk() } setResponse(index, UploadResponse) { if(!this.isIndexRegistered(index)) { this.uploads[index] = { attemp: 1, result: UploadResponse } console.log({UploadResponse}) } else { this.uploads[index].attemp++; this.uploads[index].result = UploadResponse } this.setPercentage() } doneChunkUpload() { this.merging = true this.onSetLastChunk.forEach(callback => callback()); } doneCommit() { this.needToCommit = false } contentSetReady() { this.merging = false this.contentReady = true } }