PDF handling, file upload progression

This commit is contained in:
jendib 2013-07-28 18:29:03 +02:00
parent 19000d095f
commit 471933ca8c
14 changed files with 186 additions and 105 deletions

View File

@ -113,8 +113,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bitlet</groupId> <groupId>org.imgscalr</groupId>
<artifactId>weupnp</artifactId> <artifactId>imgscalr-lib</artifactId>
</dependency> </dependency>
<!-- Test dependencies --> <!-- Test dependencies -->

View File

@ -1,11 +0,0 @@
package com.sismics.docs.core.dao.lucene;
/**
* Lucene Article DAO.
*
* @author bgamard
*/
public class ArticleDao {
}

View File

@ -16,4 +16,6 @@ public class MimeType {
public static final String IMAGE_GIF = "image/gif"; public static final String IMAGE_GIF = "image/gif";
public static final String APPLICATION_ZIP = "application/zip"; public static final String APPLICATION_ZIP = "application/zip";
public static final String APPLICATION_PDF = "application/pdf";
} }

View File

@ -37,6 +37,8 @@ public class MimeTypeUtil {
return MimeType.IMAGE_PNG; return MimeType.IMAGE_PNG;
} else if (headerBytes[0] == ((byte) 0x00) && headerBytes[1] == ((byte) 0x00) && headerBytes[2] == ((byte) 0x01) && headerBytes[3] == ((byte) 0x00)) { } else if (headerBytes[0] == ((byte) 0x00) && headerBytes[1] == ((byte) 0x00) && headerBytes[2] == ((byte) 0x01) && headerBytes[3] == ((byte) 0x00)) {
return MimeType.IMAGE_X_ICON; return MimeType.IMAGE_X_ICON;
} else if (headerBytes[0] == ((byte) 0x25) && headerBytes[1] == ((byte) 0x50) && headerBytes[2] == ((byte) 0x44) && headerBytes[3] == ((byte) 0x46)) {
return MimeType.APPLICATION_PDF;
} }
return null; return null;

View File

@ -34,7 +34,7 @@
<com.googlecode.owasp-java-html-sanitizer.owasp-java-html-sanitizer.version>r156</com.googlecode.owasp-java-html-sanitizer.owasp-java-html-sanitizer.version> <com.googlecode.owasp-java-html-sanitizer.owasp-java-html-sanitizer.version>r156</com.googlecode.owasp-java-html-sanitizer.owasp-java-html-sanitizer.version>
<org.apache.lucene.version>4.2.0</org.apache.lucene.version> <org.apache.lucene.version>4.2.0</org.apache.lucene.version>
<jgoodies.forms.version>1.0.5</jgoodies.forms.version> <jgoodies.forms.version>1.0.5</jgoodies.forms.version>
<org.bitlet.weupnp.version>0.1.2</org.bitlet.weupnp.version> <org.imgscalr.imgscalr-lib.version>4.2</org.imgscalr.imgscalr-lib.version>
<com.sun.grizzly.version>1.9.18-m</com.sun.grizzly.version> <com.sun.grizzly.version>1.9.18-m</com.sun.grizzly.version>
<org.hibernate.hibernate.version>4.1.0.Final</org.hibernate.hibernate.version> <org.hibernate.hibernate.version>4.1.0.Final</org.hibernate.hibernate.version>
@ -430,9 +430,9 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bitlet</groupId> <groupId>org.imgscalr</groupId>
<artifactId>weupnp</artifactId> <artifactId>imgscalr-lib</artifactId>
<version>${org.bitlet.weupnp.version}</version> <version>${org.imgscalr.imgscalr-lib.version}</version>
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@ -105,6 +105,8 @@ public class RequestContextFilter implements Filter {
} catch (Exception e) { } catch (Exception e) {
ThreadLocalContext.cleanup(); ThreadLocalContext.cleanup();
// IOException are thrown if the client closes the connection before completion
if (!(e instanceof IOException)) {
log.error("An exception occured, rolling back current transaction", e); log.error("An exception occured, rolling back current transaction", e);
// If an unprocessed error comes up from the application layers (Jersey...), rollback the transaction (should not happen) // If an unprocessed error comes up from the application layers (Jersey...), rollback the transaction (should not happen)
@ -121,6 +123,7 @@ public class RequestContextFilter implements Filter {
} }
throw new ServletException(e); throw new ServletException(e);
} }
}
ThreadLocalContext.cleanup(); ThreadLocalContext.cleanup();

