Add support for chunked upload

Hacked around Blueimp's jquery.fileupload to make it work with our new
chunking API.

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
This commit is contained in:
Vincent Petry 2016-07-15 16:03:02 +02:00 committed by Roeland Jago Douma
parent c68e273664
commit 786e858d23
No known key found for this signature in database
GPG Key ID: 1E152838F164D13B
2 changed files with 116 additions and 45 deletions

View File

@ -18,7 +18,7 @@
* - TODO music upload button * - TODO music upload button
*/ */
/* global jQuery, humanFileSize */ /* global jQuery, humanFileSize, md5 */
/** /**
* File upload object * File upload object
@ -34,12 +34,21 @@
OC.FileUpload = function(uploader, data) { OC.FileUpload = function(uploader, data) {
this.uploader = uploader; this.uploader = uploader;
this.data = data; this.data = data;
var path = OC.joinPaths(this.uploader.fileList.getCurrentDirectory(), this.getFile().name);
this.id = 'web-file-upload-' + md5(path) + '-' + (new Date()).getTime();
}; };
OC.FileUpload.CONFLICT_MODE_DETECT = 0; OC.FileUpload.CONFLICT_MODE_DETECT = 0;
OC.FileUpload.CONFLICT_MODE_OVERWRITE = 1; OC.FileUpload.CONFLICT_MODE_OVERWRITE = 1;
OC.FileUpload.CONFLICT_MODE_AUTORENAME = 2; OC.FileUpload.CONFLICT_MODE_AUTORENAME = 2;
OC.FileUpload.prototype = { OC.FileUpload.prototype = {
/**
* Unique upload id
*
* @type string
*/
id: null,
/** /**
* Upload element * Upload element
* *
@ -66,6 +75,15 @@ OC.FileUpload.prototype = {
*/ */
_newName: null, _newName: null,
/**
* Returns the unique upload id
*
* @return string
*/
getId: function() {
return this.id;
},
/** /**
* Returns the file to be uploaded * Returns the file to be uploaded
* *
@ -143,6 +161,7 @@ OC.FileUpload.prototype = {
* Submit the upload * Submit the upload
*/ */
submit: function() { submit: function() {
var self = this;
var data = this.data; var data = this.data;
var file = this.getFile(); var file = this.getFile();
@ -192,19 +211,54 @@ OC.FileUpload.prototype = {
} }
} }
var chunkFolderPromise;
if ($.support.blobSlice
&& this.uploader.fileUploadParam.maxChunkSize
&& this.getFile().size > this.uploader.fileUploadParam.maxChunkSize
) {
data.isChunked = true;
chunkFolderPromise = this.uploader.davClient.createDirectory(
'uploads/' + encodeURIComponent(OC.getCurrentUser().uid) + '/' + encodeURIComponent(this.getId())
);
// TODO: if fails, it means same id already existed, need to retry
} else {
chunkFolderPromise = $.Deferred().resolve().promise();
}
// wait for creation of the required directory before uploading // wait for creation of the required directory before uploading
folderPromise.then(function() { $.when(folderPromise, chunkFolderPromise).then(function() {
data.submit(); data.submit();
}, function() { }, function() {
data.abort(); self.abort();
}); });
}, },
/**
* Process end of transfer
*/
done: function() {
if (!this.data.isChunked) {
return $.Deferred().resolve().promise();
}
var uid = OC.getCurrentUser().uid;
return this.uploader.davClient.move(
'uploads/' + encodeURIComponent(uid) + '/' + encodeURIComponent(this.getId()) + '/.file',
'files/' + encodeURIComponent(uid) + '/' + OC.joinPaths(this.getFullPath(), this.getFileName())
);
},
/** /**
* Abort the upload * Abort the upload
*/ */
abort: function() { abort: function() {
if (this.data.isChunked) {
// delete transfer directory for this upload
this.uploader.davClient.remove(
'uploads/' + encodeURIComponent(OC.getCurrentUser().uid) + '/' + encodeURIComponent(this.getId())
);
}
this.data.abort(); this.data.abort();
}, },
@ -280,7 +334,7 @@ OC.Uploader = function() {
this.init.apply(this, arguments); this.init.apply(this, arguments);
}; };
OC.Uploader.prototype = { OC.Uploader.prototype = _.extend({
/** /**
* @type Array<OC.FileUpload> * @type Array<OC.FileUpload>
*/ */
@ -384,7 +438,7 @@ OC.Uploader.prototype = {
self.filesClient.createDirectory(fullPath).always(function(status) { self.filesClient.createDirectory(fullPath).always(function(status) {
// 405 is expected if the folder already exists // 405 is expected if the folder already exists
if ((status >= 200 && status < 300) || status === 405) { if ((status >= 200 && status < 300) || status === 405) {
self.$uploadEl.trigger($.Event('fileuploadcreatedfolder'), fullPath); self.trigger('createdfolder', fullPath);
deferred.resolve(); deferred.resolve();
return; return;
} }
@ -407,8 +461,8 @@ OC.Uploader.prototype = {
submitUploads: function(uploads) { submitUploads: function(uploads) {
var self = this; var self = this;
_.each(uploads, function(upload) { _.each(uploads, function(upload) {
upload.submit();
self._uploads[upload.data.uploadId] = upload; self._uploads[upload.data.uploadId] = upload;
upload.submit();
}); });
}, },
@ -611,16 +665,6 @@ OC.Uploader.prototype = {
this.$uploadEl.trigger(new $.Event('resized')); this.$uploadEl.trigger(new $.Event('resized'));
}, },
on: function() {
// forward events to upload element
this.$uploadEl.on.apply(this.$uploadEl, arguments);
},
off: function() {
// forward events to upload element
this.$uploadEl.off.apply(this.$uploadEl, arguments);
},
/** /**
* Returns whether the given file is known to be a received shared file * Returns whether the given file is known to be a received shared file
* *
@ -655,6 +699,12 @@ OC.Uploader.prototype = {
this.fileList = options.fileList; this.fileList = options.fileList;
this.filesClient = options.filesClient || OC.Files.getClient(); this.filesClient = options.filesClient || OC.Files.getClient();
this.davClient = new OC.Files.Client({
host: OC.getHost(),
port: OC.getPort(),
root: OC.getRootPath() + '/remote.php/dav/',
useHTTPS: OC.getProtocol() === 'https'
});
$uploadEl = $($uploadEl); $uploadEl = $($uploadEl);
this.$uploadEl = $uploadEl; this.$uploadEl = $uploadEl;
@ -669,6 +719,7 @@ OC.Uploader.prototype = {
dropZone: options.dropZone, // restrict dropZone to content div dropZone: options.dropZone, // restrict dropZone to content div
autoUpload: false, autoUpload: false,
sequentialUploads: true, sequentialUploads: true,
maxChunkSize: 10000000,
//singleFileUploads is on by default, so the data.files array will always have length 1 //singleFileUploads is on by default, so the data.files array will always have length 1
/** /**
* on first add of every selection * on first add of every selection
@ -692,7 +743,7 @@ OC.Uploader.prototype = {
var upload = new OC.FileUpload(self, data); var upload = new OC.FileUpload(self, data);
// can't link directly due to jQuery not liking cyclic deps on its ajax object // can't link directly due to jQuery not liking cyclic deps on its ajax object
data.uploadId = _.uniqueId('file-upload'); data.uploadId = upload.getId();
// we need to collect all data upload objects before // we need to collect all data upload objects before
// starting the upload so we can check their existence // starting the upload so we can check their existence
@ -842,7 +893,10 @@ OC.Uploader.prototype = {
}, },
fail: function(e, data) { fail: function(e, data) {
var upload = self.getUpload(data); var upload = self.getUpload(data);
var status = upload.getResponseStatus(); var status = null;
if (upload) {
status = upload.getResponseStatus();
}
self.log('fail', e, upload); self.log('fail', e, upload);
if (data.textStatus === 'abort') { if (data.textStatus === 'abort') {
@ -914,10 +968,7 @@ OC.Uploader.prototype = {
// add progress handlers // add progress handlers
fileupload.on('fileuploadadd', function(e, data) { fileupload.on('fileuploadadd', function(e, data) {
self.log('progress handle fileuploadadd', e, data); self.log('progress handle fileuploadadd', e, data);
//show cancel button self.trigger('add', e, data);
//if (data.dataType !== 'iframe') { //FIXME when is iframe used? only for ie?
// $('#uploadprogresswrapper .stop').show();
//}
}); });
// add progress handlers // add progress handlers
fileupload.on('fileuploadstart', function(e, data) { fileupload.on('fileuploadstart', function(e, data) {
@ -933,10 +984,12 @@ OC.Uploader.prototype = {
+ '</span></em>'); + '</span></em>');
$('#uploadprogressbar').tipsy({gravity:'n', fade:true, live:true}); $('#uploadprogressbar').tipsy({gravity:'n', fade:true, live:true});
self._showProgressBar(); self._showProgressBar();
self.trigger('start', e, data);
}); });
fileupload.on('fileuploadprogress', function(e, data) { fileupload.on('fileuploadprogress', function(e, data) {
self.log('progress handle fileuploadprogress', e, data); self.log('progress handle fileuploadprogress', e, data);
//TODO progressbar in row //TODO progressbar in row
self.trigger('progress', e, data);
}); });
fileupload.on('fileuploadprogressall', function(e, data) { fileupload.on('fileuploadprogressall', function(e, data) {
self.log('progress handle fileuploadprogressall', e, data); self.log('progress handle fileuploadprogressall', e, data);
@ -999,6 +1052,7 @@ OC.Uploader.prototype = {
}) })
); );
$('#uploadprogressbar').progressbar('value', progress); $('#uploadprogressbar').progressbar('value', progress);
self.trigger('progressall', e, data);
}); });
fileupload.on('fileuploadstop', function(e, data) { fileupload.on('fileuploadstop', function(e, data) {
self.log('progress handle fileuploadstop', e, data); self.log('progress handle fileuploadstop', e, data);
@ -1012,6 +1066,7 @@ OC.Uploader.prototype = {
if (data.errorThrown === 'abort') { if (data.errorThrown === 'abort') {
self._hideProgressBar(); self._hideProgressBar();
} }
self.trigger('fail', e, data);
}); });
var disableDropState = function() { var disableDropState = function() {
$('#app-content').removeClass('file-drag'); $('#app-content').removeClass('file-drag');
@ -1046,22 +1101,36 @@ OC.Uploader.prototype = {
filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept'); filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept');
} }
}); });
fileupload.on('fileuploaddragleave fileuploaddrop', disableDropState); fileupload.on('fileuploaddragleave fileuploaddrop', function (){
} else { $('#app-content').removeClass('file-drag');
// for all browsers that don't support the progress bar $('.dropping-to-dir').removeClass('dropping-to-dir');
// IE 8 & 9 $('.dir-drop').removeClass('dir-drop');
$('.icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
// show a spinner
fileupload.on('fileuploadstart', function() {
$('#upload').addClass('icon-loading');
$('#upload .icon-upload').hide();
}); });
// hide a spinner fileupload.on('fileuploadchunksend', function(e, data) {
fileupload.on('fileuploadstop fileuploadfail', function() { // modify the request to adjust it to our own chunking
$('#upload').removeClass('icon-loading'); var upload = self.getUpload(data);
$('#upload .icon-upload').show(); var range = data.contentRange.split(' ')[1];
var chunkId = range.split('/')[0];
data.url = OC.getRootPath() +
'/remote.php/dav/uploads' +
'/' + encodeURIComponent(OC.getCurrentUser().uid) +
'/' + encodeURIComponent(upload.getId()) +
'/' + encodeURIComponent(chunkId);
delete data.contentRange;
delete data.headers['Content-Range'];
}); });
fileupload.on('fileuploaddone', function(e, data) {
var upload = self.getUpload(data);
upload.done().then(function() {
self.trigger('done', e, upload);
});
});
fileupload.on('fileuploaddrop', function(e, data) {
self.trigger('drop', e, data);
});
} }
} }
@ -1079,6 +1148,6 @@ OC.Uploader.prototype = {
return this.fileUploadParam; return this.fileUploadParam;
} }
}; }, OC.Backbone.Events);

View File

@ -2661,16 +2661,18 @@
/** /**
* Setup file upload events related to the file-upload plugin * Setup file upload events related to the file-upload plugin
*
* @param {OC.Uploader} uploader
*/ */
setupUploadEvents: function($uploadEl) { setupUploadEvents: function(uploader) {
var self = this; var self = this;
self._uploads = {}; self._uploads = {};
// detect the progress bar resize // detect the progress bar resize
$uploadEl.on('resized', this._onResize); uploader.on('resized', this._onResize);
$uploadEl.on('fileuploaddrop', function(e, data) { uploader.on('drop', function(e, data) {
self._uploader.log('filelist handle fileuploaddrop', e, data); self._uploader.log('filelist handle fileuploaddrop', e, data);
if (self.$el.hasClass('hidden')) { if (self.$el.hasClass('hidden')) {
@ -2734,7 +2736,7 @@
} }
} }
}); });
$uploadEl.on('fileuploadadd', function(e, data) { uploader.on('add', function(e, data) {
self._uploader.log('filelist handle fileuploadadd', e, data); self._uploader.log('filelist handle fileuploadadd', e, data);
// add ui visualization to existing folder // add ui visualization to existing folder
@ -2766,16 +2768,16 @@
* when file upload done successfully add row to filelist * when file upload done successfully add row to filelist
* update counter when uploading to sub folder * update counter when uploading to sub folder
*/ */
$uploadEl.on('fileuploaddone', function(e, data) { uploader.on('done', function(e, upload) {
self._uploader.log('filelist handle fileuploaddone', e, data); self._uploader.log('filelist handle fileuploaddone', e, data);
var data = upload.data;
var status = data.jqXHR.status; var status = data.jqXHR.status;
if (status < 200 || status >= 300) { if (status < 200 || status >= 300) {
// error was handled in OC.Uploads already // error was handled in OC.Uploads already
return; return;
} }
var upload = self._uploader.getUpload(data);
var fileName = upload.getFileName(); var fileName = upload.getFileName();
var fetchInfoPromise = self.addAndFetchFileInfo(fileName, upload.getFullPath()); var fetchInfoPromise = self.addAndFetchFileInfo(fileName, upload.getFullPath());
if (!self._uploads) { if (!self._uploads) {
@ -2785,10 +2787,10 @@
self._uploads[fileName] = fetchInfoPromise; self._uploads[fileName] = fetchInfoPromise;
} }
}); });
$uploadEl.on('fileuploadcreatedfolder', function(e, fullPath) { uploader.on('createdfolder', function(e, fullPath) {
self.addAndFetchFileInfo(OC.basename(fullPath), OC.dirname(fullPath)); self.addAndFetchFileInfo(OC.basename(fullPath), OC.dirname(fullPath));
}); });
$uploadEl.on('fileuploadstop', function() { uploader.on('stop', function() {
self._uploader.log('filelist handle fileuploadstop'); self._uploader.log('filelist handle fileuploadstop');
// prepare list of uploaded file names in the current directory // prepare list of uploaded file names in the current directory
@ -2804,7 +2806,7 @@
}); });
self.updateStorageStatistics(); self.updateStorageStatistics();
}); });
$uploadEl.on('fileuploadfail', function(e, data) { uploader.on('fail', function(e, data) {
self._uploader.log('filelist handle fileuploadfail', e, data); self._uploader.log('filelist handle fileuploadfail', e, data);
self._uploads = []; self._uploads = [];