import { Injectable } from '@angular/core'; import { FileStatus, FileUploadEvent } from '@core/typings/file.typing'; import { FileService, YcFile } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { Observable, Subscription } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class FileUploadProgressService { private maxFileSizeBytes = 50 /* MB */ * 1024 /* KB */ * 1024 /* B */; private readonly permittedExtensions: string[] = []; constructor ( private i18n: I18nService, fileService: FileService ) { this.permittedExtensions = Object.keys(fileService.fileTypes).reduce((acc, icon) => { return [ ...acc, ...fileService.fileTypes[icon] ]; }, []); } private maxFileSizeToUserFriendly (): string { let currentSize = this.maxFileSizeBytes; let currentIncrement = 'B'; let increments = [ 'KB', 'MB', 'GB' ]; while (true) { if (currentSize > 1024 && increments.length) { currentSize = Math.floor(currentSize / 1024); currentIncrement = increments[0]; increments = increments.slice(1); } else { return currentSize + currentIncrement; } } } private checkIfFileValid (userUploadRequest: YcFile): boolean { const fileName = userUploadRequest.fileName; if (userUploadRequest.file.size > this.maxFileSizeBytes) { userUploadRequest.setStatusText( this.i18n.translate('common:textFileTooLarge', { fileName, fileSize: this.maxFileSizeToUserFriendly() }, '__fileName__ is too large. The max file size is __fileSize__.'), 'exclamation-triangle', 'danger' ); return false; } if (!this.permittedExtensions.some(extension => fileName.toLowerCase().endsWith('.' + extension))) { userUploadRequest.setStatusText( this.i18n.translate('common:textFileInvalidExtension', { fileName }, '__fileName__ has an invalid extension. Please upload a different file.'), 'exclamation-triangle', 'danger' ); return false; } return true; } /** * Reacts to upload events and updates the progress and status of a user file upload request. Used for handling uploads in the `multi-dropzone` * * @param uploadRequest The API request for uploading the file * @param userUploadRequest The file that the end user requested to be uploaded (provided from the dropzone input) */ processFileUploadRequest ( uploadRequest: Observable>, userUploadRequest: YcFile ): Promise { const fileName = userUploadRequest.fileName; let sub: Subscription; return new Promise((res, rej) => { const valid = this.checkIfFileValid(userUploadRequest); userUploadRequest.cancel$.subscribe(() => { sub?.unsubscribe(); userUploadRequest.complete(); rej(); }); if (!valid) { rej(); return; } sub = uploadRequest.subscribe((e) => { switch (e.status) { case FileStatus.FAILED_VIRUS_SCAN: userUploadRequest.setStatusText( this.i18n.translate('common:textFileFailedVirusScan', { fileName }, '__fileName__ failed the virus scan. Please upload a new file.'), 'exclamation-triangle', 'danger' ); rej(e.result); sub.unsubscribe(); break; case FileStatus.FAILED_API: userUploadRequest.setStatusText( this.i18n.translate('common:textFileCouldNotBeUploaded', { fileName }, '__fileName__ could not be uploaded. Please try again.'), 'exclamation-triangle', 'danger' ); rej(e.result); sub.unsubscribe(); break; case FileStatus.UPLOAD_VIRUS_SCAN: case FileStatus.AWAITING_VIRUS_SCAN: userUploadRequest.setStatusText( this.i18n.translate('common:textScanningForViruses', {}, 'Scanning for viruses'), 'search' ); break; case FileStatus.UPLOAD_API: case FileStatus.AWAITING_API: userUploadRequest.setStatusText( this.i18n.translate('common:textUploadingFile', {}, 'Uploading file...'), 'sync fa-spin' ); break; case FileStatus.COMPLETE: setTimeout(() => { userUploadRequest.setProgress(0); userUploadRequest.setStatusText( '', '' ); userUploadRequest.complete(); }, 250); res(e.result); break; } const progress = this.calculatePercentage(e); userUploadRequest.setProgress(progress); }); }); } /** * Returns a rough percentage of the progress of the file upload * * Virus upload (0 - 34) * Await virus scan (35) * API upload (50 - 85) * Await API (85 - 99) * Complete (100) * * * @param fileEvent Current event to be evaluated */ calculatePercentage (fileEvent: FileUploadEvent): number { return 100 * this.getRawPercent(fileEvent); } private getRawPercent (fileEvent: FileUploadEvent) { switch (fileEvent.status) { case FileStatus.UPLOAD_VIRUS_SCAN: return (.35 * fileEvent.progress); case FileStatus.AWAITING_VIRUS_SCAN: return .35; case FileStatus.UPLOAD_API: return .5 + (.35 * fileEvent.progress); case FileStatus.AWAITING_API: return .85; case FileStatus.FAILED_API: case FileStatus.FAILED_VIRUS_SCAN: return 0; default: return 1; } } }