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'; 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 ) { window["sanitizer"] = this.sanitizer } } 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()) { // 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.startsWith('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.startsWith('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) { this.chucksManager = new ChucksManager({chunks}) } get hasChunkManger() { return this.chucksManager?.chunks } get hasChunkManager() { return this.chucksManager != null } } 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; DocumentId: any; ProcessId: any; Title: any; Message: any; DatePublication: any; OriginalFileName: string; Files: PublicationAttachmentEntity[]; hasSet = false setData(data: IPublicationFormModelEntity) { if(!this.hasSet) { Object.assign(this, data) } this.hasSet = true } } export class PublicationFormMV { private UploadFileUseCase = new UploadFileUseCase() private form = new PublicationFormModel() ObjectMergeNotification = new ObjectMergeNotification() constructor() { // this.ObjectMergeNotification.connect(); } setDataToFrom(data: IPublicationFormModelEntity) { this.form.setData(data) } private getVideoFiles() { return this.form.Files.filter( x => x.FileType == 'video') } private upload(PublicationAttachmentEntity: PublicationAttachmentEntity) { return new Promise(async (resolve, reject)=> { if(!PublicationAttachmentEntity.hasChunkManger) { const fileBlob = PublicationAttachmentEntity.blobFile; const fileChunks = new Chunks({chunkSize: 40 }) fileChunks.setFile(fileBlob) PublicationAttachmentEntity.setChunkManger(fileChunks) } else if(PublicationAttachmentEntity.chucksManager.doneUpload) { return resolve(true) } let attemp = 0; let result: Result if( PublicationAttachmentEntity.chucksManager.isUploading == false) { do { attemp++ PublicationAttachmentEntity.chucksManager.clearManualRetry() PublicationAttachmentEntity.chucksManager.setUploading() result = await this.UploadFileUseCase.execute(PublicationAttachmentEntity) PublicationAttachmentEntity.chucksManager.clearUploading() } while (attemp<3 && result.isErr() && result.error == 'slow') if(result.isErr()) { PublicationAttachmentEntity.chucksManager.setManualRetry() resolve(false) } else { const guid = PublicationAttachmentEntity.chucksManager.path this.ObjectMergeNotification.subscribe(guid, (data) => { PublicationAttachmentEntity.chucksManager.contentSetReady() resolve(true) }) PublicationAttachmentEntity.chucksManager.doneChunkUpload() this.ObjectMergeNotification.socket.commit(PublicationAttachmentEntity.chucksManager.path) resolve(true) } } else { } }) } uploadVideosFiles(): Promise { return new Promise((resolve, reject) => { // this.ObjectMergeNotification.socket.registerWhenConnected(() => { const videosFiles = this.getVideoFiles() const videosFilesToUploads = videosFiles.filter( e => e.FileType == "video") const Promises: Promise[] = [] for(const file of videosFilesToUploads) { const promise = this.upload(file) Promises.push(promise) } // Use Promise.all to wait for all promises to resolve Promise.all(Promises) .then((results) => { // Check if every promise resolved successfully const allPromisesResolvedSuccessfully = results.every((result) => result == true); if (allPromisesResolvedSuccessfully) { console.log('All promises resolved successfully.'); resolve(true) } else { resolve(false) console.log('Some promises failed to resolve successfully.'); } }) .catch((error) => { resolve(false) console.error('An error occurred while resolving promises:', error); }); //}) }) } } 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 } } } 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 subscribeToUseCaseResponse: Function[] = [] 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.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()); } contentSetReady() { this.merging = false this.contentReady = true } }