apps/files/js/file-upload.js
/*
* Copyright (c) 2018
*
* This file is licensed under the Affero General Public License version 3
* or later.
*
* See the COPYING-README file.
*
*/
/**
* 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
*/
/* global jQuery, humanFileSize, md5 */
/**
* File upload object
*
* @class OC.FileUpload
* @classdesc
*
* Represents a file upload
*
* @param {OC.Uploader} uploader uploader
* @param {Object} data blueimp data
*/
OC.FileUpload = function(uploader, data) {
this.uploader = uploader;
this.data = data;
if (!data) {
throw 'Missing "data" argument in OC.FileUpload constructor';
}
var basePath = '';
if (this.uploader.fileList) {
basePath = this.uploader.fileList.getCurrentDirectory();
}
var path = OC.joinPaths(basePath, this.getFile().relativePath || '', 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.CONFLICT_MODE_AUTORENAME_SERVER = 3;
OC.FileUpload.prototype = {
/**
* Unique upload id
*
* @type string
*/
id: null,
/**
* Upload element
*
* @type Object
*/
$uploadEl: null,
/**
* 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.
*
* @return {String} file name
*/
getFileName: function() {
// autorenamed name
if (this._newName) {
return this._newName;
}
var fileName = this.getFile().name;
return this.sanitizeFileName(fileName);
},
/**
* Return the sanitized file name.
*
* @return {String} file name
*/
sanitizeFileName: function(fileName) {
return fileName.trim();
},
/**
* Return the sanitized path.
*
* @return {String} path
*/
sanitizePath: function(path) {
if(!path){
return path;
}
var pathSegments = path.split('/');
var sanitizedPathSegments = [];
for (var i = 0; i < pathSegments.length; i++) {
sanitizedPathSegments.push(pathSegments[i].trim());
}
return sanitizedPathSegments.join('/');
},
getLastModified: function() {
var file = this.getFile();
if (file.lastModifiedDate) {
return file.lastModifiedDate.getTime() / 1000;
}
if (file.lastModified) {
return file.lastModified / 1000;
}
return null;
},
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() {
var relativePath = this.getFile().relativePath;
var sanitizedRelativePath = this.sanitizePath(relativePath);
return OC.joinPaths(this._targetFolder, sanitizedRelativePath || '');
},
/**
* 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;
},
/**
* Returns whether the upload is in progress
*
* @return {bool}
*/
isPending: function() {
if (!this.data || !this.data.state) {
// this should not be possible!
var stack = new Error().stack;
console.error(stack);
return false;
}
return this.data.state() === 'pending';
},
deleteUpload: function() {
delete this.data.jqXHR;
},
/**
* Trigger autorename and append "(2)".
* Multiple calls will increment the appended number.
*/
autoRename: function() {
var name = this.sanitizeFileName(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();
/**
* If the variable file is a directory, we just create it and return.
* Files being handled separately
*/
if (file.isDirectory){
return this.uploader.ensureFolderExists(OC.joinPaths(this._targetFolder, this.sanitizePath(file.fullPath)));
}
// it was a folder upload, so make sure the parent directory exists already
var folderPromise;
if (file.relativePath) {
folderPromise = this.uploader.ensureFolderExists(this.getFullPath());
} else {
folderPromise = $.Deferred().resolve().promise();
}
if (this.uploader.url) {
if (_.isFunction(this.uploader.url)) {
this.data.url = this.uploader.url(this.getFileName(), this.getFullPath());
} else {
this.data.url = this.uploader.url;
}
} else 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'] = '*';
}
if (this._conflictMode === OC.FileUpload.CONFLICT_MODE_AUTORENAME_SERVER) {
// server-side autorename (not supported on all endpoints)
this.data.headers['OC-Autorename'] = '1';
}
var lastModified = this.getLastModified();
if (lastModified) {
// preserve timestamp
this.data.headers['X-OC-Mtime'] = '' + lastModified;
}
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 || ''));
}
this.data.headers['requesttoken'] = OC.requestToken;
// prevent global error handler to kick in on timeout
this.data.allowAuthErrors = true;
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.getLastModified();
var size = this.getFile().size;
var headers = {};
if (mtime) {
headers['X-OC-Mtime'] = mtime;
}
if (size) {
headers['OC-Total-Length'] = size;
}
headers['OC-LazyOps'] = 1;
var doneDeferred = $.Deferred();
this.uploader.davClient.move(
'uploads/' + uid + '/' + this.getId() + '/.file',
'files/' + uid + '/' + OC.joinPaths(this.getFullPath(), this.getFileName()),
true,
headers
).then(function (status, response) {
// a 202 response means the server is performing the final MOVE in an async manner,
// so we need to poll its status
if (status === 202) {
var poll = function() {
$.ajax(response.xhr.getResponseHeader('oc-jobstatus-location')).then(function(data) {
var obj = JSON.parse(data);
if (obj.status === 'finished') {
doneDeferred.resolve(status, response);
}
if (obj.status === 'error') {
OC.Notification.show(obj.errorMessage);
doneDeferred.reject(status, response);
}
if (obj.status === 'started' || obj.status === 'initial') {
// call it again after some short delay
setTimeout(poll, 1000);
}
});
};
// start the polling
poll();
} else {
doneDeferred.resolve(status, response);
}
}).fail( function(status, response) {
doneDeferred.reject(status, response);
});
return doneDeferred.promise();
},
_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.stalled = false;
this.data.abort();
this.deleteUpload();
},
/**
* retry the upload
*/
retry: function() {
if (!this.data.stalled) {
console.log('Retrying upload ' + this.id);
this.data.stalled = true;
this.data.abort();
}
},
/**
* 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) {
if (response.errorThrown === 'timeout') {
return {
status: 0,
message: t('files', 'Upload timeout for file "{file}"', {file: this.getFileName()})
};
}
// 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 if (response.result) {
response = response.result;
} else if (response.jqXHR) {
if (response.jqXHR.status === 0 && response.jqXHR.statusText === 'error') {
// timeout (IE11)
return {
status: 0,
message: t('files', 'Upload timeout for file "{file}"', {file: this.getFileName()})
};
}
return {
status: response.jqXHR.status,
message: t('files', 'Unknown error "{error}" uploading file "{file}"', {error: response.jqXHR.statusText, file: this.getFileName()})
};
}
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;
}
};
/**
* keeps track of uploads in progress and implements callbacks for the conflicts dialog
* @namespace
*/
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,
/**
* Webdav client pointing at the root "dav" endpoint
*
* @type OC.Files.Client
*/
davClient: null,
/**
* Upload progressbar element
*
* @type Object
*/
$uploadprogressbar: null,
/**
* @type int
*/
_uploadStallTimeout: 60,
/**
* 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);
},
/**
* 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;
// once the promise ends, we'll remove it from the this._knownDirs
promise.always(function() {
delete self._knownDirs[fullPath];
});
// 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.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, upload) {
upload.abort();
upload.aborted = true;
});
this.clear();
},
/**
* Clear uploads
*/
clear: function() {
var remainingUploads = {};
_.each(this._uploads, function(upload, key) {
if (!upload.isDone && !upload.aborted) {
remainingUploads[key] = upload;
}
});
this._uploads = remainingUploads;
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.show(t('files', 'Upload cancelled.'), {timeout : 7, type: 'error'});
}, 500),
/**
* Checks the currently known uploads.
* returns true if any hxr has the state 'pending'
* @returns {boolean}
*/
isProcessing:function() {
var count = 0;
jQuery.each(this._uploads, function(i, upload) {
if (upload.isPending()) {
count++;
}
});
return count > 0;
},
/**
* callback for the conflicts dialog
*/
onCancel:function() {
this.cancelUploads();
},
/**
* callback for the conflicts dialog
* calls onSkip, onReplace or onAutorename for each conflict
* @param {object} conflicts - list of conflict elements
*/
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 selected -> skip
// when none selected -> skip
self.onSkip(conflict.data('data'));
}
});
},
/**
* handle skipping an upload
* @param {OC.FileUpload} upload
*/
onSkip:function(upload) {
this.log('skip', null, upload);
upload.deleteUpload();
},
/**
* handle replacing a file on the server with an uploaded file
* @param {FileUpload} data
*/
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} upload
*/
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]);
},
_trace:false, //TODO implement log handler for JS per class?
log:function(caption, e, data) {
if (this._trace) {
console.log(caption);
console.log(data);
}
},
/**
* checks the list of existing files prior to uploading and shows a simple dialog to choose
* skip all, replace all or choose which files to keep
*
* @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
*/
checkExistingFiles: function (selection, callbacks) {
var self = this;
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, let backend handle this
return true;
}
if (!fileList) {
// no list to check against
return true;
}
var currentDirectory = fileList.getCurrentDirectory();
var targetFolder = upload.getTargetFolder();
// let backend handle the conflict check if files are dragged into another folder
if (targetFolder && currentDirectory && currentDirectory !== targetFolder) {
return true;
}
var fileInfo = fileList.findFile(self.sanitizeFileName(file.name));
if (fileInfo) {
var sharePermission = parseInt($("#sharePermission").val());
if (sharePermission === (OC.PERMISSION_READ | OC.PERMISSION_CREATE)) {
OC.Notification.show(t('files', 'The file {file} already exists', {file: fileInfo.name}), {type: 'error'});
return false;
}
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);
},
/**
* Return the sanitized file name.
*
* @return {String} file name
*/
sanitizeFileName: function(fileName) {
return fileName.trim();
},
_hideProgressBar: function() {
var self = this;
window.clearInterval(this._progressBarInterval);
$('#uploadprogresswrapper .stop').fadeOut();
this.$uploadprogressbar.fadeOut(function() {
self.$uploadEl.trigger(new $.Event('resized'));
});
},
_showProgressBar: function() {
this.$uploadprogressbar.fadeIn();
this.$uploadEl.trigger(new $.Event('resized'));
if (this._progressBarInterval) {
window.clearInterval(this._progressBarInterval);
}
this._progressBarInterval = window.setInterval(_.bind(this._updateProgressBar, this), 1000);
this._lastProgress = 0;
},
_updateProgressBar: function() {
var progress = parseInt(this.$uploadprogressbar.attr('data-loaded'), 10);
var total = parseInt(this.$uploadprogressbar.attr('data-total'), 10);
if (progress !== this._lastProgress) {
this._lastProgress = progress;
this._lastProgressTime = new Date().getTime();
} else {
if (progress >= total) {
// change message if we stalled at 100%
this.$uploadprogressbar.find('.label .desktop').text(t('files', 'Processing files...'));
}
if (new Date().getTime() - this._lastProgressTime >= this._uploadStallTimeout * 1000 ) {
// TODO: move to "fileuploadprogress" event instead and use data.uploadedBytes
// stalling needs to be checked here because the file upload no longer triggers events
// restart upload
this.log('progress stalled'); // retry chunk (and prevent IE from dying)
_.each(this._uploads, function(upload) {
// FIXME: harden by only retry pending, not the finished ones
upload.retry();
});
}
}
},
/**
* 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
* @param {String|function} [options.url] optional target url or function
*/
init: function($uploadEl, options) {
var self = this;
options = options || {};
this._uploads = {};
this._knownDirs = {};
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()
});
if (options.url) {
this.url = options.url;
}
if (options.uploadStallTimeout) {
this._uploadStallTimeout = options.uploadStallTimeout;
}
$uploadEl = $($uploadEl);
this.$uploadEl = $uploadEl;
this.$uploadprogressbar = $('#uploadprogressbar');
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,
maxRetries: options.uploadStallRetries || 3,
retryTimeout: 500,
//singleFileUploads is on by default, so the data.files array will always have length 1
/**
* on first add of every selection
* - 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);
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,
// 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 {
var reader = new FileReader();
reader.readAsBinaryString(file);
} catch (NS_ERROR_FILE_ACCESS_DENIED) {
//file is a directory
dirUploadFailure = true;
}
if (dirUploadFailure) {
data.textStatus = 'dirorzero';
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';
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();
});
}
};
_.each(selection.uploads, function(upload) {
self.trigger('beforeadd', upload);
});
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);
if (upload && upload.data && upload.data.stalled) {
self.log('retry', e, upload);
// jQuery Widget Factory uses "namespace-widgetname" since version 1.10.0:
var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'),
retries = upload.data.retries || 0,
retry = function () {
var uid = OC.getCurrentUser().uid;
upload.uploader.davClient.getFolderContents(
'uploads/' + uid + '/' + upload.getId()
)
.done(function (status, files) {
data.uploadedBytes = 0;
_.each(files, function(file) {
// only count numeric file names to omit .file and .file.zsync
if (!isNaN(parseFloat(file.name))
&& isFinite(file.name)
// only count full chunks
&& file.size === fu.options.maxChunkSize
) {
data.uploadedBytes += file.size;
}
});
// clear the previous data:
upload.data.stalled = false;
data.data = null;
// overwrite chunk
delete data.headers['If-None-Match'];
data.submit();
})
.fail(function (status, ex) {
self.log('failed to retry', status, ex);
fu._trigger('fail', e, data);
});
};
if (upload && upload.data && upload.data.stalled &&
data.uploadedBytes < data.files[0].size &&
retries < fu.options.maxRetries) {
retries += 1;
upload.data.retries = retries;
window.setTimeout(retry, retries * fu.options.retryTimeout);
return;
}
fu.prototype
.options.fail.call(this, e, data);
return;
}
var status = null;
if (upload) {
status = upload.getResponseStatus();
}
self.log('fail', e, upload);
self._hideProgressBar();
if (data.textStatus === 'abort') {
self.showUploadCancelMessage();
} else if (status === 412) {
// file already exists
self.showConflict(upload);
} else if (status === 403) {
// permission denied
var response = upload.getResponse();
message = response.message;
// If the message comes from a storage wrapper exception it should be already
// translated. Otherwise we have a default exception message and translate it here
if (message === '' || message === 'You don’t have permission to upload or create files here') {
message = t('files', 'You don’t have permission to upload or create files here');
}
OC.Notification.show(message, {type: 'error'});
} else if (status === 404) {
// target folder does not exist any more
var dir = upload.getFullPath();
if (dir && dir !== '/') {
OC.Notification.show(t('files', 'Target folder "{dir}" does not exist any more', {dir: dir}), {type: 'error'});
} else {
OC.Notification.show(t('files', 'Target folder does not exist any more'), {type: 'error'});
}
self.cancelUploads();
} else if (status === 423) {
// file is locked
OC.Notification.show(t('files', 'The file {file} is currently locked, please try again later', {file: upload.getFileName()}), {type: 'error'});
} 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 = t('files', 'Failed to upload the file "{fileName}": {error}', {fileName: upload.getFileName(), error: response.message});
}
OC.Notification.show(message || data.errorThrown, {type: 'error'});
}
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);
upload.isDone = true;
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);
}
};
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 bufferSize = 20;
var buffer = [];
var bufferIndex = 0;
var bufferTotal = 0;
var filledBufferSize = 0;
for(var i = 0; i < bufferSize;i++){
buffer[i] = 0;
}
// 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);
$('#uploadprogresswrapper .stop').show();
$('#uploadprogresswrapper .label').show();
self.$uploadprogressbar.progressbar({value: 0});
self.$uploadprogressbar.find('.ui-progressbar-value').
html('<em class="label inner"><span class="desktop">'
+ t('files', 'Uploading...')
+ '</span><span class="mobile">'
+ t('files', '...')
+ '</span></em>');
self.$uploadprogressbar.tooltip();
self._showProgressBar();
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 remainingBits = (data.total - data.loaded) * 8;
var remainingSeconds = remainingBits / data.bitrate;
//Take the average remaining seconds of the last bufferSize events
//to prevent fluctuation and provide a smooth experience
if (isFinite(remainingSeconds) && remainingSeconds >= 0) {
bufferTotal = bufferTotal - (buffer[bufferIndex]) + remainingSeconds;
buffer[bufferIndex] = remainingSeconds;
bufferIndex = (bufferIndex + 1) % bufferSize;
if (filledBufferSize < bufferSize) {
filledBufferSize++;
}
}
if (!oc_appconfig.files.hide_upload_estimation) {
var smoothRemainingSeconds = (bufferTotal / filledBufferSize);
var h = moment.duration(smoothRemainingSeconds, "seconds").humanize();
self.$uploadprogressbar.find('.label .mobile').text(h);
self.$uploadprogressbar.find('.label .desktop').text(h);
}
self.$uploadprogressbar.attr('data-loaded', data.loaded);
self.$uploadprogressbar.attr('data-total', data.total);
self.$uploadprogressbar.attr('original-title',
t('files', '{loadedSize} of {totalSize} ({bitrate})' , {
loadedSize: humanFileSize(data.loaded),
totalSize: humanFileSize(data.total),
bitrate: humanFileSize(data.bitrate) + '/s'
})
);
self.$uploadprogressbar.progressbar('value', progress);
self.trigger('progressall', e, data);
});
fileupload.on('fileuploadstop', function(e, data) {
self.log('progress handle fileuploadstop', e, data);
self.clear();
self.trigger('stop', e, data);
});
fileupload.on('fileuploadfail', function(e, data) {
self.log('progress handle fileuploadfail', e, data);
//if user pressed cancel hide upload progress bar and cancel button
if (data.errorThrown === 'abort') {
self._hideProgressBar();
}
self.trigger('fail', e, data);
});
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' +
'/' + encodeURIComponent(OC.getCurrentUser().uid) +
'/' + encodeURIComponent(upload.getId()) +
'/' + encodeURIComponent(chunkId);
delete data.contentRange;
delete data.headers['Content-Range'];
// reset retries
upload.data.retries = 0;
});
fileupload.on('fileuploadchunkdone', function(e, data) {
$(data.xhr().upload).unbind('progress');
});
fileupload.on('fileuploaddone', function(e, data) {
var upload = self.getUpload(data);
upload.done().then(function() {
self.trigger('done', e, upload);
// defer because sometimes the current upload is still in pending
// state but frees itself afterwards
_.defer(function() {
// don't hide if there are more files to process
if (!self.isProcessing()) {
self._hideProgressBar();
}
});
}).fail(function(status, response) {
var message = response.message;
self._hideProgressBar();
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);
});
}
}
// warn user not to leave the page while upload is in progress
$(window).on('beforeunload', function(e) {
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) {
this.$uploadEl.attr('multiple', 'multiple');
}
return this.fileUploadParam;
}
}, OC.Backbone.Events);