From 9e8aec3e4ab4d211a323e8ddc9819f74354ffcdb Mon Sep 17 00:00:00 2001 From: Paulo Date: Fri, 9 Feb 2018 22:45:20 -0200 Subject: [PATCH] Adding Support For Chunk File Upload Changes to implement chunk file upload, more information on the README --- README.md | 19 +++ src/file-upload/file-item.class.ts | 14 ++ src/file-upload/file-uploader.class.ts | 182 +++++++++++++++++++------ 3 files changed, 173 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index a422bdf..0426d78 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,11 @@ Easy to use Angular2 directives for files upload ([demo](http://valor-software.g 5. `formatDataFunction` - Function to modify the request body. 'DisableMultipart' must be 'true' for this function to be called. 6. `formatDataFunctionIsAsync` - Informs if the function sent in 'formatDataFunction' is asynchronous. Defaults to false. 7. `parametersBeforeFiles` - States if additional parameters should be appended before or after the file. Defaults to false. + 8. `chunkSize` - The Size of each chunk in Bytes, if this parameter is set the file chunk upload functionality will run. Defaults to Null. + 9. `currentChunkParam` - Parameter Sent with the chunk request, the current chunk number of the file. Defaults to 'current_chunk'. + 10. `totalChunkParam` - Parameter Sent with the chunk request, the total number of chunks of the file. Defaults to 'total_chunks'. + 11. `chunkMethod` - After the first chunk, this method is set. Defaults to 'PUT' because is the standard for update. + ### Events @@ -84,6 +89,20 @@ Please follow this guidelines when reporting bugs and feature requests: Thanks for understanding! +### Using/Sending Chunk Files Feature + + If you want to send the files chunked you can just set the chunk paramets on the uploader object + + If your chunk request changes the link after the first request you should use this code + ``` + this.uploader.onCompleteChunk = (item,response,status,headers)=>{ + response = JSON.parse(response); + if(response['id']){ + item.url = YOUR_NEW_URL+response['id']+'/'; + } + } + ```txt + ### License The MIT License (see the [LICENSE](https://github.com/valor-software/ng2-file-upload/blob/master/LICENSE) file for the full text) diff --git a/src/file-upload/file-item.class.ts b/src/file-upload/file-item.class.ts index daa732e..7aad7ed 100644 --- a/src/file-upload/file-item.class.ts +++ b/src/file-upload/file-item.class.ts @@ -20,6 +20,9 @@ export class FileItem { public index: number = void 0; public _xhr: XMLHttpRequest; public _form: any; + public _chunkUploaders: any = []; + public _currentChunk: number = 0; + public _totalChunks: number = 0; protected uploader: FileUploader; protected some: File; @@ -31,6 +34,7 @@ export class FileItem { this.options = options; this.file = new FileLikeObject(some); this._file = some; + if (uploader.options) { this.method = uploader.options.method || 'POST'; this.alias = uploader.options.itemAlias || 'file'; @@ -82,6 +86,9 @@ export class FileItem { public onComplete(response: string, status: number, headers: ParsedResponseHeaders): any { return { response, status, headers }; } + public onCompleteChunk(response: string, status: number, headers: ParsedResponseHeaders): any { + return { response, status, headers }; + } public _onBeforeUpload(): void { this.isReady = true; @@ -146,6 +153,13 @@ export class FileItem { this.remove(); } } + public _onCompleteChunk(response: string, status: number, headers: ParsedResponseHeaders): void{ + this._onCompleteChunkCallnext(); + this.onCompleteChunk(response, status, headers); + } + public _onCompleteChunkCallnext(): void{ + + } public _prepareToUploading(): void { this.index = this.index || ++this.uploader._nextIndex; diff --git a/src/file-upload/file-uploader.class.ts b/src/file-upload/file-uploader.class.ts index 0a60acf..9e16b6f 100644 --- a/src/file-upload/file-uploader.class.ts +++ b/src/file-upload/file-uploader.class.ts @@ -39,6 +39,11 @@ export interface FileUploaderOptions { parametersBeforeFiles?: boolean; formatDataFunction?: Function; formatDataFunctionIsAsync?: boolean; + chunkSize?: number; + currentChunkParam?: string; + totalChunkParam?: string; + chunkMethod?: string; + } export class FileUploader { @@ -51,11 +56,18 @@ export class FileUploader { public autoUpload: any; public authTokenHeader: string; public response: EventEmitter; - + public chunkSize: number = null; + public currentChunkParam: string = "current_chunk"; + public totalChunkParam: string = "total_chunks"; + public chunkMethod: string = "PUT"; public options: FileUploaderOptions = { autoUpload: false, isHTML5: true, filters: [], + chunkSize: null, + currentChunkParam: "current_chunk", + totalChunkParam: "total_chunks", + chunkMethod: "PUT", removeAfterUpload: false, disableMultipart: false, formatDataFunction: (item: FileItem) => item._file, @@ -75,6 +87,10 @@ export class FileUploader { this.authToken = this.options.authToken; this.authTokenHeader = this.options.authTokenHeader || 'Authorization'; this.autoUpload = this.options.autoUpload; + this.chunkSize = this.options.chunkSize; + this.currentChunkParam = this.options.currentChunkParam; + this.totalChunkParam = this.options.totalChunkParam; + this.chunkMethod = this.options.chunkMethod; this.options.filters.unshift({ name: 'queueLimit', fn: this._queueLimitFilter }); if (this.options.maxFileSize) { @@ -245,7 +261,9 @@ export class FileUploader { public onCancelItem(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): any { return { item, response, status, headers }; } - + public onCompleteChunk(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 }; } @@ -271,7 +289,12 @@ export class FileUploader { item._onError(response, status, headers); this.onErrorItem(item, response, status, headers); } - + public _onCompleteChunk(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): void{ + this.onCompleteChunk(item,response,status,headers); + item._onCompleteChunk(response, status, headers); + this.progress = this._getTotalProgress(); + this._render(); + } public _onCompleteItem(item: FileItem, response: string, status: number, headers: ParsedResponseHeaders): void { item._onComplete(response, status, headers); this.onCompleteItem(item, response, status, headers); @@ -294,55 +317,37 @@ export class FileUploader { return parsedHeaders; }; } - - protected _xhrTransport(item: FileItem): any { + protected _xhrAppendEvents(xhr: XMLHttpRequest, item: FileItem): XMLHttpRequest{ let that = this; - let xhr = item._xhr = new XMLHttpRequest(); - let sendable: any; - this._onBeforeUploadItem(item); - - if (typeof item._file.size !== 'number') { - throw new TypeError('The file specified is no longer valid'); - } - if (!this.options.disableMultipart) { - sendable = new FormData(); - this._onBuildItemForm(item, sendable); - - const appendFile = () => sendable.append(item.alias, item._file, item.file.name); - if (!this.options.parametersBeforeFiles) { - appendFile(); - } - - // For AWS, Additional Parameters must come BEFORE Files - if (this.options.additionalParameter !== undefined) { - Object.keys(this.options.additionalParameter).forEach((key: string) => { - let paramVal = this.options.additionalParameter[ key ]; - // Allow an additional parameter to include the filename - if (typeof paramVal === 'string' && paramVal.indexOf('{{file_name}}') >= 0) { - paramVal = paramVal.replace('{{file_name}}', item.file.name); - } - sendable.append(key, paramVal); - }); - } - - if (this.options.parametersBeforeFiles) { - appendFile(); - } - } else { - sendable = this.options.formatDataFunction(item); - } - xhr.upload.onprogress = (event: any) => { let progress = Math.round(event.lengthComputable ? event.loaded * 100 / event.total : 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'; - (this as any)[ method ](item, response, xhr.status, headers); - this._onCompleteItem(item, response, xhr.status, headers); + + + 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); + } + }else{ + (this as any)[ method ](item, response, xhr.status, headers); + this._onCompleteItem(item, response, xhr.status, headers); + } + }; xhr.onerror = () => { let headers = this._parseHeaders(xhr.getAllResponseHeaders()); @@ -356,6 +361,7 @@ export class FileUploader { this._onCancelItem(item, response, xhr.status, headers); this._onCompleteItem(item, response, xhr.status, headers); }; + xhr.open(item.method, item.url, true); xhr.withCredentials = item.withCredentials; if (this.options.headers) { @@ -376,6 +382,98 @@ export class FileUploader { that.response.emit(xhr.responseText) } } + return xhr; + } + + protected _buildMultiPartSendable(item: FileItem,start: number = null, end:number = null): FormData{ + let sendable: FormData; + sendable = new FormData(); + this._onBuildItemForm(item, sendable); + let file: any = item._file; + if( start && end ){ + file = file.slice(start,end); + } + const appendFile = () => sendable.append(item.alias, file, item.file.name); + if (!this.options.parametersBeforeFiles) { + appendFile(); + } + + // For AWS, Additional Parameters must come BEFORE Files + if (this.options.additionalParameter !== undefined) { + Object.keys(this.options.additionalParameter).forEach((key: string) => { + let paramVal = this.options.additionalParameter[ key ]; + // Allow an additional parameter to include the filename + if (typeof paramVal === 'string' && paramVal.indexOf('{{file_name}}') >= 0) { + paramVal = paramVal.replace('{{file_name}}', item.file.name); + } + sendable.append(key, paramVal); + }); + } + if (this.options.chunkSize > 0 && this.options.totalChunkParam){ + sendable.append(this.options.totalChunkParam,item._totalChunks.toString() ); + } + if (this.options.chunkSize > 0 && this.options.currentChunkParam){ + sendable.append(this.options.currentChunkParam,item._currentChunk.toString() ); + } + + if (this.options.parametersBeforeFiles) { + appendFile(); + } + + return sendable; + } + protected _xhrTransport(item: FileItem): any { + let that = this; + let xhr = item._xhr = new XMLHttpRequest(); + let sendable: any; + this._onBeforeUploadItem(item); + + 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; + + item._chunkUploaders = []; + item._currentChunk = 0; + item._totalChunks = NUM_CHUNKS; + + item._onCompleteChunkCallnext = function(): void{ + item._currentChunk ++; + if(item._currentChunk > 1){ + item.method = chunkMethod; + } + let sendable = this.uploader._buildMultiPartSendable(item,start,end); + let xhr = new XMLHttpRequest(); + xhr = this.uploader._xhrAppendEvents(xhr,item); + + item._chunkUploaders.push(xhr); + xhr.send(sendable); + + start = end; + end = start + chunkSize; + } + item._onCompleteChunkCallnext(); + this._render(); + return; + }else{ + sendable = this._buildMultiPartSendable(item); + } + + } else { + sendable = this.options.formatDataFunction(item); + } + + // Append Evenets + xhr = this._xhrAppendEvents(xhr,item); if (this.options.formatDataFunctionIsAsync) { sendable.then( (result: any) => xhr.send(JSON.stringify(result))