From cd8d13b9e6e7bbf0e17f31c60021804ac4772939 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Fri, 7 Oct 2016 16:27:54 +0200 Subject: [PATCH] Enable chunking for bigger files in authenticated web upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds chunked uploads in the Web UI (for authenticated users, but not for public uploads). To do that the server endpoint used by the uploader is changed from WebDAV v1 to WebDAV v2. The chunking itself is done automatically by the jQuery-File-Upload plugin when the "maxChunkSize" parameter is set; in "fileuploadchunksend" the request is adjusted to adapt the behaviour of the plugin to the one expected by "uploads/" in WebDAV v2. The chunk size to be used by the Web UI can be set in the "max_chunk_size" parameter of the Files app configuration. By default it is set to 10MiB. Signed-off-by: Daniel Calviño Sánchez --- apps/files/appinfo/app.php | 2 + apps/files/js/app.js | 3 +- apps/files/js/file-upload.js | 71 ++++++++++++++++++++++++++++++------ apps/files/js/filelist.js | 3 +- apps/files/lib/App.php | 10 +++++ core/js/files/client.js | 18 +++++++-- 6 files changed, 90 insertions(+), 17 deletions(-) diff --git a/apps/files/appinfo/app.php b/apps/files/appinfo/app.php index 63acda3a70674..cd81aa3af3107 100644 --- a/apps/files/appinfo/app.php +++ b/apps/files/appinfo/app.php @@ -57,3 +57,5 @@ 'name' => $l->t('Recent'), ]; }); + +\OCP\Util::connectHook('\OCP\Config', 'js', '\OCA\Files\App', 'extendJsConfig'); diff --git a/apps/files/js/app.js b/apps/files/js/app.js index 47011d23f4424..6e4e8c1b1361b 100644 --- a/apps/files/js/app.js +++ b/apps/files/js/app.js @@ -93,7 +93,8 @@ direction: $('#defaultFileSortingDirection').val() }, config: this._filesConfig, - enableUpload: true + enableUpload: true, + maxChunkSize: OC.appConfig.files && OC.appConfig.files.max_chunk_size } ); this.files.initialize(); diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index b86b42bdb9a44..5dc18907c7b51 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -220,8 +220,8 @@ OC.FileUpload.prototype = { this.data.headers['If-None-Match'] = '*'; } - var userName = this.uploader.filesClient.getUserName(); - var password = this.uploader.filesClient.getPassword(); + var userName = this.uploader.davClient.getUserName(); + var password = this.uploader.davClient.getPassword(); if (userName) { // copy username/password from DAV client this.data.headers['Authorization'] = @@ -234,7 +234,7 @@ OC.FileUpload.prototype = { && this.getFile().size > this.uploader.fileUploadParam.maxChunkSize ) { data.isChunked = true; - chunkFolderPromise = this.uploader.filesClient.createDirectory( + 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 @@ -260,9 +260,18 @@ OC.FileUpload.prototype = { } var uid = OC.getCurrentUser().uid; - return this.uploader.filesClient.move( + return this.uploader.davClient.move( 'uploads/' + encodeURIComponent(uid) + '/' + encodeURIComponent(this.getId()) + '/.file', - 'files/' + encodeURIComponent(uid) + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()) + 'files/' + encodeURIComponent(uid) + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()), + true, + {'X-OC-Mtime': this.getFile().lastModified / 1000} + ); + }, + + _deleteChunkFolder: function() { + // delete transfer directory for this upload + this.uploader.davClient.remove( + 'uploads/' + encodeURIComponent(OC.getCurrentUser().uid) + '/' + encodeURIComponent(this.getId()) ); }, @@ -271,12 +280,20 @@ OC.FileUpload.prototype = { */ abort: function() { if (this.data.isChunked) { - // delete transfer directory for this upload - this.uploader.filesClient.remove( - 'uploads/' + encodeURIComponent(OC.getCurrentUser().uid) + '/' + encodeURIComponent(this.getId()) - ); + this._deleteChunkFolder(); } this.data.abort(); + this.deleteUpload(); + }, + + /** + * Fail the upload + */ + fail: function() { + this.deleteUpload(); + if (this.data.isChunked) { + this._deleteChunkFolder(); + } }, /** @@ -375,6 +392,13 @@ OC.Uploader.prototype = _.extend({ */ filesClient: null, + /** + * Webdav client pointing at the root "dav" endpoint + * + * @type OC.Files.Client + */ + davClient: null, + /** * Function that will allow us to know if Ajax uploads are supported * @link https://github.com/New-Bamboo/example-ajax-upload/blob/master/public/index.html @@ -721,6 +745,13 @@ OC.Uploader.prototype = _.extend({ this.fileList = options.fileList; this.filesClient = options.filesClient || OC.Files.getClient(); + this.davClient = new OC.Files.Client({ + host: this.filesClient.getHost(), + root: OC.linkToRemoteBase('dav'), + useHTTPS: OC.getProtocol() === 'https', + userName: this.filesClient.getUserName(), + password: this.filesClient.getPassword() + }); $uploadEl = $($uploadEl); this.$uploadEl = $uploadEl; @@ -920,7 +951,7 @@ OC.Uploader.prototype = _.extend({ } if (upload) { - upload.deleteUpload(); + upload.fail(); } }, /** @@ -951,6 +982,10 @@ OC.Uploader.prototype = _.extend({ } }; + if (options.maxChunkSize) { + this.fileUploadParam.maxChunkSize = options.maxChunkSize; + } + // initialize jquery fileupload (blueimp) var fileupload = this.$uploadEl.fileupload(this.fileUploadParam); @@ -1041,7 +1076,6 @@ OC.Uploader.prototype = _.extend({ self.log('progress handle fileuploadstop', e, data); self.clear(); - self._hideProgressBar(); self.trigger('stop', e, data); }); fileupload.on('fileuploadfail', function(e, data) { @@ -1096,7 +1130,7 @@ OC.Uploader.prototype = _.extend({ // modify the request to adjust it to our own chunking var upload = self.getUpload(data); var range = data.contentRange.split(' ')[1]; - var chunkId = range.split('/')[0]; + var chunkId = range.split('/')[0].split('-')[0]; data.url = OC.getRootPath() + '/remote.php/dav/uploads' + '/' + encodeURIComponent(OC.getCurrentUser().uid) + @@ -1108,7 +1142,20 @@ OC.Uploader.prototype = _.extend({ fileupload.on('fileuploaddone', function(e, data) { var upload = self.getUpload(data); upload.done().then(function() { + self._hideProgressBar(); self.trigger('done', e, upload); + }).fail(function(status) { + self._hideProgressBar(); + if (status === 507) { + // not enough space + OC.Notification.show(t('files', 'Not enough free space'), {type: 'error'}); + self.cancelUploads(); + } else if (status === 409) { + OC.Notification.show(t('files', 'Target folder does not exist any more'), {type: 'error'}); + } else { + OC.Notification.show(t('files', 'Error when assembling chunks, status code {status}', {status: status}), {type: 'error'}); + } + self.trigger('fail', e, data); }); }); fileupload.on('fileuploaddrop', function(e, data) { diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 4790afcf4d0a8..b8fadc6c435bc 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -357,7 +357,8 @@ this._uploader = new OC.Uploader($uploadEl, { fileList: this, filesClient: this.filesClient, - dropZone: $('#content') + dropZone: $('#content'), + maxChunkSize: options.maxChunkSize }); this.setupUploadEvents(this._uploader); diff --git a/apps/files/lib/App.php b/apps/files/lib/App.php index 34d3ab4384c47..d16291c6fcaa5 100644 --- a/apps/files/lib/App.php +++ b/apps/files/lib/App.php @@ -53,4 +53,14 @@ public static function getNavigationManager() { return self::$navigationManager; } + public static function extendJsConfig($settings) { + $appConfig = json_decode($settings['array']['oc_appconfig'], true); + + $maxChunkSize = (int)(\OC::$server->getConfig()->getAppValue('files', 'max_chunk_size', (10 * 1024 * 1024))); + $appConfig['files'] = [ + 'max_chunk_size' => $maxChunkSize + ]; + + $settings['array']['oc_appconfig'] = json_encode($appConfig); + } } diff --git a/core/js/files/client.js b/core/js/files/client.js index dc9f6ade64178..e810381342a6e 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -37,6 +37,7 @@ } url += options.host + this._root; + this._host = options.host; this._defaultHeaders = options.defaultHeaders || { 'X-Requested-With': 'XMLHttpRequest', 'requesttoken': OC.requestToken @@ -698,10 +699,11 @@ * @param {String} destinationPath destination path * @param {boolean} [allowOverwrite=false] true to allow overwriting, * false otherwise + * @param {Object} [headers=null] additional headers * * @return {Promise} promise */ - move: function(path, destinationPath, allowOverwrite) { + move: function(path, destinationPath, allowOverwrite, headers) { if (!path) { throw 'Missing argument "path"'; } @@ -712,9 +714,9 @@ var self = this; var deferred = $.Deferred(); var promise = deferred.promise(); - var headers = { + headers = _.extend({}, headers, { 'Destination' : this._buildUrl(destinationPath) - }; + }); if (!allowOverwrite) { headers.Overwrite = 'F'; @@ -828,6 +830,16 @@ */ getBaseUrl: function() { return this._client.baseUrl; + }, + + /** + * Returns the host + * + * @since 13.0.0 + * @return {String} base URL + */ + getHost: function() { + return this._host; } };