From c36014b46f39d71ec8cf68cc81a2651a378a2534 Mon Sep 17 00:00:00 2001 From: jendib Date: Tue, 3 Mar 2015 00:23:30 +0100 Subject: [PATCH] Ability to upload files without document (no OCR, no Lucene) + New resource to attach a document to a file and OCR/Lucene it --- .../sismics/docs/core/dao/jpa/FileDao.java | 22 +- .../src/main/resources/config.properties | 2 +- docs-web/src/dev/resources/config.properties | 2 +- .../docs/rest/resource/FileResource.java | 116 +++- docs-web/src/main/webapp/src/app/docs/app.js | 2 +- docs-web/src/main/webapp/src/index.html | 1 + .../webapp/src/lib/angular.file-upload.js | 602 ++++++++++++++++++ .../src/partial/docs/document.default.html | 14 +- .../src/partial/docs/document.view.html | 2 +- .../main/webapp/src/partial/share/share.html | 2 +- docs-web/src/prod/resources/config.properties | 2 +- .../sismics/docs/rest/TestFileResource.java | 42 ++ 12 files changed, 774 insertions(+), 35 deletions(-) create mode 100644 docs-web/src/main/webapp/src/lib/angular.file-upload.js diff --git a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java index 1aa007a5..97743f03 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java +++ b/docs-core/src/main/java/com/sismics/docs/core/dao/jpa/FileDao.java @@ -93,12 +93,32 @@ public class FileDao { q.setParameter("id", file.getId()); File fileFromDb = (File) q.getSingleResult(); - // Update the user + // Update the file fileFromDb.setContent(file.getContent()); return file; } + /** + * Update the document of a file. + * + * @param file File to update + * @return Updated file + */ + public File updateDocument(File file) { + EntityManager em = ThreadLocalContext.get().getEntityManager(); + + // Get the file + Query q = em.createQuery("select f from File f where f.id = :id and f.deleteDate is null"); + q.setParameter("id", file.getId()); + File fileFromDb = (File) q.getSingleResult(); + + // Update the file + fileFromDb.setDocumentId(file.getDocumentId()); + + return file; + } + /** * Gets a file by its ID. * diff --git a/docs-core/src/main/resources/config.properties b/docs-core/src/main/resources/config.properties index cff11a73..f4e9af41 100644 --- a/docs-core/src/main/resources/config.properties +++ b/docs-core/src/main/resources/config.properties @@ -1 +1 @@ -db.version=6 \ No newline at end of file +db.version=7 \ No newline at end of file diff --git a/docs-web/src/dev/resources/config.properties b/docs-web/src/dev/resources/config.properties index 6c2faf82..1bfb8fb0 100644 --- a/docs-web/src/dev/resources/config.properties +++ b/docs-web/src/dev/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=6 \ No newline at end of file +db.version=7 \ No newline at end of file diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java index 5ab4bd10..38696c11 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/FileResource.java @@ -33,6 +33,7 @@ import javax.ws.rs.core.StreamingOutput; import org.codehaus.jettison.json.JSONException; import org.codehaus.jettison.json.JSONObject; +import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; import com.sismics.docs.core.dao.jpa.DocumentDao; @@ -65,7 +66,7 @@ import com.sun.jersey.multipart.FormDataParam; @Path("/file") public class FileResource extends BaseResource { /** - * Add a file to a document. + * Add a file (with or without a document). * * @param documentId Document ID * @param fileBodyPart File to add @@ -83,20 +84,23 @@ public class FileResource extends BaseResource { } // Validate input data - ValidationUtil.validateRequired(documentId, "id"); ValidationUtil.validateRequired(fileBodyPart, "file"); - // Get the document - DocumentDao documentDao = new DocumentDao(); - FileDao fileDao = new FileDao(); + // Get the current user UserDao userDao = new UserDao(); - Document document; - User user; - try { - document = documentDao.getDocument(documentId, principal.getId()); - user = userDao.getById(principal.getId()); - } catch (NoResultException e) { - throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + User user = userDao.getById(principal.getId()); + + // Get the document + Document document = null; + if (Strings.isNullOrEmpty(documentId)) { + documentId = null; + } else { + DocumentDao documentDao = new DocumentDao(); + try { + document = documentDao.getDocument(documentId, principal.getId()); + } catch (NoResultException e) { + throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + } } // Keep unencrypted data in memory, because we will need it two times @@ -121,27 +125,32 @@ public class FileResource extends BaseResource { try { // Get files of this document + FileDao fileDao = new FileDao(); int order = 0; - for (File file : fileDao.getByDocumentId(documentId)) { - file.setOrder(order++); + if (documentId != null) { + for (File file : fileDao.getByDocumentId(documentId)) { + file.setOrder(order++); + } } // Create the file File file = new File(); file.setOrder(order); - file.setDocumentId(document.getId()); + file.setDocumentId(documentId); file.setMimeType(mimeType); String fileId = fileDao.create(file); // Save the file FileUtil.save(fileInputStream, file, user.getPrivateKey()); - // Raise a new file created event - FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); - fileCreatedAsyncEvent.setDocument(document); - fileCreatedAsyncEvent.setFile(file); - fileCreatedAsyncEvent.setInputStream(fileInputStream); - AppContext.getInstance().getAsyncEventBus().post(fileCreatedAsyncEvent); + // Raise a new file created event if we have a document + if (documentId != null) { + FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); + fileCreatedAsyncEvent.setDocument(document); + fileCreatedAsyncEvent.setFile(file); + fileCreatedAsyncEvent.setInputStream(fileInputStream); + AppContext.getInstance().getAsyncEventBus().post(fileCreatedAsyncEvent); + } // Always return ok JSONObject response = new JSONObject(); @@ -153,6 +162,71 @@ public class FileResource extends BaseResource { } } + /** + * Attach a file to a document. + * + * @param id File ID + * @return Response + * @throws JSONException + */ + @POST + @Path("{id: [a-z0-9\\-]+}") + @Produces(MediaType.APPLICATION_JSON) + public Response attach( + @PathParam("id") String id, + @FormParam("id") String documentId) throws JSONException { + if (!authenticate()) { + throw new ForbiddenClientException(); + } + + // Validate input data + ValidationUtil.validateRequired(documentId, "id"); + + // Get the current user + UserDao userDao = new UserDao(); + User user = userDao.getById(principal.getId()); + + // Get the document and the file + DocumentDao documentDao = new DocumentDao(); + FileDao fileDao = new FileDao(); + Document document; + File file; + try { + file = fileDao.getFile(id); + document = documentDao.getDocument(documentId, principal.getId()); + } catch (NoResultException e) { + throw new ClientException("DocumentNotFound", MessageFormat.format("Document not found: {0}", documentId)); + } + + // Check that the file is orphan + if (file.getDocumentId() != null) { + throw new ClientException("IllegalFile", MessageFormat.format("File not orphan: {0}", id)); + } + + // Update the file + file.setDocumentId(documentId); + fileDao.updateDocument(file); + + // Raise a new file created event (it wasn't sent during file creation) + try { + java.io.File storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), id).toFile(); + InputStream fileInputStream = new FileInputStream(storedfile); + final InputStream responseInputStream = EncryptionUtil.decryptInputStream(fileInputStream, user.getPrivateKey()); + FileCreatedAsyncEvent fileCreatedAsyncEvent = new FileCreatedAsyncEvent(); + fileCreatedAsyncEvent.setDocument(document); + fileCreatedAsyncEvent.setFile(file); + fileCreatedAsyncEvent.setInputStream(responseInputStream); + AppContext.getInstance().getAsyncEventBus().post(fileCreatedAsyncEvent); + } catch (Exception e) { + throw new ClientException("AttachError", "Error attaching file to document", e); + } + + // Always return ok + JSONObject response = new JSONObject(); + response.put("status", "ok"); + return Response.ok().entity(response).build(); + } + /** * Reorder files. * diff --git a/docs-web/src/main/webapp/src/app/docs/app.js b/docs-web/src/main/webapp/src/app/docs/app.js index 5cf5fc47..348478f7 100644 --- a/docs-web/src/main/webapp/src/app/docs/app.js +++ b/docs-web/src/main/webapp/src/app/docs/app.js @@ -6,7 +6,7 @@ angular.module('docs', // Dependencies ['ui.router', 'ui.route', 'ui.bootstrap', 'ui.keypress', 'ui.validate', 'dialog', - 'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module'] + 'ui.sortable', 'restangular', 'ngSanitize', 'ngTouch', 'colorpicker.module', 'angularFileUpload'] ) /** diff --git a/docs-web/src/main/webapp/src/index.html b/docs-web/src/main/webapp/src/index.html index bd871b5a..8d19dca3 100644 --- a/docs-web/src/main/webapp/src/index.html +++ b/docs-web/src/main/webapp/src/index.html @@ -36,6 +36,7 @@ + diff --git a/docs-web/src/main/webapp/src/lib/angular.file-upload.js b/docs-web/src/main/webapp/src/lib/angular.file-upload.js new file mode 100644 index 00000000..118d3318 --- /dev/null +++ b/docs-web/src/main/webapp/src/lib/angular.file-upload.js @@ -0,0 +1,602 @@ +/** + * AngularJS file upload/drop directive and service with progress and abort + * @author Danial + * @version 3.1.1 + */ +(function() { + + function patchXHR(fnName, newFn) { + window.XMLHttpRequest.prototype[fnName] = newFn(window.XMLHttpRequest.prototype[fnName]); + } + + if (window.XMLHttpRequest && !window.XMLHttpRequest.__isFileAPIShim) { + patchXHR('setRequestHeader', function(orig) { + return function(header, value) { + if (header === '__setXHR_') { + var val = value(this); + // fix for angular < 1.2.0 + if (val instanceof Function) { + val(this); + } + } else { + orig.apply(this, arguments); + } + } + }); + } + + var angularFileUpload = angular.module('angularFileUpload', []); + + angularFileUpload.version = '3.1.1'; + angularFileUpload.service('$upload', ['$http', '$q', '$timeout', function($http, $q, $timeout) { + function sendHttp(config) { + config.method = config.method || 'POST'; + config.headers = config.headers || {}; + config.transformRequest = config.transformRequest || function(data, headersGetter) { + if (window.ArrayBuffer && data instanceof window.ArrayBuffer) { + return data; + } + return $http.defaults.transformRequest[0](data, headersGetter); + }; + var deferred = $q.defer(); + var promise = deferred.promise; + + config.headers['__setXHR_'] = function() { + return function(xhr) { + if (!xhr) return; + config.__XHR = xhr; + config.xhrFn && config.xhrFn(xhr); + xhr.upload.addEventListener('progress', function(e) { + e.config = config; + deferred.notify ? deferred.notify(e) : promise.progress_fn && $timeout(function(){promise.progress_fn(e)}); + }, false); + //fix for firefox not firing upload progress end, also IE8-9 + xhr.upload.addEventListener('load', function(e) { + if (e.lengthComputable) { + e.config = config; + deferred.notify ? deferred.notify(e) : promise.progress_fn && $timeout(function(){promise.progress_fn(e)}); + } + }, false); + }; + }; + + $http(config).then(function(r){deferred.resolve(r)}, function(e){deferred.reject(e)}, function(n){deferred.notify(n)}); + + promise.success = function(fn) { + promise.then(function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + promise.error = function(fn) { + promise.then(null, function(response) { + fn(response.data, response.status, response.headers, config); + }); + return promise; + }; + + promise.progress = function(fn) { + promise.progress_fn = fn; + promise.then(null, null, function(update) { + fn(update); + }); + return promise; + }; + promise.abort = function() { + if (config.__XHR) { + $timeout(function() { + config.__XHR.abort(); + }); + } + return promise; + }; + promise.xhr = function(fn) { + config.xhrFn = (function(origXhrFn) { + return function() { + origXhrFn && origXhrFn.apply(promise, arguments); + fn.apply(promise, arguments); + } + })(config.xhrFn); + return promise; + }; + + return promise; + } + + this.upload = function(config) { + config.headers = config.headers || {}; + config.headers['Content-Type'] = undefined; + var origTransformRequest = config.transformRequest; + config.transformRequest = config.transformRequest ? + (Object.prototype.toString.call(config.transformRequest) === '[object Array]' ? + config.transformRequest : [config.transformRequest]) : []; + config.transformRequest.push(function(data, headerGetter) { + var formData = new FormData(); + var allFields = {}; + for (var key in config.fields) allFields[key] = config.fields[key]; + if (data) allFields['data'] = data; + + if (config.formDataAppender) { + for (var key in allFields) { + config.formDataAppender(formData, key, allFields[key]); + } + } else { + for (var key in allFields) { + var val = allFields[key]; + if (val !== undefined) { + if (Object.prototype.toString.call(val) === '[object String]') { + formData.append(key, val); + } else { + if (config.sendObjectsAsJsonBlob && typeof val === 'object') { + formData.append(key, new Blob([val], { type: 'application/json' })); + } else { + formData.append(key, JSON.stringify(val)); + } + } + } + } + } + + if (config.file != null) { + var fileFormName = config.fileFormDataName || 'file'; + + if (Object.prototype.toString.call(config.file) === '[object Array]') { + var isFileFormNameString = Object.prototype.toString.call(fileFormName) === '[object String]'; + for (var i = 0; i < config.file.length; i++) { + formData.append(isFileFormNameString ? fileFormName : fileFormName[i], config.file[i], + (config.fileName && config.fileName[i]) || config.file[i].name); + } + } else { + formData.append(fileFormName, config.file, config.fileName || config.file.name); + } + } + return formData; + }); + + return sendHttp(config); + }; + + this.http = function(config) { + return sendHttp(config); + }; + }]); + + angularFileUpload.directive('ngFileSelect', [ '$parse', '$timeout', '$compile', + function($parse, $timeout, $compile) { return { + restrict: 'AEC', + require:'?ngModel', + link: function(scope, elem, attr, ngModel) { + handleFileSelect(scope, elem, attr, ngModel, $parse, $timeout, $compile); + } + }}]); + + function handleFileSelect(scope, elem, attr, ngModel, $parse, $timeout, $compile) { + function isInputTypeFile() { + return elem[0].tagName.toLowerCase() === 'input' && elem.attr('type') && elem.attr('type').toLowerCase() === 'file'; + } + + var watchers = []; + + function watchForRecompile(attrVal) { + $timeout(function() { + if (elem.parent().length) { + watchers.push(scope.$watch(attrVal, function(val, oldVal) { + if (val != oldVal) { + recompileElem(); + } + })); + } + }); + } + + function recompileElem() { + var clone = elem.clone(); + if (elem.attr('__afu_gen__')) { + angular.element(document.getElementById(elem.attr('id').substring(1))).remove(); + } + if (elem.parent().length) { + for (var i = 0; i < watchers.length; i++) { + watchers[i](); + } + elem.replaceWith(clone); + $compile(clone)(scope); + } + return clone; + } + + function bindAttr(bindAttr, attrName) { + if (bindAttr) { + watchForRecompile(bindAttr); + var val = $parse(bindAttr)(scope); + if (val) { + elem.attr(attrName, val); + attr[attrName] = val; + } else { + elem.attr(attrName, null); + delete attr[attrName]; + } + } + } + + bindAttr(attr.ngMultiple, 'multiple'); + bindAttr(attr.ngAccept, 'ng-accept'); + bindAttr(attr.ngCapture, 'capture'); + + if (attr['ngFileSelect'] != '') { + attr.ngFileChange = attr.ngFileSelect; + } + + function onChangeFn(evt) { + var files = [], fileList, i; + fileList = evt.__files_ || (evt.target && evt.target.files); + updateModel(fileList, attr, ngModel, scope, evt); + }; + + var fileElem = elem; + if (!isInputTypeFile()) { + fileElem = angular.element('') + if (elem.attr['multiple']) fileElem.attr('multiple', elem.attr['multiple']); + if (elem.attr['accept']) fileElem.attr('accept', elem.attr['accept']); + if (elem.attr['capture']) fileElem.attr('capture', elem.attr['capture']); + for (var key in attr) { + if (key.indexOf('inputFile') == 0) { + var name = key.substring('inputFile'.length); + name = name[0].toLowerCase() + name.substring(1); + fileElem.attr(name, attr[key]); + } + } + + fileElem.css('width', '0px').css('height', '0px').css('position', 'absolute').css('padding', 0).css('margin', 0) + .css('overflow', 'hidden').attr('tabindex', '-1').css('opacity', 0).attr('__afu_gen__', true); + elem.attr('__refElem__', true); + fileElem[0].__refElem__ = elem[0]; + elem.parent()[0].insertBefore(fileElem[0], elem[0]) + elem.css('overflow', 'hidden'); + elem.bind('click', function(e) { + if (!resetAndClick(e)) { + fileElem[0].click(); + } + }); + } else { + elem.bind('click', resetAndClick); + } + + function resetAndClick(evt) { + if (fileElem[0].value != null && fileElem[0].value != '') { + fileElem[0].value = null; + // IE 11 already fires change event when you set the value to null + if (navigator.userAgent.indexOf("Trident/7") === -1) { + onChangeFn({target: {files: []}}); + } + } + // if this is manual click trigger we don't need to reset again + if (!elem.attr('__afu_clone__')) { + // fix for IE10 cannot set the value of the input to null programmatically by cloning and replacing input + // IE 11 setting the value to null event will be fired after file change clearing the selected file so + // we just recreate the element for IE 11 as well + if (navigator.appVersion.indexOf("MSIE 10") !== -1 || navigator.userAgent.indexOf("Trident/7") !== -1) { + var clone = recompileElem(); + clone.attr('__afu_clone__', true); + clone[0].click(); + evt.preventDefault(); + evt.stopPropagation(); + return true; + } + } else { + elem.attr('__afu_clone__', null); + } + } + + fileElem.bind('change', onChangeFn); + + elem.on('$destroy', function() { + for (var i = 0; i < watchers.length; i++) { + watchers[i](); + } + if (elem[0] != fileElem[0]) fileElem.remove(); + }); + + watchers.push(scope.$watch(attr.ngModel, function(val, oldVal) { + if (val != oldVal && (val == null || !val.length)) { + if (navigator.appVersion.indexOf("MSIE 10") !== -1) { + recompileElem(); + } else { + fileElem[0].value = null; + } + } + })); + + function updateModel(fileList, attr, ngModel, scope, evt) { + var files = [], rejFiles = []; + var accept = $parse(attr.ngAccept)(scope); + var regexp = angular.isString(accept) && accept ? new RegExp(globStringToRegex(accept), 'gi') : null; + var acceptFn = regexp ? null : attr.ngAccept; + + for (var i = 0; i < fileList.length; i++) { + var file = fileList.item(i); + if ((!regexp || file.type.match(regexp) || (file.name != null && file.name.match(regexp))) && + (!acceptFn || $parse(acceptFn)(scope, {$file: file, $event: evt}))) { + files.push(file); + } else { + rejFiles.push(file); + } + } + $timeout(function() { + if (ngModel) { + $parse(attr.ngModel).assign(scope, files); + ngModel && ngModel.$setViewValue(files != null && files.length == 0 ? '' : files); + if (attr.ngModelRejected) { + $parse(attr.ngModelRejected).assign(scope, rejFiles); + } + } + if (attr.ngFileChange && attr.ngFileChange != "") { + $parse(attr.ngFileChange)(scope, { + $files: files, + $rejectedFiles: rejFiles, + $event: evt + }); + } + }); + } + } + + angularFileUpload.directive('ngFileDrop', [ '$parse', '$timeout', '$location', function($parse, $timeout, $location) { return { + restrict: 'AEC', + require:'?ngModel', + link: function(scope, elem, attr, ngModel) { + handleDrop(scope, elem, attr, ngModel, $parse, $timeout, $location); + } + }}]); + + angularFileUpload.directive('ngNoFileDrop', function() { + return function(scope, elem, attr) { + if (dropAvailable()) elem.css('display', 'none') + } + }); + +//for backward compatibility + angularFileUpload.directive('ngFileDropAvailable', [ '$parse', '$timeout', function($parse, $timeout) { + return function(scope, elem, attr) { + if (dropAvailable()) { + var fn = $parse(attr['ngFileDropAvailable']); + $timeout(function() { + fn(scope); + }); + } + } + }]); + + function handleDrop(scope, elem, attr, ngModel, $parse, $timeout, $location) { + var available = dropAvailable(); + if (attr['dropAvailable']) { + $timeout(function() { + scope.dropAvailable ? scope.dropAvailable.value = available : scope.dropAvailable = available; + }); + } + if (!available) { + if ($parse(attr.hideOnDropNotAvailable)(scope) != false) { + elem.css('display', 'none'); + } + return; + } + var leaveTimeout = null; + var stopPropagation = $parse(attr.stopPropagation)(scope); + var dragOverDelay = 1; + var accept = $parse(attr.ngAccept)(scope) || attr.accept; + var regexp = angular.isString(accept) && accept ? new RegExp(globStringToRegex(accept), 'gi') : null; + var acceptFn = regexp ? null : attr.ngAccept; + var actualDragOverClass; + elem[0].addEventListener('dragover', function(evt) { + evt.preventDefault(); + if (stopPropagation) evt.stopPropagation(); + // handling dragover events from the Chrome download bar + if (navigator.userAgent.indexOf("Chrome") > -1) { + var b = evt.dataTransfer.effectAllowed; + evt.dataTransfer.dropEffect = ('move' === b || 'linkMove' === b) ? 'move' : 'copy'; + } + $timeout.cancel(leaveTimeout); + if (!scope.actualDragOverClass) { + actualDragOverClass = calculateDragOverClass(scope, attr, evt); + } + elem.addClass(actualDragOverClass); + }, false); + elem[0].addEventListener('dragenter', function(evt) { + evt.preventDefault(); + if (stopPropagation) evt.stopPropagation(); + }, false); + elem[0].addEventListener('dragleave', function(evt) { + leaveTimeout = $timeout(function() { + elem.removeClass(actualDragOverClass); + actualDragOverClass = null; + }, dragOverDelay || 1); + }, false); + if (attr['ngFileDrop'] != '') { + attr.ngFileChange = scope.ngFileDrop; + } + elem[0].addEventListener('drop', function(evt) { + evt.preventDefault(); + if (stopPropagation) evt.stopPropagation(); + elem.removeClass(actualDragOverClass); + actualDragOverClass = null; + extractFiles(evt, function(files, rejFiles) { + $timeout(function() { + if (ngModel) { + $parse(attr.ngModel).assign(scope, files); + ngModel && ngModel.$setViewValue(files != null && files.length == 0 ? '' : files); + } + if (attr['ngModelRejected']) { + if (scope[attr.ngModelRejected]) { + $parse(attr.ngModelRejected).assign(scope, rejFiles); + } + } + }); + $timeout(function() { + $parse(attr.ngFileChange)(scope, { + $files: files, + $rejectedFiles: rejFiles, + $event: evt + }); + }); + }, $parse(attr.allowDir)(scope) != false, attr.multiple || $parse(attr.ngMultiple)(scope)); + }, false); + + function calculateDragOverClass(scope, attr, evt) { + var valid = true; + if (regexp || acceptFn) { + var items = evt.dataTransfer.items; + if (items != null) { + for (var i = 0 ; i < items.length && valid; i++) { + valid = valid && (items[i].kind == 'file' || items[i].kind == '') && + ((acceptFn && $parse(acceptFn)(scope, {$file: items[i], $event: evt})) || + (regexp && (items[i].type != null && items[i].type.match(regexp)) || + (items[i].name != null && items[i].name.match(regexp)))); + } + } + } + var clazz = $parse(attr.dragOverClass)(scope, {$event : evt}); + if (clazz) { + if (clazz.delay) dragOverDelay = clazz.delay; + if (clazz.accept) clazz = valid ? clazz.accept : clazz.reject; + } + return clazz || attr['dragOverClass'] || 'dragover'; + } + + function extractFiles(evt, callback, allowDir, multiple) { + var files = [], rejFiles = [], items = evt.dataTransfer.items, processing = 0; + + function addFile(file) { + if ((!regexp || file.type.match(regexp) || (file.name != null && file.name.match(regexp))) && + (!acceptFn || $parse(acceptFn)(scope, {$file: file, $event: evt}))) { + files.push(file); + } else { + rejFiles.push(file); + } + } + + if (items && items.length > 0 && $location.protocol() != 'file') { + for (var i = 0; i < items.length; i++) { + if (items[i].webkitGetAsEntry && items[i].webkitGetAsEntry() && items[i].webkitGetAsEntry().isDirectory) { + var entry = items[i].webkitGetAsEntry(); + if (entry.isDirectory && !allowDir) { + continue; + } + if (entry != null) { + traverseFileTree(files, entry); + } + } else { + var f = items[i].getAsFile(); + if (f != null) addFile(f); + } + if (!multiple && files.length > 0) break; + } + } else { + var fileList = evt.dataTransfer.files; + if (fileList != null) { + for (var i = 0; i < fileList.length; i++) { + addFile(fileList.item(i)); + if (!multiple && files.length > 0) break; + } + } + } + var delays = 0; + (function waitForProcess(delay) { + $timeout(function() { + if (!processing) { + if (!multiple && files.length > 1) { + var i = 0; + while (files[i].type == 'directory') i++; + files = [files[i]]; + } + callback(files, rejFiles); + } else { + if (delays++ * 10 < 20 * 1000) { + waitForProcess(10); + } + } + }, delay || 0) + })(); + + function traverseFileTree(files, entry, path) { + if (entry != null) { + if (entry.isDirectory) { + var filePath = (path || '') + entry.name; + addFile({name: entry.name, type: 'directory', path: filePath}); + var dirReader = entry.createReader(); + var entries = []; + processing++; + var readEntries = function() { + dirReader.readEntries(function(results) { + try { + if (!results.length) { + for (var i = 0; i < entries.length; i++) { + traverseFileTree(files, entries[i], (path ? path : '') + entry.name + '/'); + } + processing--; + } else { + entries = entries.concat(Array.prototype.slice.call(results || [], 0)); + readEntries(); + } + } catch (e) { + processing--; + console.error(e); + } + }, function() { + processing--; + }); + }; + readEntries(); + } else { + processing++; + entry.file(function(file) { + try { + processing--; + file.path = (path ? path : '') + file.name; + addFile(file); + } catch (e) { + processing--; + console.error(e); + } + }, function(e) { + processing--; + }); + } + } + } + } + } + + function dropAvailable() { + var div = document.createElement('div'); + return ('draggable' in div) && ('ondrop' in div); + } + + function globStringToRegex(str) { + if (str.length > 2 && str[0] === '/' && str[str.length -1] === '/') { + return str.substring(1, str.length - 1); + } + var split = str.split(','), result = ''; + if (split.length > 1) { + for (var i = 0; i < split.length; i++) { + result += '(' + globStringToRegex(split[i]) + ')'; + if (i < split.length - 1) { + result += '|' + } + } + } else { + if (str.indexOf('.') == 0) { + str= '*' + str; + } + result = '^' + str.replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + '-]', 'g'), '\\$&') + '$'; + result = result.replace(/\\\*/g, '.*').replace(/\\\?/g, '.'); + } + return result; + } + + var ngFileUpload = angular.module('ngFileUpload', []); + + for (var key in angularFileUpload) { + ngFileUpload[key] = angularFileUpload[key]; + } + +})(); \ No newline at end of file diff --git a/docs-web/src/main/webapp/src/partial/docs/document.default.html b/docs-web/src/main/webapp/src/partial/docs/document.default.html index 69bd28c0..603b6b35 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.default.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.default.html @@ -4,13 +4,13 @@

{{ app.document_count }} document{{ app.document_count > 1 ? 's' : '' }} in the database

- -
-

There seems to be a kind of order in the universe, but human life itself is almost pure chaos.

- Katherine Anne Porter -
- -
+ +
    diff --git a/docs-web/src/main/webapp/src/partial/docs/document.view.html b/docs-web/src/main/webapp/src/partial/docs/document.view.html index c70d8930..85a8137b 100644 --- a/docs-web/src/main/webapp/src/partial/docs/document.view.html +++ b/docs-web/src/main/webapp/src/partial/docs/document.view.html @@ -13,7 +13,7 @@ {{ document.title }} {{ document.create_date | date: 'yyyy-MM-dd' }} - +

    diff --git a/docs-web/src/main/webapp/src/partial/share/share.html b/docs-web/src/main/webapp/src/partial/share/share.html index 502a754a..42efc1b1 100644 --- a/docs-web/src/main/webapp/src/partial/share/share.html +++ b/docs-web/src/main/webapp/src/partial/share/share.html @@ -4,7 +4,7 @@

    {{ document.title }} {{ document.create_date | date: 'yyyy-MM-dd' }} - +

      diff --git a/docs-web/src/prod/resources/config.properties b/docs-web/src/prod/resources/config.properties index 6c2faf82..1bfb8fb0 100644 --- a/docs-web/src/prod/resources/config.properties +++ b/docs-web/src/prod/resources/config.properties @@ -1,3 +1,3 @@ api.current_version=${project.version} api.min_version=1.0 -db.version=6 \ No newline at end of file +db.version=7 \ No newline at end of file diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java index ce54c629..e6ca1d4a 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestFileResource.java @@ -197,4 +197,46 @@ public class TestFileResource extends BaseJerseyTest { files = json.getJSONArray("files"); Assert.assertEquals(1, files.length()); } + + @Test + public void testOrphanFile() throws Exception { + // Login file1 + clientUtil.createUser("file2"); + String file2AuthenticationToken = clientUtil.login("file2"); + + // Add a file + WebResource fileResource = resource().path("/file"); + fileResource.addFilter(new CookieAuthenticationFilter(file2AuthenticationToken)); + FormDataMultiPart form = new FormDataMultiPart(); + InputStream file = this.getClass().getResourceAsStream("/file/PIA00452.jpg"); + FormDataBodyPart fdp = new FormDataBodyPart("file", + new BufferedInputStream(file), + MediaType.APPLICATION_OCTET_STREAM_TYPE); + form.bodyPart(fdp); + ClientResponse response = fileResource.type(MediaType.MULTIPART_FORM_DATA).put(ClientResponse.class, form); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + JSONObject json = response.getEntity(JSONObject.class); + String file1Id = json.getString("id"); + + // Create a document + WebResource documentResource = resource().path("/document"); + documentResource.addFilter(new CookieAuthenticationFilter(file2AuthenticationToken)); + MultivaluedMapImpl postParams = new MultivaluedMapImpl(); + postParams.add("title", "File test document 1"); + postParams.add("language", "eng"); + response = documentResource.put(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + String document1Id = json.optString("id"); + Assert.assertNotNull(document1Id); + + // Attach a file to a document + documentResource = resource().path("/file/" + file1Id); + documentResource.addFilter(new CookieAuthenticationFilter(file2AuthenticationToken)); + postParams = new MultivaluedMapImpl(); + postParams.add("id", document1Id); + response = documentResource.post(ClientResponse.class, postParams); + Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); + json = response.getEntity(JSONObject.class); + } } \ No newline at end of file