nextcloud/apps/files/js/file-upload.js

1207 lines
34 KiB
JavaScript
Raw Normal View History

/*
* Copyright (c) 2014
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
2013-08-14 19:49:45 +04:00
/**
2013-09-08 12:41:20 +04:00
* The file upload code uses several hooks to interact with blueimps jQuery file upload library:
* 1. the core upload handling hooks are added when initializing the plugin,
* 2. if the browser supports progress events they are added in a separate set after the initialization
* 3. every app can add it's own triggers for fileupload
* - files adds d'n'd handlers and also reacts to done events to add new rows to the filelist
* - TODO pictures upload button
* - TODO music upload button
2013-08-14 19:49:45 +04:00
*/
/* global jQuery, humanFileSize, md5 */
2013-09-16 16:10:19 +04:00
/**
* File upload object
*
* @class OC.FileUpload
* @classdesc
*
* Represents a file upload
*
* @param {OC.Uploader} uploader uploader
* @param {Object} data blueimp data
2013-09-16 16:10:19 +04:00
*/
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 = {
/**
* Unique upload id
*
* @type string
*/
id: null,
/**
* Upload element
*
* @type Object
*/
$uploadEl: null,
/**
* Target folder
*
* @type string
*/
_targetFolder: '',
2013-08-16 13:40:55 +04:00
/**
* @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.
*
* @return {String} file name
*/
getFileName: function() {
// autorenamed name
if (this._newName) {
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 || '');
},
/**
* Get full path for the target file,
* including relative path and file name.
*
* @return {String} full path
*/
getFullFilePath: function() {
return OC.joinPaths(this.getFullPath(), this.getFile().name);
},
/**
* Returns conflict resolution mode.
*
* @return {int} conflict mode
*/
getConflictMode: function() {
return this._conflictMode || OC.FileUpload.CONFLICT_MODE_DETECT;
},
/**
* Set conflict resolution mode.
* See CONFLICT_MODE_* constants.
*
* @param {int} mode conflict mode
*/
setConflictMode: function(mode) {
this._conflictMode = mode;
},
deleteUpload: function() {
delete this.data.jqXHR;
},
/**
* Trigger autorename and append "(2)".
* Multiple calls will increment the appended number.
*/
autoRename: function() {
var name = this.getFile().name;
if (!this._renameAttempt) {
this._renameAttempt = 1;
}
var dotPos = name.lastIndexOf('.');
var extPart = '';
if (dotPos > 0) {
this._newName = name.substr(0, dotPos);
extPart = name.substr(dotPos);
} else {
this._newName = name;
}
// generate new name
this._renameAttempt++;
this._newName = this._newName + ' (' + this._renameAttempt + ')' + extPart;
},
/**
* 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(this.getFileName(), 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._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
this.data.headers['If-None-Match'] = '*';
}
var userName = this.uploader.davClient.getUserName();
var password = this.uploader.davClient.getPassword();
if (userName) {
// copy username/password from DAV client
this.data.headers['Authorization'] =
'Basic ' + btoa(userName + ':' + (password || ''));
}
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/' + OC.getCurrentUser().uid + '/' + 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();
});
},
/**
* Process end of transfer
*/
done: function() {
if (!this.data.isChunked) {
return $.Deferred().resolve().promise();
}
var uid = OC.getCurrentUser().uid;
var mtime = this.getFile().lastModified;
var size = this.getFile().size;
var headers = {};
if (mtime) {
headers['X-OC-Mtime'] = mtime / 1000;
}
if (size) {
headers['OC-Total-Length'] = size;
}
return this.uploader.davClient.move(
'uploads/' + uid + '/' + this.getId() + '/.file',
'files/' + uid + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()),
true,
headers
);
},
_deleteChunkFolder: function() {
// delete transfer directory for this upload
this.uploader.davClient.remove(
'uploads/' + OC.getCurrentUser().uid + '/' + this.getId()
);
},
/**
* Abort the upload
*/
abort: function() {
if (this.data.isChunked) {
this._deleteChunkFolder();
}
this.data.abort();
this.deleteUpload();
},
/**
* Fail the upload
*/
fail: function() {
this.deleteUpload();
if (this.data.isChunked) {
this._deleteChunkFolder();
}
},
/**
* Returns the server response
*
* @return {Object} response
*/
getResponse: function() {
var response = this.data.response();
if (response.errorThrown) {
// attempt parsing Sabre exception is available
var xml = response.jqXHR.responseXML;
if (xml.documentElement.localName === 'error' && xml.documentElement.namespaceURI === 'DAV:') {
var messages = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'message');
var exceptions = xml.getElementsByTagNameNS('http://sabredav.org/ns', 'exception');
if (messages.length) {
response.message = messages[0].textContent;
}
if (exceptions.length) {
response.exception = exceptions[0].textContent;
}
return response;
}
}
if (typeof response.result !== 'string' && response.result) {
//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;
}
};
2013-09-08 12:41:20 +04:00
/**
* keeps track of uploads in progress and implements callbacks for the conflicts dialog
* @namespace
2013-09-08 12:41:20 +04:00
*/
OC.Uploader = function() {
this.init.apply(this, arguments);
};
OC.Uploader.prototype = _.extend({
/**
* @type Array<OC.FileUpload>
*/
_uploads: {},
2013-09-08 12:41:20 +04:00
/**
* 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,
/**
* 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
* 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
2013-09-08 12:41:20 +04:00
*/
isXHRUpload: function () {
return !this.fileUploadParam.forceIframeTransport &&
((!this.fileUploadParam.multipart && $.support.xhrFileUpload) ||
$.support.xhrFormDataFileUpload);
},
/**
* 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
*/
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.show(t('files', 'Could not create folder "{dir}"', {dir: fullPath}), {type: 'error'});
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();
// already attempted autorename but the server said the file exists ? (concurrently added)
if (fileUpload.getConflictMode() === OC.FileUpload.CONFLICT_MODE_AUTORENAME) {
// attempt another autorename, defer to let the current callback finish
_.defer(function() {
self.onAutorename(fileUpload);
});
return;
}
// retrieve more info about this file
this.filesClient.getFileInfo(fileUpload.getFullFilePath()).then(function(status, fileInfo) {
var original = fileInfo;
var replacement = file;
original.directory = original.path;
OC.dialogs.fileexists(fileUpload, original, replacement, self);
});
2013-09-07 00:40:10 +04:00
},
2013-09-08 12:41:20 +04:00
/**
* cancels all uploads
*/
2013-08-16 13:40:55 +04:00
cancelUploads:function() {
this.log('canceling uploads');
jQuery.each(this._uploads, function(i, upload) {
upload.abort();
2013-08-16 13:40:55 +04:00
});
this.clear();
},
/**
* 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 && this._uploads[data.uploadId]) {
this._uploads[data.uploadId].data = data;
return this._uploads[data.uploadId];
}
return null;
2013-08-16 13:40:55 +04:00
},
2015-10-07 17:34:06 +03:00
showUploadCancelMessage: _.debounce(function() {
OC.Notification.show(t('files', 'Upload cancelled.'), {timeout : 7, type: 'error'});
2015-10-07 17:34:06 +03:00
}, 500),
2013-09-08 12:41:20 +04:00
/**
* callback for the conflicts dialog
*/
onCancel:function() {
2013-09-07 00:40:10 +04:00
this.cancelUploads();
},
2013-09-08 12:41:20 +04:00
/**
* callback for the conflicts dialog
* calls onSkip, onReplace or onAutorename for each conflict
2013-09-19 13:11:22 +04:00
* @param {object} conflicts - list of conflict elements
2013-09-08 12:41:20 +04:00
*/
2013-09-05 19:46:19 +04:00
onContinue:function(conflicts) {
var self = this;
//iterate over all conflicts
jQuery.each(conflicts, function (i, conflict) {
conflict = $(conflict);
var keepOriginal = conflict.find('.original input[type="checkbox"]:checked').length === 1;
var keepReplacement = conflict.find('.replacement input[type="checkbox"]:checked').length === 1;
if (keepOriginal && keepReplacement) {
// when both selected -> autorename
self.onAutorename(conflict.data('data'));
} else if (keepReplacement) {
// when only replacement selected -> overwrite
self.onReplace(conflict.data('data'));
} else {
// when only original seleted -> skip
// when none selected -> skip
self.onSkip(conflict.data('data'));
}
});
},
2013-09-08 12:41:20 +04:00
/**
* handle skipping an upload
* @param {OC.FileUpload} upload
2013-09-08 12:41:20 +04:00
*/
onSkip:function(upload) {
this.log('skip', null, upload);
upload.deleteUpload();
},
2013-09-08 12:41:20 +04:00
/**
* handle replacing a file on the server with an uploaded file
* @param {FileUpload} data
2013-09-08 12:41:20 +04:00
*/
onReplace:function(upload) {
this.log('replace', null, upload);
upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_OVERWRITE);
this.submitUploads([upload]);
},
2013-09-08 12:41:20 +04:00
/**
* handle uploading a file and letting the server decide a new name
* @param {object} upload
2013-09-08 12:41:20 +04:00
*/
onAutorename:function(upload) {
this.log('autorename', null, upload);
upload.setConflictMode(OC.FileUpload.CONFLICT_MODE_AUTORENAME);
do {
upload.autoRename();
// if file known to exist on the client side, retry
} while (this.fileList && this.fileList.inList(upload.getFileName()));
// resubmit upload
this.submitUploads([upload]);
2013-08-16 13:40:55 +04:00
},
_trace:false, //TODO implement log handler for JS per class?
log:function(caption, e, data) {
if (this._trace) {
console.log(caption);
console.log(data);
}
},
2013-09-08 12:41:20 +04:00
/**
* checks the list of existing files prior to uploading and shows a simple dialog to choose
2013-09-19 13:13:11 +04:00
* skip all, replace all or choose which files to keep
*
2013-09-19 13:11:22 +04:00
* @param {array} selection of files to upload
* @param {object} callbacks - object with several callback methods
* @param {function} callbacks.onNoConflicts
* @param {function} callbacks.onSkipConflicts
* @param {function} callbacks.onReplaceConflicts
* @param {function} callbacks.onChooseConflicts
* @param {function} callbacks.onCancel
2013-09-08 12:41:20 +04:00
*/
checkExistingFiles: function (selection, callbacks) {
var fileList = this.fileList;
var conflicts = [];
// only keep non-conflicting uploads
selection.uploads = _.filter(selection.uploads, function(upload) {
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
_.extend(fileInfo, {
directory: fileInfo.directory || fileInfo.path || fileList.getCurrentDirectory()
}),
// replacement (File object)
upload
]);
return false;
}
return true;
});
if (conflicts.length) {
// wait for template loading
OC.dialogs.fileexists(null, null, null, this).done(function() {
_.each(conflicts, function(conflictData) {
OC.dialogs.fileexists(conflictData[1], conflictData[0], conflictData[1].getFile(), this);
});
});
}
// upload non-conflicting files
// note: when reaching the server they might still meet conflicts
// if the folder was concurrently modified, these will get added
// to the already visible dialog, if applicable
callbacks.onNoConflicts(selection);
},
_hideProgressBar: function() {
var self = this;
2014-12-18 16:09:17 +03:00
$('#uploadprogresswrapper .stop').fadeOut();
$('#uploadprogressbar').fadeOut(function() {
self.$uploadEl.trigger(new $.Event('resized'));
});
},
_showProgressBar: function() {
$('#uploadprogressbar').fadeIn();
this.$uploadEl.trigger(new $.Event('resized'));
},
/**
* Returns whether the given file is known to be a received shared file
*
* @param {Object} file file
* @return {bool} true if the file is a shared file
*/
_isReceivedSharedFile: function(file) {
if (!window.FileList) {
return false;
}
var $tr = window.FileList.findFileEl(file.name);
if (!$tr.length) {
return false;
}
return ($tr.attr('data-mounttype') === 'shared-root' && $tr.attr('data-mime') !== 'httpd/unix-directory');
},
/**
* 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;
options = options || {};
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;
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
/**
* on first add of every selection
* - check all files of originalFiles array with files in dir
* - on conflict show dialog
* - skip all -> remember as single skip action for all conflicting files
* - replace all -> remember as single replace action for all conflicting files
* - choose -> show choose dialog
* - mark files to keep
* - when only existing -> remember as single skip action
* - when only new -> remember as single replace action
* - when both -> remember as single autorename action
* - start uploading selection
* @param {object} e
* @param {object} data
* @returns {boolean}
*/
add: function(e, data) {
self.log('add', e, data);
2014-05-09 16:06:59 +04:00
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();
2014-05-09 16:06:59 +04:00
// we need to collect all data upload objects before
// starting the upload so we can check their existence
// and set individual conflict actions. Unfortunately,
// there is only one variable that we can use to identify
// the selection a data upload is part of, so we have to
// collect them in data.originalFiles turning
// singleFileUploads off is not an option because we want
// to gracefully handle server errors like 'already exists'
// create a container where we can store the data objects
if ( ! data.originalFiles.selection ) {
// initialize selection and remember number of files to upload
data.originalFiles.selection = {
uploads: [],
filesToUpload: data.originalFiles.length,
totalBytes: 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(upload);
}
//examine file
var file = upload.getFile();
try {
// FIXME: not so elegant... need to refactor that method to return a value
Files.isFileNameValid(file.name);
}
catch (errorMessage) {
data.textStatus = 'invalidcharacters';
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) {
var dirUploadFailure = false;
try {
2014-05-09 16:06:59 +04:00
var reader = new FileReader();
reader.readAsBinaryString(file);
} catch (NS_ERROR_FILE_ACCESS_DENIED) {
//file is a directory
dirUploadFailure = true;
}
if (dirUploadFailure) {
data.textStatus = 'dirorzero';
2014-05-09 16:06:59 +04:00
data.errorThrown = t('files',
'Unable to upload {filename} as it is a directory or has 0 bytes',
{filename: file.name}
);
}
}
// only count if we're not overwriting an existing shared file
if (self._isReceivedSharedFile(file)) {
file.isReceivedShare = true;
} else {
// add size
selection.totalBytes += file.size;
}
// check free space
freeSpace = $('#free_space').val();
if (freeSpace >= 0 && selection.totalBytes > freeSpace) {
data.textStatus = 'notenoughspace';
2014-05-09 16:06:59 +04:00
data.errorThrown = t('files',
'Not enough free space, you are uploading {size1} but only {size2} is left', {
'size1': humanFileSize(selection.totalBytes),
'size2': humanFileSize($('#free_space').val())
});
}
// end upload for whole selection on error
if (data.errorThrown) {
// trigger fileupload fail handler
var fu = that.data('blueimp-fileupload') || that.data('fileupload');
fu._trigger('fail', e, data);
return false; //don't upload anything
}
// check existing files when all is collected
if ( selection.uploads.length >= selection.filesToUpload ) {
//remove our selection hack:
delete data.originalFiles.selection;
var callbacks = {
onNoConflicts: function (selection) {
self.submitUploads(selection.uploads);
},
onSkipConflicts: function (selection) {
//TODO mark conflicting files as toskip
},
onReplaceConflicts: function (selection) {
//TODO mark conflicting files as toreplace
},
onChooseConflicts: function (selection) {
//TODO mark conflicting files as chosen
},
onCancel: function (selection) {
$.each(selection.uploads, function(i, upload) {
upload.abort();
});
}
};
self.checkExistingFiles(selection, callbacks);
}
return true; // continue adding files
},
/**
* called after the first add, does NOT have the data param
* @param {object} e
*/
start: function(e) {
self.log('start', e, null);
//hide the tooltip otherwise it covers the progress bar
$('#upload').tooltip('hide');
},
fail: function(e, data) {
var upload = self.getUpload(data);
var status = null;
if (upload) {
status = upload.getResponseStatus();
}
self.log('fail', e, upload);
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.show(t('files', 'Target folder "{dir}" does not exist any more', {dir: upload.getFullPath()} ), {type: 'error'});
self.cancelUploads();
} else if (status === 507) {
// not enough space
OC.Notification.show(t('files', 'Not enough free space'), {type: 'error'});
self.cancelUploads();
} else {
// HTTP connection problem or other error
var message = '';
if (upload) {
var response = upload.getResponse();
message = response.message;
}
OC.Notification.show(message || data.errorThrown, {type: 'error'});
2013-09-16 16:10:19 +04:00
}
if (upload) {
upload.fail();
}
},
/**
* called for every successful upload
* @param {object} e
* @param {object} data
*/
done:function(e, data) {
var upload = self.getUpload(data);
var that = $(this);
self.log('done', e, upload);
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);
return;
}
},
/**
* called after last upload
* @param {object} e
* @param {object} data
*/
stop: function(e, data) {
self.log('stop', e, data);
2013-09-16 16:10:19 +04:00
}
};
if (options.maxChunkSize) {
this.fileUploadParam.maxChunkSize = options.maxChunkSize;
}
// initialize jquery fileupload (blueimp)
var fileupload = this.$uploadEl.fileupload(this.fileUploadParam);
if (this._supportAjaxUploadWithProgress()) {
//remaining time
var lastUpdate, lastSize, bufferSize, buffer, bufferIndex, bufferIndex2, bufferTotal;
// add progress handlers
fileupload.on('fileuploadadd', function(e, data) {
self.log('progress handle fileuploadadd', e, data);
self.trigger('add', e, data);
});
// add progress handlers
fileupload.on('fileuploadstart', function(e, data) {
self.log('progress handle fileuploadstart', e, data);
2014-12-18 16:09:17 +03:00
$('#uploadprogresswrapper .stop').show();
$('#uploadprogresswrapper .label').show();
2014-05-09 16:06:59 +04:00
$('#uploadprogressbar').progressbar({value: 0});
2014-08-13 20:46:23 +04:00
$('#uploadprogressbar .ui-progressbar-value').
html('<em class="label inner"><span class="desktop">'
+ t('files', 'Uploading …')
2014-08-13 20:46:23 +04:00
+ '</span><span class="mobile">'
+ t('files', '…')
2014-08-13 20:46:23 +04:00
+ '</span></em>');
$('#uploadprogressbar').tooltip({placement: 'bottom'});
self._showProgressBar();
// initial remaining time variables
lastUpdate = new Date().getTime();
lastSize = 0;
bufferSize = 20;
buffer = [];
bufferIndex = 0;
bufferIndex2 = 0;
bufferTotal = 0;
for(var i = 0; i < bufferSize; i++){
buffer[i] = 0;
}
self.trigger('start', e, data);
});
fileupload.on('fileuploadprogress', function(e, data) {
self.log('progress handle fileuploadprogress', e, data);
//TODO progressbar in row
self.trigger('progress', e, data);
});
fileupload.on('fileuploadprogressall', function(e, data) {
self.log('progress handle fileuploadprogressall', e, data);
var progress = (data.loaded / data.total) * 100;
var thisUpdate = new Date().getTime();
var diffUpdate = (thisUpdate - lastUpdate)/1000; // eg. 2s
lastUpdate = thisUpdate;
var diffSize = data.loaded - lastSize;
lastSize = data.loaded;
diffSize = diffSize / diffUpdate; // apply timing factor, eg. 1MiB/2s = 0.5MiB/s, unit is byte per second
var remainingSeconds = ((data.total - data.loaded) / diffSize);
if(remainingSeconds >= 0) {
bufferTotal = bufferTotal - (buffer[bufferIndex]) + remainingSeconds;
buffer[bufferIndex] = remainingSeconds; //buffer to make it smoother
bufferIndex = (bufferIndex + 1) % bufferSize;
bufferIndex2++;
}
var smoothRemainingSeconds;
if (bufferIndex2 > 0 && bufferIndex2 < 20) {
smoothRemainingSeconds = bufferTotal / bufferIndex2;
} else if (bufferSize > 0) {
smoothRemainingSeconds = bufferTotal / bufferSize;
} else {
smoothRemainingSeconds = 1;
}
var h = moment.duration(smoothRemainingSeconds, "seconds").humanize();
if (!(smoothRemainingSeconds >= 0 && smoothRemainingSeconds < 14400)) {
// show "Uploading ..." for durations longer than 4 hours
h = t('files', 'Uploading …');
}
$('#uploadprogressbar .label .mobile').text(h);
$('#uploadprogressbar .label .desktop').text(h);
2014-08-13 20:46:23 +04:00
$('#uploadprogressbar').attr('original-title',
t('files', '{loadedSize} of {totalSize} ({bitrate})' , {
loadedSize: humanFileSize(data.loaded),
totalSize: humanFileSize(data.total),
bitrate: humanFileSize(data.bitrate / 8) + '/s'
2014-08-13 20:46:23 +04:00
})
);
$('#uploadprogressbar').progressbar('value', progress);
self.trigger('progressall', e, data);
});
fileupload.on('fileuploadstop', function(e, data) {
self.log('progress handle fileuploadstop', e, data);
self.clear();
Fix progress bar hidden before the upload ends The jQuery File Upload plugin triggers the "stop" event once there are no more files being uploaded (even if some of them were added when another upload was already in progress). Therefore, the progress bar should be hidden in the "fileuploadstop" callback. In some cases the "stop" event is not triggered and thus the progress bar is not hidden once no more files are being uploaded. This is caused by a race condition and it will be fixed in another commit; except in buggy cases like that one (that need to be fixed anyway) it is safe to hide the progress bar in the "fileuploadstop" callback. In any case, note that the callbacks in "fileuploaddone" may be called after the "stop" event was triggered and handled when using chunked uploads. In that case once all the chunks are uploaded the assembled file is moved to its final destination, so its promise could be resolved after the "stop" event was triggered. Therefore a different approach would be needed to keep the progress bar visible until the chunked upload is truly finished, but for the time being the current one is good enough. Before this commit the progress bar was being hidden when the first upload finished, either successfully or with an error, no matter if there were other files being uploaded too. The progress bar was being explicitly hidden also when the upload was cancelled. When an upload is cancelled all the single uploads are aborted, which triggers a "fail" event for each of them. However, the "stop" event is always triggered when no more files are being uploaded, so it is triggered too once all the single uploads were aborted. As all the single uploads are immediately aborted in a loop when the general upload is cancelled it makes no difference to hide the progress bar when the first single upload is aborted or when all the single uploads were aborted, so the progress bar is no longer explicitly hidden in the former case. Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
2018-04-04 19:04:26 +03:00
self._hideProgressBar();
self.trigger('stop', e, data);
});
fileupload.on('fileuploadfail', function(e, data) {
self.log('progress handle fileuploadfail', e, data);
self.trigger('fail', e, data);
});
var disableDropState = 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');
};
var disableClassOnFirefox = _.debounce(function() {
disableDropState();
}, 100);
2016-06-15 14:16:11 +03:00
fileupload.on('fileuploaddragover', function(e){
$('#app-content').addClass('file-drag');
// dropping a folder in firefox doesn't cause a drop event
// this is simulated by simply invoke disabling all classes
// once no dragover event isn't noticed anymore
if (/Firefox/i.test(navigator.userAgent)) {
disableClassOnFirefox();
}
$('#emptycontent .icon-folder').addClass('icon-filetype-folder-drag-accept');
var filerow = $(e.delegatedEvent.target).closest('tr');
if(!filerow.hasClass('dropping-to-dir')){
$('.dropping-to-dir .icon-filetype-folder-drag-accept').removeClass('icon-filetype-folder-drag-accept');
$('.dropping-to-dir').removeClass('dropping-to-dir');
$('.dir-drop').removeClass('dir-drop');
}
if(filerow.attr('data-type') === 'dir'){
$('#app-content').addClass('dir-drop');
filerow.addClass('dropping-to-dir');
2016-06-15 14:16:11 +03:00
filerow.find('.thumbnail').addClass('icon-filetype-folder-drag-accept');
}
});
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');
2014-12-18 23:33:08 +03:00
});
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].split('-')[0];
data.url = OC.getRootPath() +
'/remote.php/dav/uploads' +
'/' + OC.getCurrentUser().uid +
'/' + upload.getId() +
'/' + chunkId;
delete data.contentRange;
delete data.headers['Content-Range'];
2014-12-18 23:33:08 +03:00
});
fileupload.on('fileuploaddone', function(e, data) {
var upload = self.getUpload(data);
upload.done().then(function() {
self.trigger('done', e, upload);
}).fail(function(status, response) {
var message = response.message;
if (status === 507) {
// not enough space
OC.Notification.show(message || t('files', 'Not enough free space'), {type: 'error'});
self.cancelUploads();
} else if (status === 409) {
OC.Notification.show(message || t('files', 'Target folder does not exist any more'), {type: 'error'});
} else {
OC.Notification.show(message || t('files', 'Error when assembling chunks, status code {status}', {status: status}), {type: 'error'});
}
self.trigger('fail', e, data);
});
});
fileupload.on('fileuploaddrop', function(e, data) {
self.trigger('drop', e, data);
if (e.isPropagationStopped()) {
return false;
}
});
}
}
//add multiply file upload attribute to all browsers except konqueror (which crashes when it's used)
if (navigator.userAgent.search(/konqueror/i) === -1) {
this.$uploadEl.attr('multiple', 'multiple');
}
return this.fileUploadParam;
}
}, OC.Backbone.Events);