UNPKG

@progress/kendo-angular-upload

Version:

Kendo UI Angular Upload Component

432 lines (431 loc) 16.5 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { HttpClient, HttpEventType, HttpHeaders, HttpRequest, HttpResponse } from '@angular/common/http'; import { EventEmitter, Injectable } from '@angular/core'; import { FileState } from './types'; import { FileMap } from './types/file-map'; import { CancelEvent, ClearEvent, ErrorEvent, PauseEvent, RemoveEvent, ResumeEvent, SelectEvent, SuccessEvent, UploadEvent, UploadProgressEvent } from './events'; import { getInitialFileInfo, convertFileToFileInfo } from './common/util'; import { ChunkMap } from './types/chunk-map'; import * as i0 from "@angular/core"; import * as i1 from "@angular/common/http"; /** * @hidden */ export class UploadService { http; cancelEvent = new EventEmitter(); clearEvent = new EventEmitter(); completeEvent = new EventEmitter(); errorEvent = new EventEmitter(); pauseEvent = new EventEmitter(); removeEvent = new EventEmitter(); resumeEvent = new EventEmitter(); selectEvent = new EventEmitter(); successEvent = new EventEmitter(); uploadEvent = new EventEmitter(); uploadProgressEvent = new EventEmitter(); /** * Required for the `ControlValueAccessor` integration */ changeEvent = new EventEmitter(); /** * Default async settings */ async = { autoUpload: true, batch: false, chunk: false, concurrent: true, removeField: "fileNames", removeHeaders: new HttpHeaders(), removeMethod: "POST", removeUrl: "", responseType: "json", saveField: "files", saveHeaders: new HttpHeaders(), saveMethod: "POST", saveUrl: "", withCredentials: true }; /** * Default chunk settings */ chunk = { autoRetryAfter: 100, size: 1024 * 1024, maxAutoRetries: 1, resumable: true }; component = 'Upload'; chunkMap = new ChunkMap(); fileList = new FileMap(); constructor(http) { this.http = http; } get files() { return this.fileList; } setChunkSettings(settings) { if (settings !== false) { this.async.chunk = true; if (typeof settings === "object") { this.chunk = Object.assign({}, this.chunk, settings); } } } onChange() { const files = this.fileList.filesFlat.filter((file) => { return file.state === FileState.Initial || file.state === FileState.Uploaded; }); this.changeEvent.emit(files.length > 0 ? files : null); } addFiles(files) { const selectEventArgs = new SelectEvent(files); this.selectEvent.emit(selectEventArgs); if (!selectEventArgs.isDefaultPrevented()) { for (const file of files) { this.fileList.add(file); } if (this.async.autoUpload) { this.uploadFiles(); } } if (this.component === 'FileSelect') { const flatFiles = this.fileList.filesFlat; this.changeEvent.emit(flatFiles.length > 0 ? flatFiles : null); } } addInitialFiles(initialFiles) { this.fileList.clear(); initialFiles.forEach((file) => { const fakeFile = getInitialFileInfo(file); this.fileList.add(fakeFile); }); } addInitialFileSelectFiles(initialFiles) { this.fileList.clear(); initialFiles.forEach((file) => { if (file instanceof File) { this.fileList.add(convertFileToFileInfo(file)); } else { this.fileList.add(getInitialFileInfo(file)); } }); } resumeFile(uid) { const fileToResume = this.fileList.get(uid); this.resumeEvent.emit(new ResumeEvent(fileToResume[0])); this.fileList.setFilesStateByUid(uid, FileState.Uploading); this._uploadFiles([fileToResume]); } pauseFile(uid) { const pausedFile = this.fileList.get(uid)[0]; this.pauseEvent.emit(new PauseEvent(pausedFile)); this.fileList.setFilesStateByUid(uid, FileState.Paused); } removeFiles(uid) { const removedFiles = this.fileList.get(uid); // Clone the Headers so that the default ones are not overridden const removeEventArgs = new RemoveEvent(removedFiles, this.cloneRequestHeaders(this.async.removeHeaders)); this.removeEvent.emit(removeEventArgs); if (!removeEventArgs.isDefaultPrevented()) { if (this.component === 'Upload' && (removedFiles[0].state === FileState.Uploaded || removedFiles[0].state === FileState.Initial)) { this.performRemove(removedFiles, removeEventArgs); } else { this.fileList.remove(uid); if (this.component === 'FileSelect') { const flatFiles = this.fileList.filesFlat; this.changeEvent.emit(flatFiles.length > 0 ? flatFiles : null); } } } } cancelFiles(uid) { const canceledFiles = this.fileList.get(uid); const cancelEventArgs = new CancelEvent(canceledFiles); this.cancelEvent.emit(cancelEventArgs); for (const file of canceledFiles) { if (file.httpSubscription) { file.httpSubscription.unsubscribe(); } } this.fileList.remove(uid); this.checkAllComplete(); } clearFiles() { const clearEventArgs = new ClearEvent(); this.clearEvent.emit(clearEventArgs); if (!clearEventArgs.isDefaultPrevented()) { const triggerChange = this.fileList.hasFileWithState([ FileState.Initial, FileState.Uploaded ]); this.fileList.clear(); if (triggerChange) { this.onChange(); } } } uploadFiles() { let filesToUpload = []; if (this.async.concurrent) { filesToUpload = this.fileList.filesToUpload; } if (!this.async.concurrent && !this.fileList.hasFileWithState([FileState.Uploading])) { filesToUpload = this.fileList.firstFileToUpload ? [this.fileList.firstFileToUpload] : []; } if (filesToUpload && filesToUpload.length > 0) { this._uploadFiles(filesToUpload); } } retryFiles(uid) { const filesToRetry = [this.fileList.get(uid)]; if (filesToRetry) { this._uploadFiles(filesToRetry); } } _uploadFiles(allFiles) { for (const filesToUpload of allFiles) { if (filesToUpload[0].state === FileState.Paused) { return; } // Clone the Headers so that the default ones are not overridden const uploadEventArgs = new UploadEvent(filesToUpload, this.cloneRequestHeaders(this.async.saveHeaders)); this.uploadEvent.emit(uploadEventArgs); if (!uploadEventArgs.isDefaultPrevented()) { this.fileList.setFilesState(filesToUpload, FileState.Uploading); const httpSubcription = this.performUpload(filesToUpload, uploadEventArgs); filesToUpload.forEach((file) => { file.httpSubscription = httpSubcription; }); } else { this.fileList.remove(filesToUpload[0].uid); this.checkAllComplete(); } } } performRemove(files, removeEventArgs) { const async = this.async; const fileNames = files.map((file) => { return file.name; }); const formData = this.populateRemoveFormData(fileNames, removeEventArgs.data); const options = this.populateRequestOptions(removeEventArgs.headers, false); const removeRequest = new HttpRequest(async.removeMethod, async.removeUrl, formData, options); this.http.request(removeRequest) .subscribe(success => { this.onSuccess(success, files, "remove"); }, error => { this.onError(error, files, "remove"); }); } performUpload(files, uploadEventArgs) { const async = this.async; const formData = this.populateUploadFormData(files, uploadEventArgs.data); const options = this.populateRequestOptions(uploadEventArgs.headers); const uploadRequest = new HttpRequest(async.saveMethod, async.saveUrl, formData, options); const httpSubscription = this.http.request(uploadRequest) .subscribe(event => { if (event.type === HttpEventType.UploadProgress && !this.async.chunk) { this.onProgress(event, files); } else if (event instanceof HttpResponse) { this.onSuccess(event, files, "upload"); this.checkAllComplete(); } }, error => { this.onError(error, files, "upload"); this.checkAllComplete(); }); return httpSubscription; } onSuccess(successResponse, files, operation) { if (operation === "upload" && this.async.chunk) { this.onChunkProgress(files); if (this.isChunkUploadComplete(files[0].uid)) { this.removeChunkInfo(files[0].uid); } else { this.updateChunkInfo(files[0].uid); this._uploadFiles([files]); return; } } const successArgs = new SuccessEvent(files, operation, successResponse); this.successEvent.emit(successArgs); if (operation === "upload") { this.fileList.setFilesState(files, successArgs.isDefaultPrevented() ? FileState.Failed : FileState.Uploaded); } else { if (!successArgs.isDefaultPrevented()) { this.fileList.remove(files[0].uid); } } if (!successArgs.isDefaultPrevented()) { this.onChange(); } } onError(errorResponse, files, operation) { if (operation === "upload" && this.async.chunk) { const maxRetries = this.chunk.maxAutoRetries; const chunkInfo = this.chunkMap.get(files[0].uid); if (chunkInfo.retries < maxRetries) { chunkInfo.retries += 1; setTimeout(() => { this.retryFiles(files[0].uid); }, this.chunk.autoRetryAfter); return; } } const errorArgs = new ErrorEvent(files, operation, errorResponse); this.errorEvent.emit(errorArgs); if (operation === "upload") { this.fileList.setFilesState(files, FileState.Failed); } } onProgress(event, files) { const percentComplete = Math.round(100 * event.loaded / event.total); const progressArgs = new UploadProgressEvent(files, percentComplete < 100 ? percentComplete : 100); this.uploadProgressEvent.emit(progressArgs); } onChunkProgress(files) { const chunkInfo = this.chunkMap.get(files[0].uid); let percentComplete = 0; if (chunkInfo) { if (chunkInfo.index === chunkInfo.totalChunks - 1) { percentComplete = 100; } else { percentComplete = Math.round(((chunkInfo.index + 1) / chunkInfo.totalChunks) * 100); } } const progressArgs = new UploadProgressEvent(files, percentComplete < 100 ? percentComplete : 100); this.uploadProgressEvent.emit(progressArgs); } checkAllComplete() { if (!this.fileList.hasFileWithState([ FileState.Uploading, FileState.Paused ]) && this.areAllSelectedFilesHandled()) { this.completeEvent.emit(); } else if (this.shouldUploadNextFile()) { this.uploadFiles(); } } shouldUploadNextFile() { return !this.async.concurrent && this.fileList.hasFileWithState([FileState.Selected]) && !this.fileList.hasFileWithState([FileState.Uploading]); } areAllSelectedFilesHandled() { const validSelectedFiles = this.fileList.getFilesWithState(FileState.Selected).filter(file => !file.validationErrors); return validSelectedFiles.length === 0; } cloneRequestHeaders(headers) { const cloned = {}; if (headers) { headers.keys().forEach((key) => { if (key !== 'constructor' && key !== '__proto__' && key !== 'prototype') cloned[key] = headers.get(key); }); } return new HttpHeaders(cloned); } populateRequestOptions(headers, reportProgress = true) { return { headers: headers, reportProgress: reportProgress, responseType: this.async.responseType, withCredentials: this.async.withCredentials }; } populateUploadFormData(files, clientData) { const saveField = this.async.saveField; const data = new FormData(); this.populateClientFormData(data, clientData); if (this.async.chunk) { data.append(saveField, this.getNextChunk(files[0])); data.append("metadata", this.getChunkMetadata(files[0])); } else { for (const file of files) { data.append(saveField, file.rawFile); } } return data; } populateRemoveFormData(fileNames, clientData) { const data = new FormData(); this.populateClientFormData(data, clientData); for (const fileName of fileNames) { data.append(this.async.removeField, fileName); } return data; } populateClientFormData(data, clientData) { for (const key in clientData) { if (clientData.hasOwnProperty(key)) { data.append(key, clientData[key]); } } } /* Chunking Helper Methods Section */ getNextChunk(file) { const info = this.getChunkInfo(file); const newPosition = info.position + this.chunk.size; return file.rawFile.slice(info.position, newPosition); } getChunkInfo(file) { let chunkInfo = this.chunkMap.get(file.uid); if (!chunkInfo) { const totalChunks = file.size > 0 ? Math.ceil(file.size / this.chunk.size) : 1; chunkInfo = this.chunkMap.add(file.uid, totalChunks); } return chunkInfo; } updateChunkInfo(uid) { const chunkInfo = this.chunkMap.get(uid); if (chunkInfo.index < chunkInfo.totalChunks - 1) { chunkInfo.index += 1; chunkInfo.position += this.chunk.size; chunkInfo.retries = 0; } } removeChunkInfo(uid) { this.chunkMap.remove(uid); } getChunkMetadata(file) { const chunkInfo = this.chunkMap.get(file.uid); const chunkMetadata = { chunkIndex: chunkInfo.index, contentType: file.rawFile.type, fileName: file.name, fileSize: file.size, fileUid: file.uid, totalChunks: chunkInfo.totalChunks }; return JSON.stringify(chunkMetadata); } isChunkUploadComplete(uid) { const chunkInfo = this.chunkMap.get(uid); if (chunkInfo) { return chunkInfo.index + 1 === chunkInfo.totalChunks; } return false; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i1.HttpClient }]; } });