From e129f6845b8ec728455737e46cda872cc45f50c2 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 6 Mar 2014 12:50:18 +0100 Subject: [PATCH 1/3] Added testArea for JS Unit tests Added a testArea that is outside the viewport but still considered as visible from the jQuery code. The testArea is useful when: - tested code tries to access DOM elements using global ids - tested code is requiring some UI elements to be visible to activate themselves --- core/js/tests/specHelper.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js index b119324058..48dcb97ef7 100644 --- a/core/js/tests/specHelper.js +++ b/core/js/tests/specHelper.js @@ -68,9 +68,14 @@ window.oc_defaults = {}; // global setup for all tests (function setupTests() { var fakeServer = null, + $testArea = null, routesRequestStub; beforeEach(function() { + // test area for elements that need absolute selector access or measure widths/heights + // which wouldn't work for detached or hidden elements + $testArea = $('
'); + $('body').append($testArea); // enforce fake XHR, tests should not depend on the server and // must use fake responses for expected calls fakeServer = sinon.fakeServer.create(); @@ -101,6 +106,8 @@ window.oc_defaults = {}; // uncomment this to log requests // console.log(window.fakeServer.requests); fakeServer.restore(); + + $testArea.remove(); }); })(); From 07f78c824833bfcb3233f881286dc625d81e44f6 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 6 Mar 2014 13:49:57 +0100 Subject: [PATCH 2/3] Added unit tests for "add()" method for file upload - Added OC.Upload.init() to make the code testable - Added unit tests for the add() method of the uploader with some error cases --- apps/files/js/file-upload.js | 1087 +++++++++++++------------ apps/files/tests/js/fileUploadSpec.js | 127 +++ 2 files changed, 673 insertions(+), 541 deletions(-) create mode 100644 apps/files/tests/js/fileUploadSpec.js diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index aa85644cef..b9c4dc941f 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -177,549 +177,554 @@ OC.Upload = { checkExistingFiles: function (selection, callbacks) { // TODO check filelist before uploading and show dialog on conflicts, use callbacks callbacks.onNoConflicts(selection); + }, + + init: function() { + if ( $('#file_upload_start').exists() ) { + + var file_upload_param = { + dropZone: $('#content'), // 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) { + OC.Upload.log('add', e, data); + var that = $(this); + + // 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 + }; + } + var selection = data.originalFiles.selection; + + // add uploads + if ( selection.uploads.length < selection.filesToUpload ) { + // remember upload + selection.uploads.push(data); + } + + //examine file + var file = data.files[0]; + try { + // FIXME: not so elegant... need to refactor that method to return a value + Files.isFileNameValid(file.name, FileList.getCurrentDirectory()); + } + catch (errorMessage) { + data.textStatus = 'invalidcharacters'; + data.errorThrown = errorMessage; + } + + if (file.type === '' && file.size === 4096) { + data.textStatus = 'dirorzero'; + data.errorThrown = t('files', 'Unable to upload {filename} as it is a directory or has 0 bytes', + {filename: file.name} + ); + } + + // add size + selection.totalBytes += file.size; + + // check PHP upload limit + if (selection.totalBytes > $('#upload_limit').val()) { + data.textStatus = 'sizeexceedlimit'; + data.errorThrown = t('files', 'Total file size {size1} exceeds upload limit {size2}', { + 'size1': humanFileSize(selection.totalBytes), + 'size2': humanFileSize($('#upload_limit').val()) + }); + } + + // check free space + if (selection.totalBytes > $('#free_space').val()) { + 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 + 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) { + $.each(selection.uploads, function(i, upload) { + upload.submit(); + }); + }, + 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(); + }); + } + }; + + OC.Upload.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) { + OC.Upload.log('start', e, null); + }, + submit: function(e, data) { + OC.Upload.rememberUpload(data); + if ( ! data.formData ) { + // noone set update parameters, we set the minimum + data.formData = { + requesttoken: oc_requesttoken, + dir: $('#dir').val() + }; + } + }, + fail: function(e, data) { + OC.Upload.log('fail', e, data); + if (typeof data.textStatus !== 'undefined' && data.textStatus !== 'success' ) { + if (data.textStatus === 'abort') { + OC.Notification.show(t('files', 'Upload cancelled.')); + } else { + // HTTP connection problem + OC.Notification.show(data.errorThrown); + 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(); + } + } + } + //hide notification after 10 sec + setTimeout(function() { + OC.Notification.hide(); + }, 10000); + } + 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=$.parseJSON(response); + + delete data.jqXHR; + + if (result.status === 'error' && result.data && result.data.message){ + data.textStatus = 'servererror'; + data.errorThrown = result.data.message; + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + fu._trigger('fail', e, data); + } else if (typeof result[0] === 'undefined') { + data.textStatus = 'servererror'; + data.errorThrown = t('files', 'Could not get result from server.'); + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + fu._trigger('fail', e, data); + } else if (result[0].status === 'existserror') { + //show "file already exists" dialog + var original = result[0]; + var replacement = data.files[0]; + var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + OC.dialogs.fileexists(data, original, replacement, OC.Upload, fu); + } 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 fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); + fu._trigger('fail', e, data); + } + }, + /** + * called after last upload + * @param {object} e + * @param {object} data + */ + stop: function(e, data) { + OC.Upload.log('stop', e, data); + } + }; + + // initialize jquery fileupload (blueimp) + var fileupload = $('#file_upload_start').fileupload(file_upload_param); + window.file_upload_param = fileupload; + + if (supportAjaxUploadWithProgress()) { + + // 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 input.stop').show(); + //} + }); + // add progress handlers + fileupload.on('fileuploadstart', function(e, data) { + OC.Upload.log('progress handle fileuploadstart', e, data); + $('#uploadprogresswrapper input.stop').show(); + $('#uploadprogressbar').progressbar({value:0}); + $('#uploadprogressbar').fadeIn(); + }); + fileupload.on('fileuploadprogress', function(e, data) { + OC.Upload.log('progress handle fileuploadprogress', e, data); + //TODO progressbar in row + }); + fileupload.on('fileuploadprogressall', function(e, data) { + OC.Upload.log('progress handle fileuploadprogressall', e, data); + var progress = (data.loaded / data.total) * 100; + $('#uploadprogressbar').progressbar('value', progress); + }); + fileupload.on('fileuploadstop', function(e, data) { + OC.Upload.log('progress handle fileuploadstop', e, data); + + $('#uploadprogresswrapper input.stop').fadeOut(); + $('#uploadprogressbar').fadeOut(); + Files.updateStorageStatistics(); + }); + fileupload.on('fileuploadfail', function(e, data) { + OC.Upload.log('progress handle fileuploadfail', e, data); + //if user pressed cancel hide upload progress bar and cancel button + if (data.errorThrown === 'abort') { + $('#uploadprogresswrapper input.stop').fadeOut(); + $('#uploadprogressbar').fadeOut(); + } + }); + + } else { + console.log('skipping file progress because your browser is broken'); + } + } + + $.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()) { + 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'); + } + + //if the breadcrumb is to long, start by replacing foldernames with '...' except for the current folder + var crumb=$('div.crumb').first(); + while($('div.controls').height() > 40 && crumb.next('div.crumb').length > 0) { + crumb.children('a').text('...'); + crumb = crumb.next('div.crumb'); + } + //if that isn't enough, start removing items from the breacrumb except for the current folder and it's parent + var crumb = $('div.crumb').first(); + var next = crumb.next('div.crumb'); + while($('div.controls').height()>40 && next.next('div.crumb').length > 0) { + crumb.remove(); + crumb = next; + next = crumb.next('div.crumb'); + } + //still not enough, start shorting down the current folder name + var crumb=$('div.crumb>a').last(); + while($('div.controls').height() > 40 && crumb.text().length > 6) { + var text=crumb.text(); + text = text.substr(0,text.length-6)+'...'; + crumb.text(text); + } + + $(document).click(function(ev) { + // do not close when clicking in the dropdown + if ($(ev.target).closest('#new').length){ + return; + } + $('#new>ul').hide(); + $('#new').removeClass('active'); + if ($('#new .error').length > 0) { + $('#new .error').tipsy('hide'); + } + $('#new li').each(function(i,element) { + if ($(element).children('p').length === 0) { + $(element).children('form').remove(); + $(element).append('

