diff options
author | Lukas Reschke <lukas@statuscode.ch> | 2016-10-25 10:31:03 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-10-25 10:31:03 +0200 |
commit | 79706e0ddc6ab970d5709e89b8d0caec4d34662b (patch) | |
tree | 168f9bc806e7eed287bce63e7f6d277eb5adb956 /apps/files/js | |
parent | 5926da3dd6535e0eea7fe7871d2347f8b33bb337 (diff) | |
parent | c8a13f644ebbc5840d0e632cf86e5ae46856f7f0 (diff) | |
download | nextcloud-server-79706e0ddc6ab970d5709e89b8d0caec4d34662b.tar.gz nextcloud-server-79706e0ddc6ab970d5709e89b8d0caec4d34662b.zip |
Merge pull request #1283 from nextcloud/us_files-ui-webdav-upload
Use Webdav PUT for uploads
Diffstat (limited to 'apps/files/js')
-rw-r--r-- | apps/files/js/app.js | 1 | ||||
-rw-r--r-- | apps/files/js/file-upload.js | 901 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 338 | ||||
-rw-r--r-- | apps/files/js/files.js | 11 |
4 files changed, 831 insertions, 420 deletions
diff --git a/apps/files/js/app.js b/apps/files/js/app.js index fbfa510e07e..17e92de90dd 100644 --- a/apps/files/js/app.js +++ b/apps/files/js/app.js @@ -93,6 +93,7 @@ direction: $('#defaultFileSortingDirection').val() }, config: this._filesConfig, + enableUpload: true } ); this.files.initialize(); diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index 56ea384c9e0..30784528700 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -18,83 +18,518 @@ * - TODO music upload button */ -/* global jQuery, oc_requesttoken, humanFileSize, FileList */ +/* global jQuery, humanFileSize, md5 */ /** - * 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 - * also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata + * File upload object + * + * @class OC.FileUpload + * @classdesc + * + * Represents a file upload + * + * @param {OC.Uploader} uploader uploader + * @param {Object} data blueimp data */ -function supportAjaxUploadWithProgress() { - return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData(); - - // Is the File API supported? - function supportFileAPI() { - var fi = document.createElement('INPUT'); - fi.type = 'file'; - return 'files' in fi; +OC.FileUpload = function(uploader, data) { + this.uploader = uploader; + this.data = data; + var path = ''; + if (this.uploader.fileList) { + path = OC.joinPaths(this.uploader.fileList.getCurrentDirectory(), this.getFile().name); + } else { + path = this.getFile().name; } + this.id = 'web-file-upload-' + md5(path) + '-' + (new Date()).getTime(); +}; +OC.FileUpload.CONFLICT_MODE_DETECT = 0; +OC.FileUpload.CONFLICT_MODE_OVERWRITE = 1; +OC.FileUpload.CONFLICT_MODE_AUTORENAME = 2; +OC.FileUpload.prototype = { - // Are progress events supported? - function supportAjaxUploadProgressEvents() { - var xhr = new XMLHttpRequest(); - return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload)); - } + /** + * Unique upload id + * + * @type string + */ + id: null, - // Is FormData supported? - function supportFormData() { - return !! window.FormData; - } -} + /** + * Upload element + * + * @type Object + */ + $uploadEl: null, -/** - * Add form data into the given form data - * - * @param {Array|Object} formData form data which can either be an array or an object - * @param {Object} newData key-values to add to the form data - * - * @return updated form data - */ -function addFormData(formData, newData) { - // in IE8, formData is an array instead of object - if (_.isArray(formData)) { - _.each(newData, function(value, key) { - formData.push({name: key, value: value}); + /** + * Target folder + * + * @type string + */ + _targetFolder: '', + + /** + * @type int + */ + _conflictMode: OC.FileUpload.CONFLICT_MODE_DETECT, + + /** + * New name from server after autorename + * + * @type String + */ + _newName: null, + + /** + * Returns the unique upload id + * + * @return string + */ + getId: function() { + return this.id; + }, + + /** + * Returns the file to be uploaded + * + * @return {File} file + */ + getFile: function() { + return this.data.files[0]; + }, + + /** + * Return the final filename. + * Either this is the original file name or the file name + * after an autorename. + * + * @return {String} file name + */ + getFileName: function() { + // in case of autorename + if (this._newName) { + return this._newName; + } + + if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) { + + var locationUrl = this.getResponseHeader('Content-Location'); + if (locationUrl) { + this._newName = decodeURIComponent(OC.basename(locationUrl)); + return this._newName; + } + } + + return this.getFile().name; + }, + + setTargetFolder: function(targetFolder) { + this._targetFolder = targetFolder; + }, + + getTargetFolder: function() { + return this._targetFolder; + }, + + /** + * Get full path for the target file, including relative path, + * without the file name. + * + * @return {String} full path + */ + getFullPath: function() { + return OC.joinPaths(this._targetFolder, this.getFile().relativePath || ''); + }, + + /** + * Set conflict resolution mode. + * See CONFLICT_MODE_* constants. + */ + setConflictMode: function(mode) { + this._conflictMode = mode; + }, + + /** + * Returns whether the upload is in progress + * + * @return {bool} + */ + isPending: function() { + return this.data.state() === 'pending'; + }, + + deleteUpload: function() { + delete this.data.jqXHR; + }, + + /** + * Submit the upload + */ + submit: function() { + var self = this; + var data = this.data; + var file = this.getFile(); + + // it was a folder upload, so make sure the parent directory exists alrady + var folderPromise; + if (file.relativePath) { + folderPromise = this.uploader.ensureFolderExists(this.getFullPath()); + } else { + folderPromise = $.Deferred().resolve().promise(); + } + + if (this.uploader.fileList) { + this.data.url = this.uploader.fileList.getUploadUrl(file.name, this.getFullPath()); + } + + if (!this.data.headers) { + this.data.headers = {}; + } + + // webdav without multipart + this.data.multipart = false; + this.data.type = 'PUT'; + + delete this.data.headers['If-None-Match']; + if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_DETECT) { + this.data.headers['If-None-Match'] = '*'; + } else if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) { + // POST to parent folder, with slug + this.data.type = 'POST'; + this.data.url = this.uploader.fileList.getUploadUrl('&' + file.name, this.getFullPath()); + } + + if (file.lastModified) { + // preserve timestamp + this.data.headers['X-OC-Mtime'] = file.lastModified / 1000; + } + + var userName = this.uploader.filesClient.getUserName(); + var password = this.uploader.filesClient.getPassword(); + if (userName) { + // copy username/password from DAV client + this.data.headers['Authorization'] = + 'Basic ' + btoa(userName + ':' + (password || '')); + } + + if (!this.uploader.isXHRUpload()) { + data.formData = []; + + // pass headers as parameters + data.formData.push({name: 'headers', value: JSON.stringify(this.data.headers)}); + data.formData.push({name: 'requesttoken', value: OC.requestToken}); + if (data.type === 'POST') { + // still add the method to the URL + data.url += '?_method=POST'; + } + } + + var chunkFolderPromise; + if ($.support.blobSlice + && this.uploader.fileUploadParam.maxChunkSize + && this.getFile().size > this.uploader.fileUploadParam.maxChunkSize + ) { + data.isChunked = true; + chunkFolderPromise = this.uploader.filesClient.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 + $.when(folderPromise, chunkFolderPromise).then(function() { + data.submit(); + }, function() { + self.abort(); }); - } else { - formData = _.extend(formData, newData); + + }, + + /** + * Process end of transfer + */ + done: function() { + if (!this.data.isChunked) { + return $.Deferred().resolve().promise(); + } + + var uid = OC.getCurrentUser().uid; + return this.uploader.filesClient.move( + 'uploads/' + encodeURIComponent(uid) + '/' + encodeURIComponent(this.getId()) + '/.file', + 'files/' + encodeURIComponent(uid) + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()) + ); + }, + + /** + * Abort the upload + */ + 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.data.abort(); + }, + + /** + * Returns the server response + * + * @return {Object} response + */ + getResponse: function() { + var response = this.data.response(); + if (typeof response.result !== 'string') { + //fetch response from iframe + response = $.parseJSON(response.result[0].body.innerText); + if (!response) { + // likely due to internal server error + response = {status: 500}; + } + } else { + response = response.result; + } + return response; + }, + + /** + * Returns the status code from the response + * + * @return {int} status code + */ + getResponseStatus: function() { + if (this.uploader.isXHRUpload()) { + var xhr = this.data.response().jqXHR; + if (xhr) { + return xhr.status; + } + return null; + } + return this.getResponse().status; + }, + + /** + * Returns the response header by name + * + * @param {String} headerName header name + * @return {Array|String} response header value(s) + */ + getResponseHeader: function(headerName) { + headerName = headerName.toLowerCase(); + if (this.uploader.isXHRUpload()) { + return this.data.response().jqXHR.getResponseHeader(headerName); + } + + var headers = this.getResponse().headers; + if (!headers) { + return null; + } + + var value = _.find(headers, function(value, key) { + return key.toLowerCase() === headerName; + }); + if (_.isArray(value) && value.length === 1) { + return value[0]; + } + return value; } - return formData; -} +}; /** * keeps track of uploads in progress and implements callbacks for the conflicts dialog * @namespace */ -OC.Upload = { - _uploads: [], + +OC.Uploader = function() { + this.init.apply(this, arguments); +}; + +OC.Uploader.prototype = _.extend({ + /** + * @type Array<OC.FileUpload> + */ + _uploads: {}, + + /** + * List of directories known to exist. + * + * Key is the fullpath and value is boolean, true meaning that the directory + * was already created so no need to create it again. + */ + _knownDirs: {}, + + /** + * @type OCA.Files.FileList + */ + fileList: null, + + /** + * @type OC.Files.Client + */ + filesClient: 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 + * also see article @link http://blog.new-bamboo.co.uk/2012/01/10/ridiculously-simple-ajax-uploads-with-formdata + */ + _supportAjaxUploadWithProgress: function() { + if (window.TESTING) { + return true; + } + return supportFileAPI() && supportAjaxUploadProgressEvents() && supportFormData(); + + // Is the File API supported? + function supportFileAPI() { + var fi = document.createElement('INPUT'); + fi.type = 'file'; + return 'files' in fi; + } + + // Are progress events supported? + function supportAjaxUploadProgressEvents() { + var xhr = new XMLHttpRequest(); + return !! (xhr && ('upload' in xhr) && ('onprogress' in xhr.upload)); + } + + // Is FormData supported? + function supportFormData() { + return !! window.FormData; + } + }, + + /** + * Returns whether an XHR upload will be used + * + * @return {bool} true if XHR upload will be used, + * false for iframe upload + */ + isXHRUpload: function () { + return !this.fileUploadParam.forceIframeTransport && + ((!this.fileUploadParam.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + /** - * deletes the jqHXR object from a data selection - * @param {object} data + * Makes sure that the upload folder and its parents exists + * + * @param {String} fullPath full path + * @return {Promise} promise that resolves when all parent folders + * were created */ - deleteUpload:function(data) { - delete data.jqXHR; + ensureFolderExists: function(fullPath) { + if (!fullPath || fullPath === '/') { + return $.Deferred().resolve().promise(); + } + + // remove trailing slash + if (fullPath.charAt(fullPath.length - 1) === '/') { + fullPath = fullPath.substr(0, fullPath.length - 1); + } + + var self = this; + var promise = this._knownDirs[fullPath]; + + if (this.fileList) { + // assume the current folder exists + this._knownDirs[this.fileList.getCurrentDirectory()] = $.Deferred().resolve().promise(); + } + + if (!promise) { + var deferred = new $.Deferred(); + promise = deferred.promise(); + this._knownDirs[fullPath] = promise; + + // make sure all parents already exist + var parentPath = OC.dirname(fullPath); + var parentPromise = this._knownDirs[parentPath]; + if (!parentPromise) { + parentPromise = this.ensureFolderExists(parentPath); + } + + parentPromise.then(function() { + self.filesClient.createDirectory(fullPath).always(function(status) { + // 405 is expected if the folder already exists + if ((status >= 200 && status < 300) || status === 405) { + self.trigger('createdfolder', fullPath); + deferred.resolve(); + return; + } + OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: fullPath})); + deferred.reject(); + }); + }, function() { + deferred.reject(); + }); + } + + return promise; + }, + + /** + * Submit the given uploads + * + * @param {Array} array of uploads to start + */ + submitUploads: function(uploads) { + var self = this; + _.each(uploads, function(upload) { + self._uploads[upload.data.uploadId] = upload; + upload.submit(); + }); + }, + + /** + * Show conflict for the given file object + * + * @param {OC.FileUpload} file upload object + */ + showConflict: function(fileUpload) { + //show "file already exists" dialog + var self = this; + var file = fileUpload.getFile(); + // retrieve more info about this file + this.filesClient.getFileInfo(fileUpload.getFullPath()).then(function(status, fileInfo) { + var original = fileInfo; + var replacement = file; + OC.dialogs.fileexists(fileUpload, original, replacement, self); + }); }, /** * cancels all uploads */ cancelUploads:function() { this.log('canceling uploads'); - jQuery.each(this._uploads, function(i, jqXHR) { - jqXHR.abort(); + jQuery.each(this._uploads, function(i, upload) { + upload.abort(); }); - this._uploads = []; + this.clear(); }, - rememberUpload:function(jqXHR) { - if (jqXHR) { - this._uploads.push(jqXHR); + /** + * Clear uploads + */ + clear: function() { + this._uploads = {}; + this._knownDirs = {}; + }, + /** + * Returns an upload by id + * + * @param {int} data uploadId + * @return {OC.FileUpload} file upload + */ + getUpload: function(data) { + if (_.isString(data)) { + return this._uploads[data]; + } else if (data.uploadId) { + return this._uploads[data.uploadId]; } + return null; }, + showUploadCancelMessage: _.debounce(function() { OC.Notification.showTemporary(t('files', 'Upload cancelled.'), {timeout: 10}); }, 500), @@ -106,8 +541,8 @@ OC.Upload = { isProcessing:function() { var count = 0; - jQuery.each(this._uploads, function(i, data) { - if (data.state() === 'pending') { + jQuery.each(this._uploads, function(i, upload) { + if (upload.isPending()) { count++; } }); @@ -115,9 +550,8 @@ OC.Upload = { }, /** * callback for the conflicts dialog - * @param {object} data */ - onCancel:function(data) { + onCancel:function() { this.cancelUploads(); }, /** @@ -147,43 +581,29 @@ OC.Upload = { }, /** * handle skipping an upload - * @param {object} data + * @param {OC.FileUpload} upload */ - onSkip:function(data) { - this.log('skip', null, data); - this.deleteUpload(data); + onSkip:function(upload) { + this.log('skip', null, upload); + upload.deleteUpload(); }, /** * handle replacing a file on the server with an uploaded file - * @param {object} data + * @param {FileUpload} data */ - onReplace:function(data) { - this.log('replace', null, data); - if (data.data) { - data.data.append('resolution', 'replace'); - } else { - if (!data.formData) { - data.formData = {}; - } - addFormData(data.formData, {resolution: 'replace'}); - } - data.submit(); + onReplace:function(upload) { + this.log('replace', null, upload); + upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_OVERWRITE); + this.submitUploads([upload]); }, /** * handle uploading a file and letting the server decide a new name - * @param {object} data + * @param {object} upload */ - onAutorename:function(data) { - this.log('autorename', null, data); - if (data.data) { - data.data.append('resolution', 'autorename'); - } else { - if (!data.formData) { - data.formData = {}; - } - addFormData(data.formData, {resolution: 'autorename'}); - } - data.submit(); + onAutorename:function(upload) { + this.log('autorename', null, upload); + upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_AUTORENAME); + this.submitUploads([upload]); }, _trace:false, //TODO implement log handler for JS per class? log:function(caption, e, data) { @@ -205,11 +625,20 @@ OC.Upload = { * @param {function} callbacks.onCancel */ checkExistingFiles: function (selection, callbacks) { - var fileList = FileList; + var fileList = this.fileList; var conflicts = []; // only keep non-conflicting uploads selection.uploads = _.filter(selection.uploads, function(upload) { - var fileInfo = fileList.findFile(upload.files[0].name); + var file = upload.getFile(); + if (file.relativePath) { + // can't check in subfolder contents + return true; + } + if (!fileList) { + // no list to check against + return true; + } + var fileInfo = fileList.findFile(file.name); if (fileInfo) { conflicts.push([ // original @@ -225,9 +654,9 @@ OC.Upload = { }); if (conflicts.length) { // wait for template loading - OC.dialogs.fileexists(null, null, null, OC.Upload).done(function() { + OC.dialogs.fileexists(null, null, null, this).done(function() { _.each(conflicts, function(conflictData) { - OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].files[0], OC.Upload); + OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].getFile(), this); }); }); } @@ -240,15 +669,16 @@ OC.Upload = { }, _hideProgressBar: function() { + var self = this; $('#uploadprogresswrapper .stop').fadeOut(); $('#uploadprogressbar').fadeOut(function() { - $('#file_upload_start').trigger(new $.Event('resized')); + self.$uploadEl.trigger(new $.Event('resized')); }); }, _showProgressBar: function() { $('#uploadprogressbar').fadeIn(); - $('#file_upload_start').trigger(new $.Event('resized')); + this.$uploadEl.trigger(new $.Event('resized')); }, /** @@ -269,12 +699,34 @@ OC.Upload = { return ($tr.attr('data-mounttype') === 'shared-root' && $tr.attr('data-mime') !== 'httpd/unix-directory'); }, - init: function() { + /** + * Initialize the upload object + * + * @param {Object} $uploadEl upload element + * @param {Object} options + * @param {OCA.Files.FileList} [options.fileList] file list object + * @param {OC.Files.Client} [options.filesClient] files client object + * @param {Object} [options.dropZone] drop zone for drag and drop upload + */ + init: function($uploadEl, options) { var self = this; - if ( $('#file_upload_start').exists() ) { - var file_upload_param = { - dropZone: $('#app-content'), // restrict dropZone to app-content div - pasteZone: null, + + options = options || {}; + + this.fileList = options.fileList; + this.filesClient = options.filesClient || OC.Files.getClient(); + + $uploadEl = $($uploadEl); + this.$uploadEl = $uploadEl; + + if ($uploadEl.exists()) { + $('#uploadprogresswrapper .stop').on('click', function() { + self.cancelUploads(); + }); + + this.fileUploadParam = { + type: 'PUT', + dropZone: options.dropZone, // restrict dropZone to content div autoUpload: false, sequentialUploads: true, //singleFileUploads is on by default, so the data.files array will always have length 1 @@ -295,9 +747,13 @@ OC.Upload = { * @returns {boolean} */ add: function(e, data) { - OC.Upload.log('add', e, data); + self.log('add', e, data); var that = $(this), freeSpace; + var upload = new OC.FileUpload(self, data); + // can't link directly due to jQuery not liking cyclic deps on its ajax object + data.uploadId = upload.getId(); + // we need to collect all data upload objects before // starting the upload so we can check their existence // and set individual conflict actions. Unfortunately, @@ -317,16 +773,17 @@ OC.Upload = { biggestFileBytes: 0 }; } + // TODO: move originalFiles to a separate container, maybe inside OC.Upload var selection = data.originalFiles.selection; // add uploads if ( selection.uploads.length < selection.filesToUpload ) { // remember upload - selection.uploads.push(data); + selection.uploads.push(upload); } //examine file - var file = data.files[0]; + var file = upload.getFile(); try { // FIXME: not so elegant... need to refactor that method to return a value Files.isFileNameValid(file.name); @@ -336,9 +793,14 @@ OC.Upload = { data.errorThrown = errorMessage; } + if (data.targetDir) { + upload.setTargetFolder(data.targetDir); + delete data.targetDir; + } + // in case folder drag and drop is not supported file will point to a directory // http://stackoverflow.com/a/20448357 - if ( ! file.type && file.size%4096 === 0 && file.size <= 102400) { + if ( ! file.type && file.size % 4096 === 0 && file.size <= 102400) { var dirUploadFailure = false; try { var reader = new FileReader(); @@ -390,7 +852,7 @@ OC.Upload = { // end upload for whole selection on error if (data.errorThrown) { - // trigger fileupload fail + // trigger fileupload fail handler var fu = that.data('blueimp-fileupload') || that.data('fileupload'); fu._trigger('fail', e, data); return false; //don't upload anything @@ -405,9 +867,7 @@ OC.Upload = { var callbacks = { onNoConflicts: function (selection) { - $.each(selection.uploads, function(i, upload) { - upload.submit(); - }); + self.submitUploads(selection.uploads); }, onSkipConflicts: function (selection) { //TODO mark conflicting files as toskip @@ -425,7 +885,7 @@ OC.Upload = { } }; - OC.Upload.checkExistingFiles(selection, callbacks); + self.checkExistingFiles(selection, callbacks); } @@ -436,106 +896,60 @@ OC.Upload = { * @param {object} e */ start: function(e) { - OC.Upload.log('start', e, null); + self.log('start', e, null); //hide the tooltip otherwise it covers the progress bar $('#upload').tipsy('hide'); }, - submit: function(e, data) { - OC.Upload.rememberUpload(data); - if (!data.formData) { - data.formData = {}; - } - - var fileDirectory = ''; - if(typeof data.files[0].relativePath !== 'undefined') { - fileDirectory = data.files[0].relativePath; + fail: function(e, data) { + var upload = self.getUpload(data); + var status = null; + if (upload) { + status = upload.getResponseStatus(); } + self.log('fail', e, upload); - var params = { - requesttoken: oc_requesttoken, - dir: data.targetDir || FileList.getCurrentDirectory(), - file_directory: fileDirectory, - }; - if (data.files[0].isReceivedShare) { - params.isReceivedShare = true; + if (data.textStatus === 'abort') { + self.showUploadCancelMessage(); + } else if (status === 412) { + // file already exists + self.showConflict(upload); + } else if (status === 404) { + // target folder does not exist any more + OC.Notification.showTemporary( + t('files', 'Target folder "{dir}" does not exist any more', {dir: upload.getFullPath()}) + ); + self.cancelUploads(); + } else if (status === 507) { + // not enough space + OC.Notification.showTemporary( + t('files', 'Not enough free space') + ); + self.cancelUploads(); + } else { + // HTTP connection problem or other error + OC.Notification.showTemporary(data.errorThrown, {timeout: 10}); } - addFormData(data.formData, params); - }, - fail: function(e, data) { - OC.Upload.log('fail', e, data); - if (typeof data.textStatus !== 'undefined' && data.textStatus !== 'success' ) { - if (data.textStatus === 'abort') { - OC.Upload.showUploadCancelMessage(); - } else { - // HTTP connection problem - var message = t('files', 'Error uploading file "{fileName}": {message}', { - fileName: escapeHTML(data.files[0].name), - message: data.errorThrown - }, undefined, {escape: false}); - OC.Notification.show(message, {timeout: 0, type: 'error'}); - if (data.result) { - var result = JSON.parse(data.result); - if (result && result[0] && result[0].data && result[0].data.code === 'targetnotfound') { - // abort upload of next files if any - OC.Upload.cancelUploads(); - } - } - } + if (upload) { + upload.deleteUpload(); } - OC.Upload.deleteUpload(data); }, /** * called for every successful upload * @param {object} e * @param {object} data */ - done: function(e, data) { - OC.Upload.log('done', e, data); - // handle different responses (json or body from iframe for ie) - var response; - if (typeof data.result === 'string') { - response = data.result; - } else { - //fetch response from iframe - response = data.result[0].body.innerText; - } - var result = JSON.parse(response); + done:function(e, data) { + var upload = self.getUpload(data); + var that = $(this); + self.log('done', e, upload); - delete data.jqXHR; - - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - - if (result.status === 'error' && result.data && result.data.message){ - data.textStatus = 'servererror'; - data.errorThrown = result.data.message; - fu._trigger('fail', e, data); - } else if (typeof result[0] === 'undefined') { - data.textStatus = 'servererror'; - data.errorThrown = t('files', 'Could not get result from server.'); - fu._trigger('fail', e, data); - } else if (result[0].status === 'readonly') { - var original = result[0]; - var replacement = data.files[0]; - OC.dialogs.fileexists(data, original, replacement, OC.Upload); - } else if (result[0].status === 'existserror') { - //show "file already exists" dialog - var original = result[0]; - var replacement = data.files[0]; - OC.dialogs.fileexists(data, original, replacement, OC.Upload); - } else if (result[0].status !== 'success') { - //delete data.jqXHR; - data.textStatus = 'servererror'; - data.errorThrown = result[0].data.message; // error message has been translated on server + var status = upload.getResponseStatus(); + if (status < 200 || status >= 300) { + // trigger fail handler + var fu = that.data('blueimp-fileupload') || that.data('fileupload'); fu._trigger('fail', e, data); - } else { // Successful upload - // Checking that the uploaded file is the last one and contained in the current directory - if (data.files[0] === data.originalFiles[data.originalFiles.length - 1] && - result[0].directory === FileList.getCurrentDirectory()) { - // Scroll to the last uploaded file and highlight all of them - var fileList = _.pluck(data.originalFiles, 'name'); - FileList.highlightFiles(fileList); - } + return; } }, /** @@ -544,15 +958,14 @@ OC.Upload = { * @param {object} data */ stop: function(e, data) { - OC.Upload.log('stop', e, data); + self.log('stop', e, data); } }; // initialize jquery fileupload (blueimp) - var fileupload = $('#file_upload_start').fileupload(file_upload_param); - window.file_upload_param = fileupload; + var fileupload = this.$uploadEl.fileupload(this.fileUploadParam); - if (supportAjaxUploadWithProgress()) { + if (this._supportAjaxUploadWithProgress()) { //remaining time var lastUpdate = new Date().getMilliseconds(); var lastSize = 0; @@ -561,19 +974,17 @@ OC.Upload = { var bufferIndex = 0; var bufferTotal = 0; for(var i = 0; i < bufferSize;i++){ - buffer[i] = 0; + buffer[i] = 0; } + // add progress handlers fileupload.on('fileuploadadd', function(e, data) { - OC.Upload.log('progress handle fileuploadadd', e, data); - //show cancel button - //if (data.dataType !== 'iframe') { //FIXME when is iframe used? only for ie? - // $('#uploadprogresswrapper .stop').show(); - //} + self.log('progress handle fileuploadadd', e, data); + self.trigger('add', e, data); }); // add progress handlers fileupload.on('fileuploadstart', function(e, data) { - OC.Upload.log('progress handle fileuploadstart', e, data); + self.log('progress handle fileuploadstart', e, data); $('#uploadprogresswrapper .stop').show(); $('#uploadprogresswrapper .label').show(); $('#uploadprogressbar').progressbar({value: 0}); @@ -584,14 +995,16 @@ OC.Upload = { + t('files', '...') + '</span></em>'); $('#uploadprogressbar').tipsy({gravity:'n', fade:true, live:true}); - OC.Upload._showProgressBar(); + self._showProgressBar(); + self.trigger('start', e, data); }); fileupload.on('fileuploadprogress', function(e, data) { - OC.Upload.log('progress handle fileuploadprogress', e, data); + self.log('progress handle fileuploadprogress', e, data); //TODO progressbar in row + self.trigger('progress', e, data); }); fileupload.on('fileuploadprogressall', function(e, data) { - OC.Upload.log('progress handle fileuploadprogressall', e, data); + self.log('progress handle fileuploadprogressall', e, data); var progress = (data.loaded / data.total) * 100; var thisUpdate = new Date().getMilliseconds(); var diffUpdate = (thisUpdate - lastUpdate)/1000; // eg. 2s @@ -608,14 +1021,14 @@ OC.Upload = { var smoothRemainingSeconds = (bufferTotal / bufferSize); //seconds var date = new Date(smoothRemainingSeconds * 1000); var timeStringDesktop = ""; - var timeStringMobile = ""; + var timeStringMobile = ""; if(date.getUTCHours() > 0){ - timeStringDesktop = t('files', '{hours}:{minutes}:{seconds} hour{plural_s} left' , { + timeStringDesktop = t('files', '{hours}:{minutes}:{seconds} hour{plural_s} left' , { hours:date.getUTCHours(), minutes: ('0' + date.getUTCMinutes()).slice(-2), seconds: ('0' + date.getUTCSeconds()).slice(-2), plural_s: ( smoothRemainingSeconds === 3600 ? "": "s") // 1 hour = 1*60m*60s = 3600s - }); + }); timeStringMobile = t('files', '{hours}:{minutes}h' , { hours:date.getUTCHours(), minutes: ('0' + date.getUTCMinutes()).slice(-2), @@ -626,12 +1039,12 @@ OC.Upload = { minutes: date.getUTCMinutes(), seconds: ('0' + date.getUTCSeconds()).slice(-2), plural_s: (smoothRemainingSeconds === 60 ? "": "s") // 1 minute = 1*60s = 60s - }); + }); timeStringMobile = t('files', '{minutes}:{seconds}m' , { minutes: date.getUTCMinutes(), seconds: ('0' + date.getUTCSeconds()).slice(-2) }); - } else if(date.getUTCSeconds() > 0){ + } else if(date.getUTCSeconds() > 0){ timeStringDesktop = t('files', '{seconds} second{plural_s} left' , { seconds: date.getUTCSeconds(), plural_s: (smoothRemainingSeconds === 1 ? "": "s") // 1 second = 1s = 1s @@ -651,17 +1064,21 @@ OC.Upload = { }) ); $('#uploadprogressbar').progressbar('value', progress); + self.trigger('progressall', e, data); }); fileupload.on('fileuploadstop', function(e, data) { - OC.Upload.log('progress handle fileuploadstop', e, data); - OC.Upload._hideProgressBar(); + self.log('progress handle fileuploadstop', e, data); + + self.clear(); + self._hideProgressBar(); }); fileupload.on('fileuploadfail', function(e, data) { - OC.Upload.log('progress handle fileuploadfail', e, data); + self.log('progress handle fileuploadfail', e, data); //if user pressed cancel hide upload progress bar and cancel button if (data.errorThrown === 'abort') { - OC.Upload._hideProgressBar(); + self._hideProgressBar(); } + self.trigger('fail', e, data); }); var disableDropState = function() { $('#app-content').removeClass('file-drag'); @@ -696,55 +1113,53 @@ OC.Upload = { filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept'); } }); - fileupload.on('fileuploaddragleave fileuploaddrop', disableDropState); - } else { - // for all browsers that don't support the progress bar - // IE 8 & 9 - - // show a spinner - fileupload.on('fileuploadstart', function() { - $('#upload').addClass('icon-loading'); - $('#upload .icon-upload').hide(); + fileupload.on('fileuploaddragleave fileuploaddrop', function (){ + $('#app-content').removeClass('file-drag'); + $('.dropping-to-dir').removeClass('dropping-to-dir'); + $('.dir-drop').removeClass('dir-drop'); + $('.icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept'); }); - // hide a spinner - fileupload.on('fileuploadstop fileuploadfail', function() { - $('#upload').removeClass('icon-loading'); - $('#upload .icon-upload').show(); + fileupload.on('fileuploadchunksend', function(e, data) { + // 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]; + 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); }); - } - } - $.assocArraySize = function(obj) { - // http://stackoverflow.com/a/6700/11236 - var size = 0, key; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - size++; - } } - return size; - }; + } // warn user not to leave the page while upload is in progress $(window).on('beforeunload', function(e) { - if (OC.Upload.isProcessing()) { + if (self.isProcessing()) { return t('files', 'File upload is in progress. Leaving the page now will cancel the upload.'); } }); //add multiply file upload attribute to all browsers except konqueror (which crashes when it's used) if (navigator.userAgent.search(/konqueror/i) === -1) { - $('#file_upload_start').attr('multiple', 'multiple'); + this.$uploadEl.attr('multiple', 'multiple'); } - window.file_upload_param = file_upload_param; - return file_upload_param; + return this.fileUploadParam; } -}; - -$(document).ready(function() { - OC.Upload.init(); -}); +}, OC.Backbone.Events); diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js index 53ad8eafeef..e728a816cc0 100644 --- a/apps/files/js/filelist.js +++ b/apps/files/js/filelist.js @@ -30,6 +30,7 @@ * @param {Object} [options.dragOptions] drag options, disabled by default * @param {Object} [options.folderDropOptions] folder drop options, disabled by default * @param {boolean} [options.detailsViewEnabled=true] whether to enable details view + * @param {boolean} [options.enableUpload=false] whether to enable uploader * @param {OC.Files.Client} [options.filesClient] files client to use */ var FileList = function($el, options) { @@ -189,6 +190,11 @@ _folderDropOptions: null, /** + * @type OC.Uploader + */ + _uploader: null, + + /** * Initialize the file list and its components * * @param $el container element with existing markup for the #controls @@ -328,8 +334,6 @@ this.$el.find('.selectedActions a').tooltip({placement:'top'}); - this.setupUploadEvents(); - this.$container.on('scroll', _.bind(this._onScroll, this)); if (options.scrollTo) { @@ -338,6 +342,20 @@ }); } + if (options.enableUpload) { + // TODO: auto-create this element + var $uploadEl = this.$el.find('#file_upload_start'); + if ($uploadEl.exists()) { + this._uploader = new OC.Uploader($uploadEl, { + fileList: this, + filesClient: this.filesClient, + dropZone: $('#content') + }); + + this.setupUploadEvents(this._uploader); + } + } + OC.Plugins.attach('OCA.Files.FileList', this); }, @@ -1420,7 +1438,10 @@ return; } this._setCurrentDir(targetDir, changeUrl, fileId); - return this.reload().then(function(success){ + + // discard finished uploads list, we'll get it through a regular reload + this._uploads = {}; + this.reload().then(function(success){ if (!success) { self.changeDirectory(currentDir, true); } @@ -1662,6 +1683,24 @@ return OCA.Files.Files.getDownloadUrl(files, dir || this.getCurrentDirectory(), isDir); }, + getUploadUrl: function(fileName, dir) { + if (_.isUndefined(dir)) { + dir = this.getCurrentDirectory(); + } + + var pathSections = dir.split('/'); + if (!_.isUndefined(fileName)) { + pathSections.push(fileName); + } + var encodedPath = ''; + _.each(pathSections, function(section) { + if (section !== '') { + encodedPath += '/' + encodeURIComponent(section); + } + }); + return OC.linkToRemoteBase('webdav') + encodedPath; + }, + /** * Generates a preview URL based on the URL space. * @param urlSpec attributes for the URL @@ -2123,19 +2162,11 @@ ) .done(function() { // TODO: error handling / conflicts - self.filesClient.getFileInfo( - targetPath, { - properties: self._getWebdavProperties() - } - ) - .then(function(status, data) { - self.add(data, {animate: true, scrollTo: true}); - deferred.resolve(status, data); - }) - .fail(function(status) { - OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name})); - deferred.reject(status); - }); + self.addAndFetchFileInfo(targetPath, '', {scrollTo: true}).then(function(status, data) { + deferred.resolve(status, data); + }, function() { + OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name})); + }); }) .fail(function(status) { if (status === 412) { @@ -2176,32 +2207,19 @@ var targetPath = this.getCurrentDirectory() + '/' + name; this.filesClient.createDirectory(targetPath) - .done(function(createStatus) { - self.filesClient.getFileInfo( - targetPath, { - properties: self._getWebdavProperties() - } - ) - .done(function(status, data) { - self.add(data, {animate: true, scrollTo: true}); - deferred.resolve(status, data); - }) - .fail(function() { - OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: name})); - deferred.reject(createStatus); - }); + .done(function() { + self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}).then(function(status, data) { + deferred.resolve(status, data); + }, function() { + OC.Notification.showTemporary(t('files', 'Could not create folder "{dir}"', {dir: name})); + }); }) .fail(function(createStatus) { // method not allowed, folder might exist already if (createStatus === 405) { - self.filesClient.getFileInfo( - targetPath, { - properties: self._getWebdavProperties() - } - ) + // add it to the list, for completeness + self.addAndFetchFileInfo(targetPath, '', {scrollTo:true}) .done(function(status, data) { - // add it to the list, for completeness - self.add(data, {animate: true, scrollTo: true}); OC.Notification.showTemporary( t('files', 'Could not create folder "{dir}" because it already exists', {dir: name}) ); @@ -2224,6 +2242,60 @@ }, /** + * Add file into the list by fetching its information from the server first. + * + * If the given directory does not match the current directory, nothing will + * be fetched. + * + * @param {String} fileName file name + * @param {String} [dir] optional directory, defaults to the current one + * @param {Object} options same options as #add + * @return {Promise} promise that resolves with the file info, or an + * already resolved Promise if no info was fetched. The promise rejects + * if the file was not found or an error occurred. + * + * @since 9.0 + */ + addAndFetchFileInfo: function(fileName, dir, options) { + var self = this; + var deferred = $.Deferred(); + if (_.isUndefined(dir)) { + dir = this.getCurrentDirectory(); + } else { + dir = dir || '/'; + } + + var targetPath = OC.joinPaths(dir, fileName); + + if ((OC.dirname(targetPath) || '/') !== this.getCurrentDirectory()) { + // no need to fetch information + deferred.resolve(); + return deferred.promise(); + } + + var addOptions = _.extend({ + animate: true, + scrollTo: false + }, options || {}); + + this.filesClient.getFileInfo(targetPath, { + properties: this._getWebdavProperties() + }) + .then(function(status, data) { + // remove first to avoid duplicates + self.remove(data.name); + self.add(data, addOptions); + deferred.resolve(status, data); + }) + .fail(function(status) { + OC.Notification.showTemporary(t('files', 'Could not create file "{file}"', {file: name})); + deferred.reject(status); + }); + + return deferred.promise(); + }, + + /** * Returns whether the given file name exists in the list * * @param {string} file file name @@ -2591,19 +2663,19 @@ /** * Setup file upload events related to the file-upload plugin + * + * @param {OC.Uploader} uploader */ - setupUploadEvents: function() { + setupUploadEvents: function(uploader) { var self = this; - // handle upload events - var fileUploadStart = this.$el; - var delegatedElement = '#file_upload_start'; + self._uploads = {}; // detect the progress bar resize - fileUploadStart.on('resized', this._onResize); + uploader.on('resized', this._onResize); - fileUploadStart.on('fileuploaddrop', delegatedElement, function(e, data) { - OC.Upload.log('filelist handle fileuploaddrop', e, data); + uploader.on('drop', function(e, data) { + self._uploader.log('filelist handle fileuploaddrop', e, data); if (self.$el.hasClass('hidden')) { // do not upload to invisible lists @@ -2654,25 +2726,20 @@ // add target dir data.targetDir = dir; } else { - // we are dropping somewhere inside the file list, which will - // upload the file to the current directory - data.targetDir = self.getCurrentDirectory(); - // cancel uploads to current dir if no permission var isCreatable = (self.getDirectoryPermissions() & OC.PERMISSION_CREATE) !== 0; if (!isCreatable) { self._showPermissionDeniedNotification(); return false; } - } - }); - fileUploadStart.on('fileuploadadd', function(e, data) { - OC.Upload.log('filelist handle fileuploadadd', e, data); - //finish delete if we are uploading a deleted file - if (self.deleteFiles && self.deleteFiles.indexOf(data.files[0].name)!==-1) { - self.finishDelete(null, true); //delete file before continuing + // we are dropping somewhere inside the file list, which will + // upload the file to the current directory + data.targetDir = self.getCurrentDirectory(); } + }); + uploader.on('add', function(e, data) { + self._uploader.log('filelist handle fileuploadadd', e, data); // add ui visualization to existing folder if (data.context && data.context.data('type') === 'dir') { @@ -2694,135 +2761,74 @@ } } + if (!data.targetDir) { + data.targetDir = self.getCurrentDirectory(); + } + }); /* * when file upload done successfully add row to filelist * update counter when uploading to sub folder */ - fileUploadStart.on('fileuploaddone', function(e, data) { - OC.Upload.log('filelist handle fileuploaddone', e, data); + uploader.on('done', function(e, upload) { + self._uploader.log('filelist handle fileuploaddone', e, data); - var response; - if (typeof data.result === 'string') { - response = data.result; - } else { - // fetch response from iframe - response = data.result[0].body.innerText; + var data = upload.data; + var status = data.jqXHR.status; + if (status < 200 || status >= 300) { + // error was handled in OC.Uploads already + return; } - var result = JSON.parse(response); - - if (typeof result[0] !== 'undefined' && result[0].status === 'success') { - var file = result[0]; - var size = 0; - - if (data.context && data.context.data('type') === 'dir') { - - // update upload counter ui - var uploadText = data.context.find('.uploadtext'); - var currentUploads = parseInt(uploadText.attr('currentUploads'), 10); - currentUploads -= 1; - uploadText.attr('currentUploads', currentUploads); - var translatedText = n('files', 'Uploading %n file', 'Uploading %n files', currentUploads); - if (currentUploads === 0) { - self.showFileBusyState(uploadText.closest('tr'), false); - uploadText.text(translatedText); - uploadText.hide(); - } else { - uploadText.text(translatedText); - } - - // update folder size - size = parseInt(data.context.data('size'), 10); - size += parseInt(file.size, 10); - data.context.attr('data-size', size); - data.context.find('td.filesize').text(humanFileSize(size)); - } else { - // only append new file if uploaded into the current folder - if (file.directory !== self.getCurrentDirectory()) { - // Uploading folders actually uploads a list of files - // for which the target directory (file.directory) might lie deeper - // than the current directory - - var fileDirectory = file.directory.replace('/','').replace(/\/$/, ""); - var currentDirectory = self.getCurrentDirectory().replace('/','').replace(/\/$/, "") + '/'; - - if (currentDirectory !== '/') { - // abort if fileDirectory does not start with current one - if (fileDirectory.indexOf(currentDirectory) !== 0) { - return; - } - - // remove the current directory part - fileDirectory = fileDirectory.substr(currentDirectory.length); - } - - // only take the first section of the path - fileDirectory = fileDirectory.split('/'); - - var fd; - // if the first section exists / is a subdir - if (fileDirectory.length) { - fileDirectory = fileDirectory[0]; - - // See whether it is already in the list - fd = self.findFileEl(fileDirectory); - if (fd.length === 0) { - var dir = { - name: fileDirectory, - type: 'dir', - mimetype: 'httpd/unix-directory', - permissions: file.permissions, - size: 0, - id: file.parentId - }; - fd = self.add(dir, {insert: true}); - } - // update folder size - size = parseInt(fd.attr('data-size'), 10); - size += parseInt(file.size, 10); - fd.attr('data-size', size); - fd.find('td.filesize').text(OC.Util.humanFileSize(size)); - } - - return; - } - - // add as stand-alone row to filelist - size = t('files', 'Pending'); - if (data.files[0].size>=0) { - size=data.files[0].size; - } - //should the file exist in the list remove it - self.remove(file.name); - - // create new file context - data.context = self.add(file, {animate: true}); - } + var fileName = upload.getFileName(); + var fetchInfoPromise = self.addAndFetchFileInfo(fileName, upload.getFullPath()); + if (!self._uploads) { + self._uploads = {}; + } + if (OC.isSamePath(OC.dirname(upload.getFullPath() + '/'), self.getCurrentDirectory())) { + self._uploads[fileName] = fetchInfoPromise; } - }); - fileUploadStart.on('fileuploadstop', function() { - OC.Upload.log('filelist handle fileuploadstop'); - //cleanup uploading to a dir var uploadText = self.$fileList.find('tr .uploadtext'); self.showFileBusyState(uploadText.closest('tr'), false); uploadText.fadeOut(); uploadText.attr('currentUploads', 0); - + }); + uploader.on('createdfolder', function(fullPath) { + self.addAndFetchFileInfo(OC.basename(fullPath), OC.dirname(fullPath)); + }); + uploader.on('stop', function() { + self._uploader.log('filelist handle fileuploadstop'); + + // prepare list of uploaded file names in the current directory + // and discard the other ones + var promises = _.values(self._uploads); + var fileNames = _.keys(self._uploads); + self._uploads = []; + + // as soon as all info is fetched + $.when.apply($, promises).then(function() { + // highlight uploaded files + self.highlightFiles(fileNames); + }); self.updateStorageStatistics(); + + var uploadText = self.$fileList.find('tr .uploadtext'); + self.showFileBusyState(uploadText.closest('tr'), false); + uploadText.fadeOut(); + uploadText.attr('currentUploads', 0); }); - fileUploadStart.on('fileuploadfail', function(e, data) { - OC.Upload.log('filelist handle fileuploadfail', e, data); + uploader.on('fail', function(e, data) { + self._uploader.log('filelist handle fileuploadfail', e, data); + + self._uploads = []; //if user pressed cancel hide upload chrome - if (data.errorThrown === 'abort') { - //cleanup uploading to a dir - var uploadText = self.$fileList.find('tr .uploadtext'); - self.showFileBusyState(uploadText.closest('tr'), false); - uploadText.fadeOut(); - uploadText.attr('currentUploads', 0); - } + //cleanup uploading to a dir + var uploadText = self.$fileList.find('tr .uploadtext'); + self.showFileBusyState(uploadText.closest('tr'), false); + uploadText.fadeOut(); + uploadText.attr('currentUploads', 0); self.updateStorageStatistics(); }); diff --git a/apps/files/js/files.js b/apps/files/js/files.js index 2873b84bc9a..0be098b2e73 100644 --- a/apps/files/js/files.js +++ b/apps/files/js/files.js @@ -226,17 +226,6 @@ // TODO: move file list related code (upload) to OCA.Files.FileList $('#file_action_panel').attr('activeAction', false); - // Triggers invisible file input - $('#upload a').on('click', function() { - $(this).parent().children('#file_upload_start').trigger('click'); - return false; - }); - - // Trigger cancelling of file upload - $('#uploadprogresswrapper .stop').on('click', function() { - OC.Upload.cancelUploads(); - }); - // drag&drop support using jquery.fileupload // TODO use OC.dialogs $(document).bind('drop dragover', function (e) { |