@elasticms/file-uploader
Version:
upload a file chunk by chunk
299 lines (249 loc) • 9.76 kB
JavaScript
'use strict';
import HashFile from '@elasticms/hash-file';
/**
* Upload chunk by chunk a file after having check that is not already on the server (the file is identified by an hash, computed client side)
*/
export default class FileUploader {
constructor(params) {
// Define all file status values and the size of slice to compute the sha1 and the size of the uploaded chunk
this.statics = {
CHUNK_SIZE: 1024 * 1024 * 8 /* bytes */,
UPLOADED: 1,
ERROR: 2,
UPLOADING: 3,
PAUSE: 4,
LOADING: 5,
UPLOADERROR: 6,
ABORTED: 7,
};
if(params.file) {
this.file = params.file;
this.size = params.file.size;
this.type = params.file.type;
this.name = params.file.name;
this.lastModified = params.file.lastModified;
this.initUrl = params.initUrl;
this.algo = params.algo;
this.onError = params.onError;
this.onHashAvailable = params.onHashAvailable;
this.onUploaded = params.onUploaded;
this.onProgress = params.onProgress;
this.errorDescription = 'N/A';
this.hash = null;
this.chunkUrl = null;
this.uploaded = 0;
if (!this.type || this.type === '') {
this.type = 'application/octet-stream';
}
new HashFile(this.file, this.callbackHashFinal, this.callbackHashProgress, this, this.algo === undefined?'sha1':this.algo);
}
}
/**
* Callback function called when the file's hash has been computed
* @param hash
*/
callbackHashFinal(hash) {
if(typeof this.onHashAvailable === "function") {
this.onHashAvailable(hash, this.type, this.name);
}
else {
console.log('Hash: '+hash);
}
this.hash = hash;
this.status = this.statics.UPLOADING;
this.initUpload();
}
/**
* Callback function called each time that a file's chunk has hashed
* @param percentage
*/
callbackHashProgress(percentage) {
if(typeof this.onProgress === "function") {
this.onProgress('Computing hash', 0, percentage+'%');
}
else {
console.log('Hash treated at '+percentage+'%');
}
}
/**
* Init the upload. The file description are send to the server. If the file is already know by the server
* the object (this) is updated (bytes already upload, file ready, ...)
*/
initUpload() {
const self = this;
if (this.status === this.statics.UPLOADING
|| this.status === this.statics.PAUSE) {
const xmlHttp = new XMLHttpRequest();
this.currentXMLHttpRequest = xmlHttp;
xmlHttp.onload = function () {
if (this.status === 200) {
const fileInfo = JSON.parse(this.responseText);
if (fileInfo && fileInfo.uploaded !== undefined) {
self.uploaded = fileInfo.uploaded;
self.chunkUrl = fileInfo.chunkUrl;
if (self.uploaded < self.size) {
self.startUpload(self);
}
else if (self.uploaded === self.size) {
self.finalizeUpload(fileInfo);
}
else {
self.setUploadError('Number bytes of already uploaded is abnormal ' + HashFile.humanFileSize(self.uploaded) + '/' + HashFile.humanFileSize(self.size));
}
}
else {
if (fileInfo.error && fileInfo.error[0]) {
self.setUploadError(fileInfo.error[0], 200);
}
else {
self.setUploadError('Upload init has failed', 200);
}
}
}
else {
self.setUploadError(this.statusText, this.status);
}
};
xmlHttp.onerror = function () {
self.setUploadError(this.statusText, this.status);
};
xmlHttp.open("POST", this.initUrl, true);
const params = JSON.stringify({name: this.name, type: this.type, size: this.size, hash: this.hash});
xmlHttp.setRequestHeader("Content-type", "application/json; charset=utf-8");
xmlHttp.send(params);
}
}
abort() {
this.status = this.statics.ABORTED;
this.errorDescription = 'Upload aborted';
if (undefined !== this.currentXMLHttpRequest && typeof this.currentXMLHttpRequest.abort === "function") {
this.currentXMLHttpRequest.abort();
}
}
/**
* An error has occurred to the file. The file status is set to ERROR
*
* @param description, description of the error
* @param errorCode; http status code returned
*/
setUploadError(description, errorCode){
this.status = this.statics.ERROR;
this.errorDescription = description;
if (this.onError) {
this.onError(description, errorCode);
}
else {
console.log(description + '/' + errorCode);
}
}
/**
* Start or resume the upload
*/
startUpload() {
this.status = this.statics.UPLOADING;
this.uploadNextChunk();
}
/**
* Finalize the upload
* @param response
*/
finalizeUpload(response) {
if (response.sha1 !== this.hash) {
console.log('hash mismatch');
this.hash = response.hash;
}
this.status = this.statics.UPLOADED;
if(typeof this.onUploaded === "function") {
this.onUploaded(response.url, response.previewUrl);
}
else {
console.log('Upload done: '+this.hash);
}
}
/**
* Upload the next chunk, only if the file status is UPLOADING
*/
uploadNextChunk() {
if (this.status === this.statics.UPLOADING) {
this.timeStamp = (new Date()).getTime();
//get the blob corresponding to the current chunk
// console.log('load from '+this.uploaded+' to '+Math.min(this.statics.CHUNK_SIZE, this.size-this.uploaded));
const blob = this.file.slice(this.uploaded, this.uploaded + Math.min(this.statics.CHUNK_SIZE, this.size - this.uploaded));
const xhr = new XMLHttpRequest();
this.currentXMLHttpRequest = xhr;
const self = this;
//add listener to the XHR object in case of success or fail
xhr.onerror = function () {
self.setUploadError(this.statusText, this.status);
};
xhr.onload = function (evt) {
self.onChunkUploadSuccess(evt)
};
//init the XHR request
xhr.open("POST", this.chunkUrl, true);
//send form with the XHR
xhr.send(blob);
}
else {
this.setUploadError('inconsistent status', 400);
}
}
/**
* callback function, called when the chunk upload has responded
*
* @param event
*/
onChunkUploadSuccess(event) {
if (event.target.status === 200) {
this.uploaded += Math.min(this.statics.CHUNK_SIZE, this.size - this.uploaded);
if (this.uploaded === this.size) {
const response = JSON.parse(event.target.responseText);
if (undefined === response.error || 0 === response.error.length) {
this.finalizeUpload(response);
}
else {
this.setUploadError(response.error[0], 200);
}
}
else {
if(typeof this.onProgress === "function") {
this.onProgress('Uploading', (this.uploaded / this.size), FileUploader.msToHumanDuration(((this.size - this.uploaded) / this.statics.CHUNK_SIZE) * ((new Date()).getTime() - this.timeStamp)));
}
else {
console.log('Uploading '+((this.uploaded / this.size) * 100).toFixed(0));
}
this.uploadNextChunk();
}
}
else {
this.setUploadError(event.target.responseText, event.target.status);
}
};
/**
* Convert ms to human readable format
*
* @param milliseconds to convert
* @param precision (number of decimals)
* @return string
*/
static msToHumanDuration(milliseconds, precision) {
const seconds = 1000;
const minutes = seconds * 60;
const hours = minutes * 60;
const days = hours * 24;
if (!precision) precision = 0;
if ((milliseconds >= 0) && (milliseconds < seconds)) {
return '> 1 s';
} else if ((milliseconds >= seconds) && (milliseconds < minutes)) {
return (milliseconds / seconds).toFixed(precision) + ' s';
} else if ((milliseconds >= minutes) && (milliseconds < hours)) {
return (milliseconds / minutes).toFixed(precision) + ' min';
} else if ((milliseconds >= hours) && (milliseconds < days)) {
return (milliseconds / hours).toFixed(precision) + ' h';
} else if (milliseconds >= days) {
return (milliseconds / days).toFixed(precision) + ' d';
} else {
return milliseconds + ' ms';
}
};
}