'+$(element).data('text')+'

'); + } + }); + }); + $('#new').click(function(event) { + event.stopPropagation(); + }); + $('#new>a').click(function() { + $('#new>ul').toggle(); + $('#new').toggleClass('active'); + }); + $('#new li').click(function() { + if ($(this).children('p').length === 0) { + return; + } + + $('#new .error').tipsy('hide'); + + $('#new li').each(function(i,element) { + if ($(element).children('p').length === 0) { + $(element).children('form').remove(); + $(element).append('

'+$(element).data('text')+'

'); + } + }); + + var type=$(this).data('type'); + var text=$(this).children('p').text(); + $(this).data('text',text); + $(this).children('p').remove(); + + // add input field + var form = $('
'); + var input = $(''); + var newName = $(this).attr('data-newname') || ''; + if (newName) { + input.val(newName); + } + form.append(input); + $(this).append(form); + var lastPos; + var checkInput = function () { + var filename = input.val(); + if (type === 'web' && filename.length === 0) { + throw t('files', 'URL cannot be empty'); + } else if (type !== 'web' && !Files.isFileNameValid(filename)) { + // Files.isFileNameValid(filename) throws an exception itself + } else if (FileList.getCurrentDirectory() === '/' && filename.toLowerCase() === 'shared') { + throw t('files', 'In the home folder \'Shared\' is a reserved filename'); + } else if (FileList.inList(filename)) { + throw t('files', '{new_name} already exists', {new_name: filename}); + } else { + return true; + } + }; + + // verify filename on typing + input.keyup(function(event) { + try { + checkInput(); + input.tipsy('hide'); + input.removeClass('error'); + } catch (error) { + input.attr('title', error); + input.tipsy({gravity: 'w', trigger: 'manual'}); + input.tipsy('show'); + input.addClass('error'); + } + }); + + input.focus(); + // pre select name up to the extension + lastPos = newName.lastIndexOf('.'); + if (lastPos === -1) { + lastPos = newName.length; + } + input.selectRange(0, lastPos); + form.submit(function(event) { + event.stopPropagation(); + event.preventDefault(); + try { + checkInput(); + var newname = input.val(); + if (FileList.lastAction) { + FileList.lastAction(); + } + var name = getUniqueName(newname); + if (newname !== name) { + FileList.checkName(name, newname, true); + var hidden = true; + } else { + var hidden = false; + } + switch(type) { + case 'file': + $.post( + OC.filePath('files', 'ajax', 'newfile.php'), + {dir:$('#dir').val(), filename:name}, + function(result) { + if (result.status === 'success') { + var date = new Date(); + // TODO: ideally addFile should be able to receive + // all attributes and set them automatically, + // and also auto-load the preview + var tr = FileList.addFile(name, 0, date, false, hidden); + tr.attr('data-size', result.data.size); + tr.attr('data-mime', result.data.mime); + tr.attr('data-id', result.data.id); + tr.attr('data-etag', result.data.etag); + tr.find('.filesize').text(humanFileSize(result.data.size)); + var path = getPathForPreview(name); + Files.lazyLoadPreview(path, result.data.mime, function(previewpath) { + tr.find('td.filename').attr('style','background-image:url('+previewpath+')'); + }, null, null, result.data.etag); + FileActions.display(tr.find('td.filename'), true); + } else { + OC.dialogs.alert(result.data.message, t('core', 'Could not create file')); + } + } + ); + break; + case 'folder': + $.post( + OC.filePath('files','ajax','newfolder.php'), + {dir:$('#dir').val(), foldername:name}, + function(result) { + if (result.status === 'success') { + var date=new Date(); + FileList.addDir(name, 0, date, hidden); + var tr = FileList.findFileEl(name); + tr.attr('data-id', result.data.id); + } else { + OC.dialogs.alert(result.data.message, t('core', 'Could not create folder')); + } + } + ); + break; + case 'web': + if (name.substr(0,8) !== 'https://' && name.substr(0,7) !== 'http://') { + name = 'http://' + name; + } + var localName=name; + if (localName.substr(localName.length-1,1)==='/') {//strip / + localName=localName.substr(0,localName.length-1); + } + if (localName.indexOf('/')) {//use last part of url + localName=localName.split('/').pop(); + } else { //or the domain + localName=(localName.match(/:\/\/(.[^\/]+)/)[1]).replace('www.',''); + } + localName = getUniqueName(localName); + //IE < 10 does not fire the necessary events for the progress bar. + if ($('html.lte9').length === 0) { + $('#uploadprogressbar').progressbar({value:0}); + $('#uploadprogressbar').fadeIn(); + } + + var eventSource=new OC.EventSource(OC.filePath('files','ajax','newfile.php'),{dir:$('#dir').val(),source:name,filename:localName}); + eventSource.listen('progress',function(progress) { + //IE < 10 does not fire the necessary events for the progress bar. + if ($('html.lte9').length === 0) { + $('#uploadprogressbar').progressbar('value',progress); + } + }); + eventSource.listen('success',function(data) { + var mime = data.mime; + var size = data.size; + var id = data.id; + $('#uploadprogressbar').fadeOut(); + var date = new Date(); + FileList.addFile(localName, size, date, false, hidden); + var tr = FileList.findFileEl(localName); + tr.data('mime', mime).data('id', id); + tr.attr('data-id', id); + var path = $('#dir').val()+'/'+localName; + Files.lazyLoadPreview(path, mime, function(previewpath) { + tr.find('td.filename').attr('style', 'background-image:url('+previewpath+')'); + }, null, null, data.etag); + FileActions.display(tr.find('td.filename'), true); + }); + eventSource.listen('error',function(error) { + $('#uploadprogressbar').fadeOut(); + var message = (error && error.message) || t('core', 'Error fetching URL'); + OC.Notification.show(message); + //hide notification after 10 sec + setTimeout(function() { + OC.Notification.hide(); + }, 10000); + }); + break; + } + var li=form.parent(); + form.remove(); + /* workaround for IE 9&10 click event trap, 2 lines: */ + $('input').first().focus(); + $('#content').focus(); + li.append('

