@progress/kendo-angular-upload
Version:
Kendo UI Angular Upload Component
432 lines (431 loc) • 16.5 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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 }]; } });