Adding Support For Chunk File Upload

Changes to implement chunk file upload, more information on the README
This commit is contained in:
Paulo
2018-02-09 22:45:20 -02:00
parent fb48f4411d
commit 9e8aec3e4a
3 changed files with 173 additions and 42 deletions

View File

@@ -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)

View File

@@ -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;

View File

@@ -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<any>;
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))