'+li.data('text')+'

'); + $('#new>a').click(); + } catch (error) { + input.attr('title', error); + input.tipsy({gravity: 'w', trigger: 'manual'}); + input.tipsy('show'); + input.addClass('error'); + } + }); + }); + window.file_upload_param = file_upload_param; + return file_upload_param; } }; $(document).ready(function() { - - if ( $('#file_upload_start').exists() ) { - - var file_upload_param = { - dropZone: $('#content'), // 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) { - OC.Upload.log('add', e, data); - var that = $(this); - - // 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 - }; - } - var selection = data.originalFiles.selection; - - // add uploads - if ( selection.uploads.length < selection.filesToUpload ) { - // remember upload - selection.uploads.push(data); - } - - //examine file - var file = data.files[0]; - try { - // FIXME: not so elegant... need to refactor that method to return a value - Files.isFileNameValid(file.name, FileList.getCurrentDirectory()); - } - catch (errorMessage) { - data.textStatus = 'invalidcharacters'; - data.errorThrown = errorMessage; - } - - if (file.type === '' && file.size === 4096) { - data.textStatus = 'dirorzero'; - data.errorThrown = t('files', 'Unable to upload {filename} as it is a directory or has 0 bytes', - {filename: file.name} - ); - } - - // add size - selection.totalBytes += file.size; - - // check PHP upload limit - if (selection.totalBytes > $('#upload_limit').val()) { - data.textStatus = 'sizeexceedlimit'; - data.errorThrown = t('files', 'Total file size {size1} exceeds upload limit {size2}', { - 'size1': humanFileSize(selection.totalBytes), - 'size2': humanFileSize($('#upload_limit').val()) - }); - } - - // check free space - if (selection.totalBytes > $('#free_space').val()) { - 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 - 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) { - $.each(selection.uploads, function(i, upload) { - upload.submit(); - }); - }, - 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(); - }); - } - }; - - OC.Upload.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) { - OC.Upload.log('start', e, null); - }, - submit: function(e, data) { - OC.Upload.rememberUpload(data); - if ( ! data.formData ) { - // noone set update parameters, we set the minimum - data.formData = { - requesttoken: oc_requesttoken, - dir: $('#dir').val() - }; - } - }, - fail: function(e, data) { - OC.Upload.log('fail', e, data); - if (typeof data.textStatus !== 'undefined' && data.textStatus !== 'success' ) { - if (data.textStatus === 'abort') { - OC.Notification.show(t('files', 'Upload cancelled.')); - } else { - // HTTP connection problem - OC.Notification.show(data.errorThrown); - 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(); - } - } - } - //hide notification after 10 sec - setTimeout(function() { - OC.Notification.hide(); - }, 10000); - } - 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=$.parseJSON(response); - - delete data.jqXHR; - - if (result.status === 'error' && result.data && result.data.message){ - data.textStatus = 'servererror'; - data.errorThrown = result.data.message; - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - fu._trigger('fail', e, data); - } else if (typeof result[0] === 'undefined') { - data.textStatus = 'servererror'; - data.errorThrown = t('files', 'Could not get result from server.'); - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - fu._trigger('fail', e, data); - } else if (result[0].status === 'existserror') { - //show "file already exists" dialog - var original = result[0]; - var replacement = data.files[0]; - var fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - OC.dialogs.fileexists(data, original, replacement, OC.Upload, fu); - } 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 fu = $(this).data('blueimp-fileupload') || $(this).data('fileupload'); - fu._trigger('fail', e, data); - } - }, - /** - * called after last upload - * @param {object} e - * @param {object} data - */ - stop: function(e, data) { - OC.Upload.log('stop', e, data); - } - }; - - // initialize jquery fileupload (blueimp) - var fileupload = $('#file_upload_start').fileupload(file_upload_param); - window.file_upload_param = fileupload; - - if (supportAjaxUploadWithProgress()) { - - // 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 input.stop').show(); - //} - }); - // add progress handlers - fileupload.on('fileuploadstart', function(e, data) { - OC.Upload.log('progress handle fileuploadstart', e, data); - $('#uploadprogresswrapper input.stop').show(); - $('#uploadprogressbar').progressbar({value:0}); - $('#uploadprogressbar').fadeIn(); - }); - fileupload.on('fileuploadprogress', function(e, data) { - OC.Upload.log('progress handle fileuploadprogress', e, data); - //TODO progressbar in row - }); - fileupload.on('fileuploadprogressall', function(e, data) { - OC.Upload.log('progress handle fileuploadprogressall', e, data); - var progress = (data.loaded / data.total) * 100; - $('#uploadprogressbar').progressbar('value', progress); - }); - fileupload.on('fileuploadstop', function(e, data) { - OC.Upload.log('progress handle fileuploadstop', e, data); - - $('#uploadprogresswrapper input.stop').fadeOut(); - $('#uploadprogressbar').fadeOut(); - Files.updateStorageStatistics(); - }); - fileupload.on('fileuploadfail', function(e, data) { - OC.Upload.log('progress handle fileuploadfail', e, data); - //if user pressed cancel hide upload progress bar and cancel button - if (data.errorThrown === 'abort') { - $('#uploadprogresswrapper input.stop').fadeOut(); - $('#uploadprogressbar').fadeOut(); - } - }); - - } else { - console.log('skipping file progress because your browser is broken'); - } - } - - $.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()) { - 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'); - } - - //if the breadcrumb is to long, start by replacing foldernames with '...' except for the current folder - var crumb=$('div.crumb').first(); - while($('div.controls').height() > 40 && crumb.next('div.crumb').length > 0) { - crumb.children('a').text('...'); - crumb = crumb.next('div.crumb'); - } - //if that isn't enough, start removing items from the breacrumb except for the current folder and it's parent - var crumb = $('div.crumb').first(); - var next = crumb.next('div.crumb'); - while($('div.controls').height()>40 && next.next('div.crumb').length > 0) { - crumb.remove(); - crumb = next; - next = crumb.next('div.crumb'); - } - //still not enough, start shorting down the current folder name - var crumb=$('div.crumb>a').last(); - while($('div.controls').height() > 40 && crumb.text().length > 6) { - var text=crumb.text(); - text = text.substr(0,text.length-6)+'...'; - crumb.text(text); - } - - $(document).click(function(ev) { - // do not close when clicking in the dropdown - if ($(ev.target).closest('#new').length){ - return; - } - $('#new>ul').hide(); - $('#new').removeClass('active'); - if ($('#new .error').length > 0) { - $('#new .error').tipsy('hide'); - } - $('#new li').each(function(i,element) { - if ($(element).children('p').length === 0) { - $(element).children('form').remove(); - $(element).append('

