import ajax from './ajax-request'; import { ConfigureFn, AjaxResponse, AjaxError } from './ajax-request'; import FileRecord from './file-record'; type ProgressFn = (event: ProgressEvent) => void; export type CreateFormDataFn = (fileRecord: FileRecord) => FormData; export interface TusOptions { retryDelays?: number[]; metadata?: any; } export type TusOptionsFn = (fileRecord: FileRecord) => TusOptions; class UploadHelper { public addHeaders(xhr: XMLHttpRequest, headers: object): XMLHttpRequest { xhr.setRequestHeader('Accept', 'application/json'); if (headers) { for (const key in headers) { if (headers.hasOwnProperty(key)) { xhr.setRequestHeader(key, (headers as any)[key]); } } } return xhr; } public doUpload( url: string, headers: object, formData: FormData, progressCallback: ProgressFn, configureFn?: ConfigureFn ): Promise { return ajax.post(url, formData, (xhr) => { this.addHeaders(xhr, headers); xhr.upload.addEventListener('progress', progressCallback, false); if (typeof configureFn === 'function') { configureFn(xhr); } }); } public doDeleteUpload( url: string, headers: object, uploadData: any, configureFn?: ConfigureFn ): Promise { if (typeof uploadData !== 'string') { uploadData = JSON.stringify(uploadData); } return ajax.delete(url, uploadData, (xhr) => { xhr.setRequestHeader('Content-Type', 'application/json'); this.addHeaders(xhr, headers); if (typeof configureFn === 'function') { configureFn(xhr); } }); } public doUpdateUpload( url: string, headers: object, uploadData: any, configureFn?: ConfigureFn ): Promise { if (typeof uploadData !== 'string') { uploadData = JSON.stringify(uploadData); } return ajax.put(url, uploadData, (xhr) => { xhr.setRequestHeader('Content-Type', 'application/json'); this.addHeaders(xhr, headers); if (typeof configureFn === 'function') { configureFn(xhr); } }); } // doUploadAxios(axios, formData, progressCallback){ // return axios.post('/upload', formData, { // onUploadProgress: progressCallback, // }); // } // doDeleteUploadAxios(axios, data, configureFn){ // return axios.delete('/upload', data, { // }); // } public prepareUploadError(fileRecord: FileRecord, err: AjaxError, timeout?: number) { let errorText = err.message; if (err.response && err.response.data) { try { const errorMsg = err.response.data.error || JSON.parse(err.response.data).error; errorText = errorMsg; } catch (e) { // ignore } } if (!fileRecord.error) { fileRecord.error = {}; } fileRecord.error.upload = errorText; fileRecord.upload.data = undefined; fileRecord.upload.error = errorText; if (timeout) { window.setTimeout(() => { if (!fileRecord.error) { fileRecord.error = {}; } fileRecord.error.upload = false; if (!fileRecord.error.size && !fileRecord.error.type) { fileRecord.error = false; } }, timeout); } } public upload( url: string, headers: object, fileRecords: FileRecord[], createFormData?: CreateFormDataFn, progressFn?: (progress: number) => void, configureFn?: ConfigureFn ) { let updateOverallProgress = () => { /* no op */ }; if (progressFn) { updateOverallProgress = () => { let prgTotal = 0; for (const fileRecord of fileRecords) { prgTotal += fileRecord.progress() as number; } progressFn(prgTotal / fileRecords.length); }; } const promises: Array> = []; let failedUploadsCount = 0; for (const fileRecord of fileRecords) { let formData; if (typeof createFormData === 'function') { formData = createFormData(fileRecord); } else { formData = new FormData(); formData.append('file', fileRecord.file as File); formData.append('filename', fileRecord.name()); } const promise = this.doUpload( url, headers, formData, (progressEvent) => { const percentCompleted = (progressEvent.loaded * 100) / progressEvent.total; // do not complete until promise resolved fileRecord.progress(percentCompleted >= 100 ? 99.9999 : percentCompleted); updateOverallProgress(); }, (xhr) => { fileRecord.xhr = xhr; if (typeof configureFn === 'function') { configureFn(xhr); } } ); promises.push( new Promise((resolve, reject) => { promise.then( (response) => { delete fileRecord.xhr; fileRecord.upload.data = response.data; fileRecord.upload.error = false; fileRecord.progress(100); if (fileRecord.xhrQueue) { fileRecord.xhrQueue(); delete fileRecord.xhrQueue; } resolve(response); } /* */, (err) => { this.prepareUploadError(fileRecord, err); resolve(err); failedUploadsCount++; } /* */ ); }) ); } // return Promise.all(promises); return new Promise((resolve, reject) => { Promise.all(promises).then((responses) => { if (failedUploadsCount === promises.length) { // all uploads failed reject(responses); return; } resolve(responses); }, reject); }); } public deleteUpload( url: string, headers: object, fileRecord: FileRecord, uploadData?: any, configureFn?: ConfigureFn ) { return new Promise((resolve, reject) => { if (fileRecord.xhr) { fileRecord.xhr.abort(); } if (uploadData === undefined) { uploadData = fileRecord.upload.data; } if (uploadData) { this.doDeleteUpload(url, headers, uploadData, (xhr) => { if (typeof configureFn === 'function') { configureFn(xhr); } }).then( (result) => { resolve(result); }, (err) => { this.prepareUploadError(fileRecord, err); reject(err); } ); } }); } public updateUpload( url: string, headers: object, fileRecord: FileRecord, uploadData: any, configureFn?: ConfigureFn ) { return new Promise((resolve, reject) => { if (fileRecord.xhr) { // probably updated while being uploaded. fileRecord.xhrQueue = () => { this.updateUpload(url, headers, fileRecord, uploadData); }; return resolve(undefined); } if (uploadData === undefined) { uploadData = fileRecord.upload.data || {}; uploadData.old_filename = fileRecord.oldFileName; uploadData.filename = fileRecord.name(); } if (uploadData) { this.doUpdateUpload(url, headers, uploadData, (xhr) => { if (typeof configureFn === 'function') { configureFn(xhr); } }).then( (response) => { fileRecord.upload.data = response.data; fileRecord.upload.error = false; resolve(response); }, (err) => { this.prepareUploadError(fileRecord, err); reject(err); } ); } }); } public doTusUpload( tus: any, url: string, fileRecord: FileRecord, headers: object, progressCallback: ProgressFn, tusOptionsFn?: TusOptionsFn, uploadWithCredentials?: boolean ) { const tusOptions: TusOptions = tusOptionsFn ? tusOptionsFn(fileRecord) : {}; return new Promise((resolve, reject) => { if (!tus) { return reject(new Error('tus required. Please install tus-js-client')); } // https://github.com/tus/tus-js-client // Create a new tus upload const file = fileRecord.file; const upload = new tus.Upload(file, { endpoint: url, headers, retryDelays: tusOptions.retryDelays ? tusOptions.retryDelays : [0, 3000, 5000, 10000, 20000], metadata: tusOptions.metadata ? tusOptions.metadata : { filename: file.name, filetype: file.type, }, onError(error: any) { reject(error); // console.log("Failed because: " + error) }, onProgress(bytesUploaded: number, bytesTotal: number) { const event = { loaded: bytesUploaded, total: bytesTotal } as ProgressEvent; progressCallback(event); }, onSuccess() { resolve(upload); }, onBeforeRequest: function (req: any) { const xhr: XMLHttpRequest = req.getUnderlyingObject(); xhr.withCredentials = uploadWithCredentials === true; }, }); fileRecord.tusUpload = upload; // Start the upload upload.start(); }); } public tusUpload( tus: any, url: string, headers: object, fileRecords: FileRecord[], progressFn?: (progress: number) => void, tusOptionsFn?: TusOptionsFn, uploadWithCredentials?: boolean ) { let updateOverallProgress = () => { /* no op */ }; if (progressFn) { updateOverallProgress = () => { let prgTotal = 0; for (const fileRecord of fileRecords) { prgTotal += fileRecord.progress() as number; } progressFn(prgTotal / fileRecords.length); }; } const promises = []; for (const fileRecord of fileRecords) { const promise = this.doTusUpload( tus, url, fileRecord, headers, (progressEvent: ProgressEvent) => { const percentCompleted = (progressEvent.loaded * 100) / progressEvent.total; // do not complete until promise resolved fileRecord.progress(percentCompleted >= 100 ? 99.9999 : percentCompleted); updateOverallProgress(); }, tusOptionsFn, uploadWithCredentials ); promise.then( (response) => { // delete fileRecord.tusUpload; fileRecord.progress(100); }, (err) => { this.prepareUploadError(fileRecord, err); } ); promises.push(promise); } return Promise.all(promises); } public tusDeleteUpload(tus: any, url: string, headers: object, fileRecord: FileRecord) { return new Promise((resolve, reject) => { if (!tus) { return reject('tus required'); } if (!fileRecord.tusUpload) { return resolve(undefined); } // const shouldTerminate = true; const abort = (shouldTerminate: boolean) => { return new Promise((res, rej) => { fileRecord.tusUpload.abort(shouldTerminate, (err: any) => { if (err) { this.prepareUploadError(fileRecord, err); rej(err); return; } res(undefined); }); }); }; abort(false).then(() => { window.setTimeout(() => { abort(true).then(resolve, reject); }, 1000); }); }); } } export default new UploadHelper();