View File

@ -1,9 +1,7 @@
package com.sismics.docs.rest.resource; package com.sismics.docs.rest.resource;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.FilenameFilter;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -33,6 +31,8 @@ import com.sismics.rest.exception.ClientException;
import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.rest.exception.ForbiddenClientException;
import com.sismics.rest.exception.ServerException; import com.sismics.rest.exception.ServerException;
import com.sismics.rest.util.ValidationUtil; import com.sismics.rest.util.ValidationUtil;
import com.sismics.util.FileUtil;
import com.sismics.util.ImageUtil;
import com.sismics.util.mime.MimeTypeUtil; import com.sismics.util.mime.MimeTypeUtil;
import com.sun.jersey.multipart.FormDataBodyPart; import com.sun.jersey.multipart.FormDataBodyPart;
import com.sun.jersey.multipart.FormDataParam; import com.sun.jersey.multipart.FormDataParam;
@ -129,8 +129,8 @@ public class FileResource extends BaseResource {
file.setMimeType(mimeType); file.setMimeType(mimeType);
String fileId = fileDao.create(file); String fileId = fileDao.create(file);
// Copy the incoming stream content into the storage directory // Save the file
Files.copy(is, Paths.get(DirectoryUtil.getStorageDirectory().getPath(), fileId)); FileUtil.save(is, file);
// Always return ok // Always return ok
JSONObject response = new JSONObject(); JSONObject response = new JSONObject();
@ -222,22 +222,36 @@ public class FileResource extends BaseResource {
@Path("{id: [a-z0-9\\-]+}/data") @Path("{id: [a-z0-9\\-]+}/data")
@Produces(MediaType.APPLICATION_OCTET_STREAM) @Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response data( public Response data(
@PathParam("id") final String id) throws JSONException { @PathParam("id") final String id,
@QueryParam("thumbnail") boolean thumbnail) throws JSONException {
if (!authenticate()) { if (!authenticate()) {
throw new ForbiddenClientException(); throw new ForbiddenClientException();
} }
// Get the file // Get the file
java.io.File storageDirectory = DirectoryUtil.getStorageDirectory(); FileDao fileDao = new FileDao();
java.io.File[] matchingFiles = storageDirectory.listFiles(new FilenameFilter() { File file = null;
@Override try {
public boolean accept(java.io.File dir, String name) { file = fileDao.getFile(id);
return name.startsWith(id); } catch (NoResultException e) {
throw new ClientException("FileNotFound", MessageFormat.format("File not found: {0}", id));
} }
});
final java.io.File storageFile = matchingFiles[0];
return Response.ok(storageFile)
// Get the stored file
java.io.File storedfile = null;
if (thumbnail) {
if (ImageUtil.isImage(file.getMimeType())) {
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), id + "_thumb").toFile();
} else {
storedfile = new java.io.File(getClass().getResource("/image/file.png").getFile());
}
} else {
storedfile = Paths.get(DirectoryUtil.getStorageDirectory().getPath(), id).toFile();
}
return Response.ok(storedfile)
.header("Content-Type", file.getMimeType())
.build(); .build();
} }
} }

View File