'+$(element).data('text')+'

'); - } - }); - }); - $('#new').click(function(event) { - event.stopPropagation(); - }); - $('#new>a').click(function() { - $('#new>ul').toggle(); - $('#new').toggleClass('active'); - }); - $('#new li').click(function() { - if ($(this).children('p').length === 0) { - return; - } - - $('#new .error').tipsy('hide'); - - $('#new li').each(function(i,element) { - if ($(element).children('p').length === 0) { - $(element).children('form').remove(); - $(element).append('

'+$(element).data('text')+'

'); - } - }); - - var type=$(this).data('type'); - var text=$(this).children('p').text(); - $(this).data('text',text); - $(this).children('p').remove(); - - // add input field - var form = $('
'); - var input = $(''); - var newName = $(this).attr('data-newname') || ''; - if (newName) { - input.val(newName); - } - form.append(input); - $(this).append(form); - var lastPos; - var checkInput = function () { - var filename = input.val(); - if (type === 'web' && filename.length === 0) { - throw t('files', 'URL cannot be empty'); - } else if (type !== 'web' && !Files.isFileNameValid(filename)) { - // Files.isFileNameValid(filename) throws an exception itself - } else if (FileList.getCurrentDirectory() === '/' && filename.toLowerCase() === 'shared') { - throw t('files', 'In the home folder \'Shared\' is a reserved filename'); - } else if (FileList.inList(filename)) { - throw t('files', '{new_name} already exists', {new_name: filename}); - } else { - return true; - } - }; - - // verify filename on typing - input.keyup(function(event) { - try { - checkInput(); - input.tipsy('hide'); - input.removeClass('error'); - } catch (error) { - input.attr('title', error); - input.tipsy({gravity: 'w', trigger: 'manual'}); - input.tipsy('show'); - input.addClass('error'); - } - }); - - input.focus(); - // pre select name up to the extension - lastPos = newName.lastIndexOf('.'); - if (lastPos === -1) { - lastPos = newName.length; - } - input.selectRange(0, lastPos); - form.submit(function(event) { - event.stopPropagation(); - event.preventDefault(); - try { - checkInput(); - var newname = input.val(); - if (FileList.lastAction) { - FileList.lastAction(); - } - var name = getUniqueName(newname); - if (newname !== name) { - FileList.checkName(name, newname, true); - var hidden = true; - } else { - var hidden = false; - } - switch(type) { - case 'file': - $.post( - OC.filePath('files', 'ajax', 'newfile.php'), - {dir:$('#dir').val(), filename:name}, - function(result) { - if (result.status === 'success') { - var date = new Date(); - // TODO: ideally addFile should be able to receive - // all attributes and set them automatically, - // and also auto-load the preview - var tr = FileList.addFile(name, 0, date, false, hidden); - tr.attr('data-size', result.data.size); - tr.attr('data-mime', result.data.mime); - tr.attr('data-id', result.data.id); - tr.attr('data-etag', result.data.etag); - tr.find('.filesize').text(humanFileSize(result.data.size)); - var path = getPathForPreview(name); - Files.lazyLoadPreview(path, result.data.mime, function(previewpath) { - tr.find('td.filename').attr('style','background-image:url('+previewpath+')'); - }, null, null, result.data.etag); - FileActions.display(tr.find('td.filename'), true); - } else { - OC.dialogs.alert(result.data.message, t('core', 'Could not create file')); - } - } - ); - break; - case 'folder': - $.post( - OC.filePath('files','ajax','newfolder.php'), - {dir:$('#dir').val(), foldername:name}, - function(result) { - if (result.status === 'success') { - var date=new Date(); - FileList.addDir(name, 0, date, hidden); - var tr = FileList.findFileEl(name); - tr.attr('data-id', result.data.id); - } else { - OC.dialogs.alert(result.data.message, t('core', 'Could not create folder')); - } - } - ); - break; - case 'web': - if (name.substr(0,8) !== 'https://' && name.substr(0,7) !== 'http://') { - name = 'http://' + name; - } - var localName=name; - if (localName.substr(localName.length-1,1)==='/') {//strip / - localName=localName.substr(0,localName.length-1); - } - if (localName.indexOf('/')) {//use last part of url - localName=localName.split('/').pop(); - } else { //or the domain - localName=(localName.match(/:\/\/(.[^\/]+)/)[1]).replace('www.',''); - } - localName = getUniqueName(localName); - //IE < 10 does not fire the necessary events for the progress bar. - if ($('html.lte9').length === 0) { - $('#uploadprogressbar').progressbar({value:0}); - $('#uploadprogressbar').fadeIn(); - } - - var eventSource=new OC.EventSource(OC.filePath('files','ajax','newfile.php'),{dir:$('#dir').val(),source:name,filename:localName}); - eventSource.listen('progress',function(progress) { - //IE < 10 does not fire the necessary events for the progress bar. - if ($('html.lte9').length === 0) { - $('#uploadprogressbar').progressbar('value',progress); - } - }); - eventSource.listen('success',function(data) { - var mime = data.mime; - var size = data.size; - var id = data.id; - $('#uploadprogressbar').fadeOut(); - var date = new Date(); - FileList.addFile(localName, size, date, false, hidden); - var tr = FileList.findFileEl(localName); - tr.data('mime', mime).data('id', id); - tr.attr('data-id', id); - var path = $('#dir').val()+'/'+localName; - Files.lazyLoadPreview(path, mime, function(previewpath) { - tr.find('td.filename').attr('style', 'background-image:url('+previewpath+')'); - }, null, null, data.etag); - FileActions.display(tr.find('td.filename'), true); - }); - eventSource.listen('error',function(error) { - $('#uploadprogressbar').fadeOut(); - var message = (error && error.message) || t('core', 'Error fetching URL'); - OC.Notification.show(message); - //hide notification after 10 sec - setTimeout(function() { - OC.Notification.hide(); - }, 10000); - }); - break; - } - var li=form.parent(); - form.remove(); - /* workaround for IE 9&10 click event trap, 2 lines: */ - $('input').first().focus(); - $('#content').focus(); - li.append('

