From 5a84b9eb0c9278c27f9a06669d8c94011729a817 Mon Sep 17 00:00:00 2001 From: Paulo Date: Fri, 9 Mar 2018 10:07:00 -0300 Subject: [PATCH] Fixing Issues with file upload and adding more Helper classes! --- src/file-upload/file-chunk.class.ts | 458 ++++++++++++------------- src/file-upload/file-item.class.ts | 41 ++- src/file-upload/file-uploader.class.ts | 145 ++++---- 3 files changed, 339 insertions(+), 305 deletions(-) diff --git a/src/file-upload/file-chunk.class.ts b/src/file-upload/file-chunk.class.ts index 2b81e4e..afca897 100644 --- a/src/file-upload/file-chunk.class.ts +++ b/src/file-upload/file-chunk.class.ts @@ -1,237 +1,235 @@ export class FileChunk { - public stepSize:number = 1024 * 1024 * 3; - public rawFile:any = null; - public uploadProgress:number = null; - public uploading:boolean = null; - public uploadComplete:boolean = null; - public byteStepSize:number = null; - public totalSize:number = null; - public startByte:number = null; - public endByte:number = null; - public currentChunk:number = 0; - public totalChunks:number = null; - public uniqueIdentifier:string = null; - public totalSent:number = null; - public extraData:any = {}; - constructor(rawFile:any, options:any = {}) { - - if (typeof options !== 'undefined'){ - if (typeof options.byteStepSize !== 'undefined'){ - this.setByteStepSize(options.byteStepSize); - } - } - this.setRawFile(rawFile) - - this.setRawFile(rawFile); - this.setUploadProgress(0); - this.setUploading(false); - this.setUploadComplete(false); - this.setTotalSize(this.getRawFile().size); - this.setStartByte(0); - this.setEndByte(this.getByteStepSize()); - this.setCurrentChunk(0); - if (!this.getBrowserSliceMethod()){ - this.setTotalChunks(1); - } - else{ - this.setTotalChunks(Math.ceil(this.totalSize / this.byteStepSize)); - } - this.setUniqueIdenfier(this.generateUniqueIdentifier()); - this.setTotalSent(0); - } - - public setExtraData(index:any, value:any){ - this.extraData[index] = value; - } - - public getExtraData(index:any){ - return this.extraData[index]; - } - - //getters and setters - public setProgress(v:any){ - this.uploadProgress = v; - } - - public getProgress(){ - return this.uploadProgress; - } - - public setUploading(v:boolean){ - this.uploading = v; - } - - public getUploading(){ - return this.uploading; - } - - public getUploadComplete(){ - return this.uploadComplete; - } - - public setUploadComplete(v:boolean){ - this.uploadComplete = v; - } - - public setUploadProgress(v:number){ - this.uploadProgress = v; - } - - public getUploadProgress(){ - return this.uploadProgress; - } - - public getStartByte(){ - return this.startByte; - } - - public setStartByte(v:number){ - this.startByte = v; - } - - public getEndByte(){ - return this.endByte; - } - - public setEndByte(v:number){ - this.endByte = v; - } - - public getByteStepSize(){ - return this.byteStepSize; - } - - public setByteStepSize(v:number){ - this.byteStepSize = v; - } - - public setTotalSize(v:number){ - this.totalSize = v; - } - - public getTotalSize(){ - return this.totalSize; - } - - public getRawFile(){ - return this.rawFile; - } - - public setRawFile(v:File){ - this.rawFile = v; - } - - public getCurrentChunk(){ - return this.currentChunk; - } - - public setCurrentChunk(v:number){ - this.currentChunk = v; - } - - public getTotalChunks(){ - return this.totalChunks; - } - - public setTotalChunks(v:number){ - this.totalChunks = v; - } - - public setUniqueIdenfier(v:string){ - this.uniqueIdentifier = v; - } - - public getUniqueIdenfier(){ - return this.uniqueIdentifier; - } - - public getRawFileExtension(){ - const extension = this.getRawFileName().split('.'); - return extension[extension.length - 1]; - } - - public getRawFileName(){ - return this.getRawFile().name; - } - - public getContentType(){ - return this.getRawFile().type; - } - - public getTotalSent(){ - return this.totalSent; - } - - public setTotalSent(v:number){ - this.totalSent = v; - } - - public getCurrentRawFileChunk(){ - if (!this.getBrowserSliceMethod()){ - return this.getRawFile(); - } - else{ - return this.getRawFile()[this.getBrowserSliceMethod()](this.getStartByte(), this.getEndByte()); - } - } - - public retrocedeChunk(){ - if (!this.getBrowserSliceMethod()){ - return false; - } - - this.setEndByte(this.getStartByte()); - this.setStartByte(this.getStartByte() - this.getByteStepSize()); - this.setCurrentChunk(this.getCurrentChunk() - 1); - - if (this.getTotalSent() != 0){ - this.setTotalSent(this.getTotalSent() - this.getByteStepSize()); - } - } - - public prepareNextChunk(){ - if (!this.getBrowserSliceMethod()){ - return false; - } - - if (this.getEndByte() > this.getTotalSize()){ //finished - return false; - } - - this.setStartByte(this.getEndByte()); - this.setEndByte(this.getEndByte() + this.getByteStepSize()); - this.setCurrentChunk(this.getCurrentChunk() + 1); - - return true; - } - - public getBrowserSliceMethod():string{ - if (this.rawFile && typeof this.rawFile !== 'undefined'){ - if (this.rawFile.slice && typeof this.rawFile.slice == 'function'){ - return 'slice'; - } - else if(this.rawFile.mozSlice && typeof this.rawFile.mozSlice == 'function'){ - return 'mozSlice'; - } - else if(this.rawFile.webkitSlice && typeof this.rawFile.webkitSlice == 'function'){ - return 'webkitSlice'; - } - } - else{ - return null; - } - }//getBrowserSliceMethod() ends here - - - public generateUniqueIdentifier():string{ - var d = new Date().getTime(); - + public stepSize: number = 1024 * 1024 * 3; + public rawFile: any = null; + public uploadProgress: number = null; + public uploading: boolean = null; + public uploadComplete: boolean = null; + public byteStepSize: number = null; + public totalSize: number = null; + public startByte: number = null; + public endByte: number = null; + public currentChunk: number = 0; + public totalChunks: number = null; + public uniqueIdentifier: string = null; + public totalSent: number = null; + public extraData: any = {}; + + constructor(rawFile: any, options: any = {}) { + this.setByteStepSize(this.stepSize); + if (typeof options !== 'undefined') { + if (typeof options.byteStepSize !== 'undefined') { + this.setByteStepSize(options.byteStepSize); + } + } + this.setRawFile(rawFile); + + this.setRawFile(rawFile); + this.setUploadProgress(0); + this.setUploading(false); + this.setUploadComplete(false); + this.setTotalSize(this.getRawFile().size); + this.setStartByte(0); + this.setEndByte(this.getByteStepSize()); + this.setCurrentChunk(0); + if (!this.getBrowserSliceMethod()) { + this.setTotalChunks(1); + } else { + this.setTotalChunks(Math.ceil(this.totalSize / this.byteStepSize)); + } + this.setUniqueIdenfier(this.generateUniqueIdentifier()); + this.setTotalSent(0); + } + + public setExtraData(index: any, value: any) { + this.extraData[index] = value; + } + + public getExtraData(index: any) { + return this.extraData[index]; + } + + //getters and setters + public setProgress(v: any) { + this.uploadProgress = v; + } + + public getProgress() { + return this.uploadProgress; + } + + public setUploading(v: boolean) { + this.uploading = v; + } + + public getUploading() { + return this.uploading; + } + + public getUploadComplete() { + return this.uploadComplete; + } + + public setUploadComplete(v: boolean) { + this.uploadComplete = v; + } + + public setUploadProgress(v: number) { + this.uploadProgress = v; + } + + public getUploadProgress() { + return this.uploadProgress; + } + + public getStartByte() { + return this.startByte; + } + + public setStartByte(v: number) { + this.startByte = v; + } + + public getEndByte() { + return this.endByte; + } + + public setEndByte(v: number) { + this.endByte = v; + } + + public getByteStepSize() { + return this.byteStepSize; + } + + public setByteStepSize(v: number) { + this.byteStepSize = v; + } + + public setTotalSize(v: number) { + this.totalSize = v; + } + + public getTotalSize() { + return this.totalSize; + } + + public getRawFile() { + return this.rawFile; + } + + public setRawFile(v: File) { + this.rawFile = v; + } + + public getCurrentChunk() { + return this.currentChunk; + } + + public setCurrentChunk(v: number) { + this.currentChunk = v; + } + + public getTotalChunks() { + return this.totalChunks; + } + + public setTotalChunks(v: number) { + this.totalChunks = v; + } + + public setUniqueIdenfier(v: string) { + this.uniqueIdentifier = v; + } + + public getUniqueIdenfier() { + return this.uniqueIdentifier; + } + + public getRawFileExtension() { + const extension = this.getRawFileName().split('.'); + return extension[extension.length - 1]; + } + + public getRawFileName() { + return this.getRawFile().name; + } + + public getContentType() { + return this.getRawFile().type; + } + + public getTotalSent() { + return this.totalSent; + } + + public setTotalSent(v: number) { + this.totalSent = v; + } + + public getCurrentRawFileChunk() { + if (!this.getBrowserSliceMethod()) { + return this.getRawFile(); + } + else { + return this.getRawFile()[this.getBrowserSliceMethod()](this.getStartByte(), this.getEndByte()); + } + } + + public retrocedeChunk() { + if (!this.getBrowserSliceMethod()) { + return false; + } + + this.setEndByte(this.getStartByte()); + this.setStartByte(this.getStartByte() - this.getByteStepSize()); + this.setCurrentChunk(this.getCurrentChunk() - 1); + + if (this.getTotalSent() != 0) { + this.setTotalSent(this.getTotalSent() - this.getByteStepSize()); + } + } + + public prepareNextChunk() { + if (!this.getBrowserSliceMethod()) { + return false; + } + if (this.getEndByte() > this.getTotalSize() && this.getCurrentChunk() < this.getTotalChunks()) { // finished + return false; + } + this.setStartByte(this.getEndByte()); + this.setEndByte(this.getEndByte() + this.getByteStepSize()); + this.setCurrentChunk(this.getCurrentChunk() + 1); + if (this.getEndByte() > this.getTotalSize() && this.getCurrentChunk() === this.getTotalChunks()) { + // something went wrong with the calculations + this.setEndByte(this.getTotalSize()); + } + return true; + } + + public getBrowserSliceMethod(): string { + if (this.rawFile && typeof this.rawFile !== 'undefined') { + if (this.rawFile.slice && typeof this.rawFile.slice === 'function') { + return 'slice'; + } + else if (this.rawFile.mozSlice && typeof this.rawFile.mozSlice === 'function') { + return 'mozSlice'; + } + else if (this.rawFile.webkitSlice && typeof this.rawFile.webkitSlice === 'function') { + return 'webkitSlice'; + } + } + else { + return null; + } + }//getBrowserSliceMethod() ends here + + public generateUniqueIdentifier(): string{ + let d = new Date().getTime(); if (typeof performance !== 'undefined' && typeof performance.now === 'function'){ - d += performance.now(); //use high-precision timer if available + d += performance.now(); // use high-precision timer if available } - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = (d + Math.random() * 16) % 16 | 0; + const r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); diff --git a/src/file-upload/file-item.class.ts b/src/file-upload/file-item.class.ts index f9bb11c..c4e14c4 100644 --- a/src/file-upload/file-item.class.ts +++ b/src/file-upload/file-item.class.ts @@ -4,6 +4,7 @@ import { FileChunk } from './file-chunk.class' export class FileItem { public file: FileLikeObject; public _file: File; + public id: any; public alias: string; public url: string = '/'; public method: string; @@ -25,6 +26,8 @@ export class FileItem { public _currentChunk: number = 0; public _totalChunks: number = 0; + protected chunkTotalRetries = 10; + protected chunkRetries = 0; protected uploader: FileUploader; protected some: File; protected options: FileUploaderOptions; @@ -56,10 +59,18 @@ export class FileItem { this._currentChunk = this.fileChunks.currentChunk; this._totalChunks = this.fileChunks.totalChunks; } - public getNextChunk():any{ - this.fileChunks.prepareNextChunk() + public getCurrentChunkFile():any{ return this.fileChunks.getCurrentRawFileChunk(); } + public prepareNextChunk():void{ + this.fileChunks.prepareNextChunk(); + } + public getCurrentChunk():any{ + return this.fileChunks.getCurrentChunk() + } + public getTotalChunks():any{ + return this.fileChunks.getTotalChunks() + } public setIsUploading(val:boolean){ this.isUploading = val; if(this.fileChunks){ @@ -73,6 +84,12 @@ export class FileItem { public get fileChunks(): FileChunk{ return this._fileChunks; } + public getId(): any { + return this.id; + } + public setId(id: any) { + this.id = id; + } public cancel(): void { this.uploader.cancelItem(this); } @@ -175,14 +192,24 @@ export class FileItem { this.remove(); } } - public _onCompleteChunk(response: string, status: number, headers: ParsedResponseHeaders): void{ - this._onCompleteChunkCallnext(); + public _onCompleteChunk(response: string, status: number, headers: ParsedResponseHeaders): void { + this.chunkRetries = 0; + this._onCompleteChunkCallNext(); this.onCompleteChunk(response, status, headers); } - public _onCompleteChunkCallnext(): void{ - + public _onCompleteChunkCallNext(): void{ + //Let's Retry to send this chunk 4 times; + } + public _onErrorChunk(response: string, status: number, headers: ParsedResponseHeaders): void { + if (this.chunkRetries > this.chunkTotalRetries) { + this.uploader.onErrorItem(this, response, status, headers); + this.uploader.onCompleteItem(this, response, status, headers); + } else { + this.chunkRetries ++; + this.fileChunks.retrocedeChunk(); + this._onCompleteChunkCallNext(); + } } - public _prepareToUploading(): void { this.index = this.index || ++this.uploader._nextIndex; this.isReady = true; diff --git a/src/file-upload/file-uploader.class.ts b/src/file-upload/file-uploader.class.ts index abd2ab1..7835e47 100644 --- a/src/file-upload/file-uploader.class.ts +++ b/src/file-upload/file-uploader.class.ts @@ -2,7 +2,6 @@ import { EventEmitter } from '@angular/core'; import { FileLikeObject } from './file-like-object.class'; import { FileItem } from './file-item.class'; import { FileType } from './file-type.class'; -import { FileChunk } from './file-chunk.class'; function isFile(value: any): boolean { return (File && value instanceof File); } @@ -80,7 +79,6 @@ export class FileUploader { this.setOptions(options); this.response = new EventEmitter(); } - public setOptions(options: FileUploaderOptions): void { this.options = Object.assign(this.options, options); @@ -150,6 +148,7 @@ export class FileUploader { if (item.isUploading) { item.cancel(); } + this.onRemoveItem(item); this.queue.splice(index, 1); this.progress = this._getTotalProgress(); } @@ -174,16 +173,16 @@ export class FileUploader { } public cancelItem(value: FileItem): void { - let index = this.getIndexOfItem(value); - let item = this.queue[ index ]; - let prop = this.options.isHTML5 ? item._xhr : item._form; + const index = this.getIndexOfItem(value); + const item = this.queue[ index ]; + const prop = this.options.isHTML5 ? item._xhr : item._form; if (item && item.isUploading) { prop.abort(); } } public uploadAll(): void { - let items = this.getNotUploadedItems().filter((item: FileItem) => !item.isUploading); + const items = this.getNotUploadedItems().filter((item: FileItem) => !item.isUploading); if (!items.length) { return; } @@ -192,7 +191,7 @@ export class FileUploader { } public cancelAll(): void { - let items = this.getNotUploadedItems(); + const items = this.getNotUploadedItems(); items.map((item: FileItem) => item.cancel()); } @@ -207,7 +206,24 @@ export class FileUploader { public getIndexOfItem(value: any): number { return typeof value === 'number' ? value : this.queue.indexOf(value); } - + + public getIsErrorItems(): any[] { + return this.queue.filter((item: FileItem) => item.isError); + } + + public getIsCancelItems(): any[] { + return this.queue.filter((item: FileItem) => item.isCancel); + } + + public getIsSuccessItems(): any[] { + return this.queue.filter((item: FileItem) => item.isSuccess ); + } + + public getAllItems(): any[] { + return this.queue; + } + + public getNotUploadedItems(): any[] { return this.queue.filter((item: FileItem) => !item.isUploaded); } @@ -261,9 +277,15 @@ export class FileUploader { public onCancelItem(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): any { return { item, response, status, headers }; } + public onRemoveItem(item: FileItem ): any { + return { item }; + } public onCompleteChunk(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): any { return { item, response, status, headers }; } + public onErrorChunk(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): any { + return { item, response, status, headers }; + } public onCompleteItem(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): any { return { item, response, status, headers }; } @@ -308,7 +330,6 @@ export class FileUploader { this.progress = this._getTotalProgress(); this._render(); } - protected _headersGetter(parsedHeaders: ParsedResponseHeaders): any { return (name: any): any => { if (name) { @@ -321,43 +342,42 @@ export class FileUploader { let that = this; xhr.upload.onprogress = (event: any) => { let progress = Math.round(event.lengthComputable ? event.loaded * 100 / event.total : 0); - if(that.options.chunkSize > 0){ + if (that.options.chunkSize > 0) { progress = Math.round( ((item._currentChunk-1) * 100) / item._totalChunks) + Math.round(progress / item._totalChunks); } this._onProgressItem(item, progress); }; xhr.onload = () => { - - let headers = this._parseHeaders(xhr.getAllResponseHeaders()); - let response = this._transformResponse(xhr.response, headers); - let gist = this._isSuccessCode(xhr.status) ? 'Success' : 'Error'; - let method = '_on' + gist + 'Item'; - - - if(this.options.chunkSize > 0){ + const headers = this._parseHeaders(xhr.getAllResponseHeaders()); + const response = this._transformResponse(xhr.response, headers); + const gist = this._isSuccessCode(xhr.status) ? 'Success' : 'Error'; + const method = '_on' + gist + 'Item'; + if (this.options.chunkSize > 0) { item._chunkUploaders.pop(); if (item._currentChunk >= item._totalChunks) { (this as any)[ method ](item, response, xhr.status, headers); - this._onCompleteItem(item, response, xhr.status, headers); - }else{ - - this._onCompleteChunk(item,response,xhr.status,headers); + this._onCompleteItem(item, response, xhr.status, headers); + } else { + this._onCompleteChunk(item, response, xhr.status, headers); } - }else{ + } else { (this as any)[ method ](item, response, xhr.status, headers); - this._onCompleteItem(item, response, xhr.status, headers); + this._onCompleteItem(item, response, xhr.status, headers); } - }; xhr.onerror = () => { - let headers = this._parseHeaders(xhr.getAllResponseHeaders()); - let response = this._transformResponse(xhr.response, headers); - this._onErrorItem(item, response, xhr.status, headers); - this._onCompleteItem(item, response, xhr.status, headers); + const headers = this._parseHeaders(xhr.getAllResponseHeaders()); + const response = this._transformResponse(xhr.response, headers); + if (this.options.chunkSize > 0) { + this._onErrorChunk(item, response, xhr.status, headers); + } else { + this._onErrorItem(item, response, xhr.status, headers); + this._onCompleteItem(item, response, xhr.status, headers); + } }; xhr.onabort = () => { - let headers = this._parseHeaders(xhr.getAllResponseHeaders()); - let response = this._transformResponse(xhr.response, headers); + const headers = this._parseHeaders(xhr.getAllResponseHeaders()); + const response = this._transformResponse(xhr.response, headers); this._onCancelItem(item, response, xhr.status, headers); this._onCompleteItem(item, response, xhr.status, headers); }; @@ -381,22 +401,21 @@ export class FileUploader { if (xhr.readyState == XMLHttpRequest.DONE) { that.response.emit(xhr.responseText) } - } + }; return xhr; } - protected _buildMultiPartSendable(item: FileItem,start: number = null, end:number = null): FormData{ + protected _buildMultiPartSendable(item: FileItem): FormData{ let sendable: FormData; sendable = new FormData(); this._onBuildItemForm(item, sendable); let file: any = null; if(this.options.chunkSize > 0){ - - file = item.getNextChunk(); + file = item.getCurrentChunkFile(); }else{ file = item._file; } - const appendFile = () => sendable.append(item.alias, file, item.file.name); + const appendFile = () => sendable.append(item.alias, file, item.file.name); if (!this.options.parametersBeforeFiles) { appendFile(); } @@ -413,10 +432,10 @@ export class FileUploader { }); } if (this.options.chunkSize > 0 && this.options.totalChunkParam){ - sendable.append(this.options.totalChunkParam,item._totalChunks.toString() ); + sendable.append(this.options.totalChunkParam,item.getTotalChunks()); } if (this.options.chunkSize > 0 && this.options.currentChunkParam){ - sendable.append(this.options.currentChunkParam,item._currentChunk.toString() ); + sendable.append(this.options.currentChunkParam,item.getCurrentChunk() +1); } if (this.options.parametersBeforeFiles) { @@ -434,51 +453,38 @@ export class FileUploader { if (typeof item._file.size !== 'number') { throw new TypeError('The file specified is no longer valid'); } - if (!this.options.disableMultipart) { /* CHUNCKED FILE UPLOAD */ - if (this.options.chunkSize > 0){ - let chunkSize = this.options.chunkSize; - let chunkMethod = this.options.chunkMethod; - let NUM_CHUNKS = Math.max(Math.ceil(item._file.size / chunkSize), 1); - let CUR_CHUNK = 0; - let start = 0; - let end = chunkSize; - + if (this.options.chunkSize > 0) { + const chunkMethod = this.options.chunkMethod; item._chunkUploaders = []; item._currentChunk = 0; - item._totalChunks = NUM_CHUNKS; - - item._onCompleteChunkCallnext = function(): void{ + item._onCompleteChunkCallNext = function(): void{ item._currentChunk ++; - if(item._currentChunk > 1){ + if (item._currentChunk > 1) { item.method = chunkMethod; } - let sendable = this.uploader._buildMultiPartSendable(item,start,end); + const sendable = this.uploader._buildMultiPartSendable(item); let xhr = new XMLHttpRequest(); - xhr = this.uploader._xhrAppendEvents(xhr,item); - + xhr = this.uploader._xhrAppendEvents(xhr, item); + item._xhr = xhr; item._chunkUploaders.push(xhr); xhr.send(sendable); - - start = end; - end = start + chunkSize; - } - item.createFileChunk(this.options.chunkSize) - item.setIsUploading(true) - item._onCompleteChunkCallnext(); + item.prepareNextChunk() + }; + item.createFileChunk(this.options.chunkSize); + item.setIsUploading(true); + item._onCompleteChunkCallNext(); this._render(); return; - }else{ - sendable = this._buildMultiPartSendable(item); + } else { + sendable = this._buildMultiPartSendable(item); } - } else { sendable = this.options.formatDataFunction(item); } - // Append Evenets - xhr = this._xhrAppendEvents(xhr,item); + xhr = this._xhrAppendEvents(xhr, item); if (this.options.formatDataFunctionIsAsync) { sendable.then( (result: any) => xhr.send(JSON.stringify(result)) @@ -581,14 +587,17 @@ export class FileUploader { } protected _onProgressItem(item: FileItem, progress: any): void { - let total = this._getTotalProgress(progress); + const total = this._getTotalProgress(progress); this.progress = total; item._onProgress(progress); this.onProgressItem(item, progress); this.onProgressAll(total); this._render(); } - + protected _onErrorChunk(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): void { + item._onErrorChunk(response, status, headers); + this.onErrorChunk(item, response, status, headers) + } protected _onSuccessItem(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): void { item._onSuccess(response, status, headers); this.onSuccessItem(item, response, status, headers);