@ -67,14 +67,10 @@ var App = angular.module('docs', ['ui.state', 'ui.bootstrap', 'ui.keypress', 're
}) })
.state('document.view.file', { .state('document.view.file', {
url: '/file/:fileId', url: '/file/:fileId',
onEnter: function($stateParams, $state, $dialog) { views: {
$dialog.dialog({ 'file': {
keyboard: true,
templateUrl: 'partial/file.view.html',
controller: 'FileView' controller: 'FileView'
}).open().then(function(result) { }
$state.transitionTo('document.view', { id: $stateParams.id });
});
} }
}) })
.state('login', { .state('login', {

View File

@ -3,7 +3,7 @@
/** /**
* Document edition controller. * Document edition controller.
*/ */
App.controller('DocumentEdit', function($scope, $http, $state, $stateParams, Restangular) { App.controller('DocumentEdit', function($scope, $q, $http, $state, $stateParams, Restangular) {
/** /**
* Returns true if in edit mode (false in add mode). * Returns true if in edit mode (false in add mode).
*/ */
@ -30,35 +30,53 @@ App.controller('DocumentEdit', function($scope, $http, $state, $stateParams, Res
promise = Restangular promise = Restangular
.one('document', $stateParams.id) .one('document', $stateParams.id)
.post('', $scope.document); .post('', $scope.document);
promise.then(function(data) {
$scope.loadDocuments();
$state.transitionTo('document.view', { id: $stateParams.id });
})
} else { } else {
promise = Restangular promise = Restangular
.one('document') .one('document')
.put($scope.document); .put($scope.document);
}
// Upload files after edition
promise.then(function(data) { promise.then(function(data) {
var promises = [];
$scope.fileProgress = 0;
_.each($scope.newFiles, function(file) {
// Build the payload
var formData = new FormData();
formData.append('id', data.id);
formData.append('file', file);
// Send the file
var promiseFile = $http.put('api/file',
formData, {
headers: { 'Content-Type': false },
transformRequest: function(data) { return data; }
});
// TODO Handle progression when $q.notify will be released
promiseFile.then(function() {
$scope.fileProgress += 100 / _.size($scope.newFiles);
});
// Store the promise for later
promises.push(promiseFile);
});
// When all files upload are over, move on
var promiseAll = $q.all(promises);
if ($scope.isEdit()) {
promiseAll.then(function(data) {
$scope.loadDocuments();
$state.transitionTo('document.view', { id: $stateParams.id });
});
} else {
promiseAll.then(function(data) {
$scope.document = {}; $scope.document = {};
$scope.loadDocuments(); $scope.loadDocuments();
}); });
} }
// Upload files after edition
// TODO Handle file upload progression and errors
promise.then(function(data) {
_.each($scope.files, function(file) {
var formData = new FormData();
formData.append('id', data.id);
formData.append('file', file);
$.ajax({
url: 'api/file',
type: 'PUT',
data: formData,
processData: false,
contentType: false
});
});
}); });
}; };

View File

@ -3,9 +3,20 @@
/** /**
* File view controller. * File view controller.
*/ */
App.controller('FileView', function($rootScope, $state, $scope, $stateParams) { App.controller('FileView', function($dialog, $state, $stateParams) {
var dialog = $dialog.dialog({
keyboard: true,
templateUrl: 'partial/file.view.html',
controller: function($rootScope, $scope, $state, $stateParams) {
$scope.id = $stateParams.fileId; $scope.id = $stateParams.fileId;
// Search current file
_.each($rootScope.files, function(value, key, list) {
if (value.id == $scope.id) {
$scope.file = value;
}
});
/** /**
* Navigate to the next file. * Navigate to the next file.
*/ */
@ -14,6 +25,7 @@ App.controller('FileView', function($rootScope, $state, $scope, $stateParams) {
if (value.id == $scope.id) { if (value.id == $scope.id) {
var next = $rootScope.files[key + 1]; var next = $rootScope.files[key + 1];
if (next) { if (next) {
dialog.close({});
$state.transitionTo('document.view.file', { id: $stateParams.id, fileId: next.id }); $state.transitionTo('document.view.file', { id: $stateParams.id, fileId: next.id });
} }
} }
@ -28,9 +40,25 @@ App.controller('FileView', function($rootScope, $state, $scope, $stateParams) {
if (value.id == $scope.id) { if (value.id == $scope.id) {
var previous = $rootScope.files[key - 1]; var previous = $rootScope.files[key - 1];
if (previous) { if (previous) {
dialog.close({});
$state.transitionTo('document.view.file', { id: $stateParams.id, fileId: previous.id }); $state.transitionTo('document.view.file', { id: $stateParams.id, fileId: previous.id });
} }
} }
}); });
}; };
/**
* Open the file in a new window.
*/
$scope.openFile = function() {
window.open('api/file/' + $scope.id + '/data');
};
}
});
dialog.open().then(function(result) {
if (result == null) {
$state.transitionTo('document.view', { id: $stateParams.id });
}
});
}); });

View File

@ -14,7 +14,7 @@
<div class="control-group"> <div class="control-group">
<label class="control-label" for="inputFiles">New files</label> <label class="control-label" for="inputFiles">New files</label>
<div class="controls"> <div class="controls">
<file class="input-block-level" id="inputFiles" multiple="multiple" ng-model="files" accept="image/png,image/jpg,image/jpeg,image/gif" /> <file class="input-block-level" id="inputFiles" multiple="multiple" ng-model="newFiles" accept="image/png,image/jpg,image/jpeg,image/gif" />
</div> </div>
</div> </div>
<div class="form-actions"> <div class="form-actions">
@ -22,3 +22,7 @@
<button type="submit" class="btn" ng-click="cancel()">Cancel</button> <button type="submit" class="btn" ng-click="cancel()">Cancel</button>
</div> </div>
</form> </form>
<div class="row-fluid">
<div class="span12"><progress percent="fileProgress" class="progress-info active"></progress></div>
</div>

View File

@ -14,7 +14,7 @@
<li class="span2" ng-repeat="file in files" ng-style="{ 'margin-left': $index % 6 == 0 ? '0' : '' }"> <li class="span2" ng-repeat="file in files" ng-style="{ 'margin-left': $index % 6 == 0 ? '0' : '' }">
<div class="thumbnail"> <div class="thumbnail">
<a ng-click="openFile(file)"> <a ng-click="openFile(file)">
<img ng-src="api/file/{{ file.id }}/data" tooltip="{{ file.mimetype }}" tooltip-placement="top" /> <img ng-src="api/file/{{ file.id }}/data?thumbnail=true" tooltip="{{ file.mimetype }}" tooltip-placement="top" />
</a> </a>
<div class="caption"> <div class="caption">
<p class="text-right"> <p class="text-right">
@ -24,3 +24,5 @@
</div> </div>
</li> </li>
</ul> </ul>
<div ui-view="file"></div>

View File

@ -3,5 +3,15 @@
<button type="button" class="btn" ng-click="previousFile()">Previous</button> <button type="button" class="btn" ng-click="previousFile()">Previous</button>
<button type="button" class="btn" ng-click="nextFile()">Next</button> <button type="button" class="btn" ng-click="nextFile()">Next</button>
</div> </div>
<div class="btn-group pull-right">
<button type="button" class="btn" ng-click="openFile()"><span class="icon-share"></span></button>
</div>
</div>
<img ng-show="file.mimetype == 'image/png' || file.mimetype == 'image/jpeg' || file.mimetype == 'image/gif'" ng-src="api/file/{{ id }}/data" />
<div class="text-center" ng-show="file.mimetype == 'application/pdf'">
<img ng-src="api/file/{{ id }}/data?thumbnail=true" />
</div> </div>
<img ng-src="api/file/{{ id }}/data" />

View File

@ -76,16 +76,29 @@ public class TestFileResource extends BaseJerseyTest {
// Get the file data // Get the file data
fileResource = resource().path("/file/" + file1Id + "/data"); fileResource = resource().path("/file/" + file1Id + "/data");
fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken)); fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken));
response = fileResource.get(ClientResponse.class); MultivaluedMapImpl getParams = new MultivaluedMapImpl();
getParams.putSingle("thumbnail", false);
response = fileResource.queryParams(getParams).get(ClientResponse.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus())); Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
InputStream is = response.getEntityInputStream(); InputStream is = response.getEntityInputStream();
byte[] fileBytes = ByteStreams.toByteArray(is); byte[] fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(163510, fileBytes.length); Assert.assertEquals(163510, fileBytes.length);
// Get the thumbnail data
fileResource = resource().path("/file/" + file1Id + "/data");
fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken));
getParams = new MultivaluedMapImpl();
getParams.putSingle("thumbnail", true);
response = fileResource.queryParams(getParams).get(ClientResponse.class);
Assert.assertEquals(Status.OK, Status.fromStatusCode(response.getStatus()));
is = response.getEntityInputStream();
fileBytes = ByteStreams.toByteArray(is);
Assert.assertEquals(41935, fileBytes.length);
// Get all files from a document // Get all files from a document
fileResource = resource().path("/file/list"); fileResource = resource().path("/file/list");
fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken)); fileResource.addFilter(new CookieAuthenticationFilter(file1AuthenticationToken));
MultivaluedMapImpl getParams = new MultivaluedMapImpl(); getParams = new MultivaluedMapImpl();
getParams.putSingle("id", document1Id); getParams.putSingle("id", document1Id);
response = fileResource.queryParams(getParams).get(ClientResponse.class); response = fileResource.queryParams(getParams).get(ClientResponse.class);
json = response.getEntity(JSONObject.class); json = response.getEntity(JSONObject.class);