'+li.data('text')+'

'); - $('#new>a').click(); - } catch (error) { - input.attr('title', error); - input.tipsy({gravity: 'w', trigger: 'manual'}); - input.tipsy('show'); - input.addClass('error'); - } - }); - }); - window.file_upload_param = file_upload_param; + OC.Upload.init(); }); + diff --git a/apps/files/tests/js/fileUploadSpec.js b/apps/files/tests/js/fileUploadSpec.js new file mode 100644 index 0000000000..2b4341ef1c --- /dev/null +++ b/apps/files/tests/js/fileUploadSpec.js @@ -0,0 +1,127 @@ +/** +* ownCloud +* +* @author Vincent Petry +* @copyright 2014 Vincent Petry +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE +* License as published by the Free Software Foundation; either +* version 3 of the License, or any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU AFFERO GENERAL PUBLIC LICENSE for more details. +* +* You should have received a copy of the GNU Affero General Public +* License along with this library. If not, see . +* +*/ + +/* global OC */ +describe('OC.Upload tests', function() { + var $dummyUploader; + var testFile; + + beforeEach(function() { + testFile = { + name: 'test.txt', + size: 5000, // 5 KB + type: 'text/plain', + lastModifiedDate: new Date() + }; + // need a dummy button because file-upload checks on it + $('#testArea').append( + '' + + '' + // 10 MB + '' // 50 MB + ); + $dummyUploader = $('#file_upload_start'); + }); + afterEach(function() { + delete window.file_upload_param; + $dummyUploader = undefined; + }); + describe('Adding files for upload', function() { + var params; + var failStub; + + beforeEach(function() { + params = OC.Upload.init(); + failStub = sinon.stub(); + $dummyUploader.on('fileuploadfail', failStub); + }); + afterEach(function() { + params = undefined; + failStub = undefined; + }); + + /** + * Add file for upload + * @param file file data + */ + function addFile(file) { + return params.add.call( + $dummyUploader[0], + {}, + { + originalFiles: {}, + files: [file] + }); + } + + it('adds file when size is below limits', function() { + var result = addFile(testFile); + expect(result).toEqual(true); + }); + it('adds file when free space is unknown', function() { + var result; + $('#free_space').val(-2); + + result = addFile(testFile); + + expect(result).toEqual(true); + expect(failStub.notCalled).toEqual(true); + }); + it('does not add file if it exceeds upload limit', function() { + var result; + $('#upload_limit').val(1000); + + result = addFile(testFile); + + expect(result).toEqual(false); + expect(failStub.calledOnce).toEqual(true); + expect(failStub.getCall(0).args[1].textStatus).toEqual('sizeexceedlimit'); + expect(failStub.getCall(0).args[1].errorThrown).toEqual( + 'Total file size 5 kB exceeds upload limit 1000 B' + ); + }); + it('does not add file if it exceeds free space', function() { + var result; + $('#free_space').val(1000); + + result = addFile(testFile); + + expect(result).toEqual(false); + expect(failStub.calledOnce).toEqual(true); + expect(failStub.getCall(0).args[1].textStatus).toEqual('notenoughspace'); + expect(failStub.getCall(0).args[1].errorThrown).toEqual( + 'Not enough free space, you are uploading 5 kB but only 1000 B is left' + ); + }); + it('does not add file if it has invalid characters', function() { + var result; + testFile.name = 'stars*stars.txt'; + + result = addFile(testFile); + + expect(result).toEqual(false); + expect(failStub.calledOnce).toEqual(true); + expect(failStub.getCall(0).args[1].textStatus).toEqual('invalidcharacters'); + expect(failStub.getCall(0).args[1].errorThrown.substr(0, 12)).toEqual( + 'Invalid name' + ); + }); + }); +}); From 0dcac65aa1eb14adc4d3693b6a2c99cbf6ea2425 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Thu, 6 Mar 2014 13:50:53 +0100 Subject: [PATCH 3/3] Fixed upload issue when free space is not known --- apps/files/js/file-upload.js | 4 +++- lib/private/helper.php | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/files/js/file-upload.js b/apps/files/js/file-upload.js index b9c4dc941f..b7b8f6fdeb 100644 --- a/apps/files/js/file-upload.js +++ b/apps/files/js/file-upload.js @@ -206,6 +206,7 @@ OC.Upload = { add: function(e, data) { OC.Upload.log('add', e, data); var that = $(this); + var freeSpace; // 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 @@ -261,7 +262,8 @@ OC.Upload = { } // check free space - if (selection.totalBytes > $('#free_space').val()) { + 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), diff --git a/lib/private/helper.php b/lib/private/helper.php index d8c4650f66..b9956d5ec1 100644 --- a/lib/private/helper.php +++ b/lib/private/helper.php @@ -839,7 +839,7 @@ class OC_Helper { * @return int number of bytes representing */ public static function maxUploadFilesize($dir, $freeSpace = null) { - if (is_null($freeSpace)){ + if (is_null($freeSpace) || $freeSpace < 0){ $freeSpace = self::freeSpace($dir); } return min($freeSpace, self::uploadLimit());