#208: rename files

This commit is contained in:
Benjamin Gamard 2018-03-23 12:52:42 +01:00
parent 37ca5ff84e
commit 55a4bb7621
13 changed files with 205 additions and 102 deletions

View File

@ -140,6 +140,7 @@ public class FileDao {
// Update the file // Update the file
fileDb.setDocumentId(file.getDocumentId()); fileDb.setDocumentId(file.getDocumentId());
fileDb.setName(file.getName());
fileDb.setContent(file.getContent()); fileDb.setContent(file.getContent());
fileDb.setOrder(file.getOrder()); fileDb.setOrder(file.getOrder());
fileDb.setMimeType(file.getMimeType()); fileDb.setMimeType(file.getMimeType());

View File

@ -1,15 +1,14 @@
package com.sismics.docs.core.dao.lucene; package com.sismics.docs.core.dao.lucene;
import java.util.HashSet; import com.sismics.docs.core.model.context.AppContext;
import java.util.List; import com.sismics.docs.core.model.jpa.Document;
import java.util.Set; import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.util.LuceneUtil;
import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Field; import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField; import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField; import org.apache.lucene.document.TextField;
import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term; import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.flexible.standard.QueryParserUtil; import org.apache.lucene.queryparser.flexible.standard.QueryParserUtil;
import org.apache.lucene.queryparser.flexible.standard.StandardQueryParser; import org.apache.lucene.queryparser.flexible.standard.StandardQueryParser;
@ -19,11 +18,9 @@ import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.TopDocs;
import com.sismics.docs.core.model.context.AppContext; import java.util.HashSet;
import com.sismics.docs.core.model.jpa.Document; import java.util.List;
import com.sismics.docs.core.model.jpa.File; import java.util.Set;
import com.sismics.docs.core.util.LuceneUtil;
import com.sismics.docs.core.util.LuceneUtil.LuceneRunnable;
/** /**
* Lucene DAO. * Lucene DAO.
@ -37,23 +34,20 @@ public class LuceneDao {
* @param fileList List of files * @param fileList List of files
*/ */
public void rebuildIndex(final List<Document> documentList, final List<File> fileList) { public void rebuildIndex(final List<Document> documentList, final List<File> fileList) {
LuceneUtil.handle(new LuceneRunnable() { LuceneUtil.handle(indexWriter -> {
@Override // Empty index
public void run(IndexWriter indexWriter) throws Exception { indexWriter.deleteAll();
// Empty index
indexWriter.deleteAll();
// Add all documents // Add all documents
for (Document document : documentList) { for (Document document : documentList) {
org.apache.lucene.document.Document luceneDocument = getDocumentFromDocument(document); org.apache.lucene.document.Document luceneDocument = getDocumentFromDocument(document);
indexWriter.addDocument(luceneDocument); indexWriter.addDocument(luceneDocument);
} }
// Add all files // Add all files
for (File file : fileList) { for (File file : fileList) {
org.apache.lucene.document.Document luceneDocument = getDocumentFromFile(file); org.apache.lucene.document.Document luceneDocument = getDocumentFromFile(file);
indexWriter.addDocument(luceneDocument); indexWriter.addDocument(luceneDocument);
}
} }
}); });
} }
@ -64,12 +58,9 @@ public class LuceneDao {
* @param document Document to add * @param document Document to add
*/ */
public void createDocument(final Document document) { public void createDocument(final Document document) {
LuceneUtil.handle(new LuceneRunnable() { LuceneUtil.handle(indexWriter -> {
@Override org.apache.lucene.document.Document luceneDocument = getDocumentFromDocument(document);
public void run(IndexWriter indexWriter) throws Exception { indexWriter.addDocument(luceneDocument);
org.apache.lucene.document.Document luceneDocument = getDocumentFromDocument(document);
indexWriter.addDocument(luceneDocument);
}
}); });
} }
@ -79,12 +70,9 @@ public class LuceneDao {
* @param file File to add * @param file File to add
*/ */
public void createFile(final File file) { public void createFile(final File file) {
LuceneUtil.handle(new LuceneRunnable() { LuceneUtil.handle(indexWriter -> {
@Override org.apache.lucene.document.Document luceneDocument = getDocumentFromFile(file);
public void run(IndexWriter indexWriter) throws Exception { indexWriter.addDocument(luceneDocument);
org.apache.lucene.document.Document luceneDocument = getDocumentFromFile(file);
indexWriter.addDocument(luceneDocument);
}
}); });
} }
@ -94,12 +82,9 @@ public class LuceneDao {
* @param document Updated document * @param document Updated document
*/ */
public void updateDocument(final Document document) { public void updateDocument(final Document document) {
LuceneUtil.handle(new LuceneRunnable() { LuceneUtil.handle(indexWriter -> {
@Override org.apache.lucene.document.Document luceneDocument = getDocumentFromDocument(document);
public void run(IndexWriter indexWriter) throws Exception { indexWriter.updateDocument(new Term("id", document.getId()), luceneDocument);
org.apache.lucene.document.Document luceneDocument = getDocumentFromDocument(document);
indexWriter.updateDocument(new Term("id", document.getId()), luceneDocument);
}
}); });
} }
@ -109,12 +94,7 @@ public class LuceneDao {
* @param id Document ID to delete * @param id Document ID to delete
*/ */
public void deleteDocument(final String id) { public void deleteDocument(final String id) {
LuceneUtil.handle(new LuceneRunnable() { LuceneUtil.handle(indexWriter -> indexWriter.deleteDocuments(new Term("id", id)));
@Override
public void run(IndexWriter indexWriter) throws Exception {
indexWriter.deleteDocuments(new Term("id", id));
}
});
} }
/** /**
@ -123,7 +103,7 @@ public class LuceneDao {
* @param searchQuery Search query on title and description * @param searchQuery Search query on title and description
* @param fullSearchQuery Search query on all fields * @param fullSearchQuery Search query on all fields
* @return List of document IDs * @return List of document IDs
* @throws Exception * @throws Exception e
*/ */
public Set<String> search(String searchQuery, String fullSearchQuery) throws Exception { public Set<String> search(String searchQuery, String fullSearchQuery) throws Exception {
// Escape query and add quotes so QueryParser generate a PhraseQuery // Escape query and add quotes so QueryParser generate a PhraseQuery

View File

@ -139,8 +139,8 @@ public class FileResource extends BaseResource {
/** /**
* Attach a file to a document. * Attach a file to a document.
* *
* @api {post} /file/:fileId Attach a file to a document * @api {post} /file/:fileId/attach Attach a file to a document
* @apiName PostFile * @apiName PostFileAttach
* @apiGroup File * @apiGroup File
* @apiParam {String} fileId File ID * @apiParam {String} fileId File ID
* @apiParam {String} id Document ID * @apiParam {String} id Document ID
@ -156,7 +156,7 @@ public class FileResource extends BaseResource {
* @return Response * @return Response
*/ */
@POST @POST
@Path("{id: [a-z0-9\\-]+}") @Path("{id: [a-z0-9\\-]+}/attach")
public Response attach( public Response attach(
@PathParam("id") String id, @PathParam("id") String id,
@FormParam("id") String documentId) { @FormParam("id") String documentId) {
@ -216,6 +216,48 @@ public class FileResource extends BaseResource {
return Response.ok().entity(response.build()).build(); return Response.ok().entity(response.build()).build();
} }
/**
* Update a file.
*
* @api {post} /file/:id Update a file
* @apiName PostFile
* @apiGroup File
* @apiParam {String} id File ID
* @apiParam {String} name Name
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ValidationError Validation error
* @apiPermission user
* @apiVersion 1.6.0
*
* @param id File ID
* @return Response
*/
@POST
@Path("{id: [a-z0-9\\-]+}")
public Response update(@PathParam("id") String id,
@FormParam("name") String name) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
// Get the file
File file = findFile(id, null);
// Validate input data
name = ValidationUtil.validateLength(name, "name", 1, 200, false);
// Update the file
FileDao fileDao = new FileDao();
file.setName(name);
fileDao.update(file);
// Always return OK
JsonObjectBuilder response = Json.createObjectBuilder()
.add("status", "ok");
return Response.ok().entity(response.build()).build();
}
/** /**
* Reorder files. * Reorder files.
* *
@ -362,24 +404,10 @@ public class FileResource extends BaseResource {
} }
// Get the file // Get the file
FileDao fileDao = new FileDao(); File file = findFile(id, null);
AclDao aclDao = new AclDao();
File file = fileDao.getFile(id);
if (file == null) {
throw new NotFoundException();
}
if (file.getDocumentId() == null) {
// It's an orphan file
if (!file.getUserId().equals(principal.getId())) {
// But not ours
throw new ForbiddenClientException();
}
} else if (!aclDao.checkPermission(file.getDocumentId(), PermType.WRITE, getTargetIdList(null))) {
throw new NotFoundException();
}
// Delete the file // Delete the file
FileDao fileDao = new FileDao();
fileDao.delete(file.getId(), principal.getId()); fileDao.delete(file.getId(), principal.getId());
// Update the user quota // Update the user quota
@ -448,29 +476,10 @@ public class FileResource extends BaseResource {
} }
// Get the file // Get the file
FileDao fileDao = new FileDao(); File file = findFile(fileId, shareId);
UserDao userDao = new UserDao();
File file = fileDao.getFile(fileId);
if (file == null) {
throw new NotFoundException();
}
if (file.getDocumentId() == null) {
// It's an orphan file
if (!file.getUserId().equals(principal.getId())) {
// But not ours
throw new ForbiddenClientException();
}
} else {
// Check document accessibility
AclDao aclDao = new AclDao();
if (!aclDao.checkPermission(file.getDocumentId(), PermType.READ, getTargetIdList(shareId))) {
throw new ForbiddenClientException();
}
}
// Get the stored file // Get the stored file
UserDao userDao = new UserDao();
java.nio.file.Path storedFile; java.nio.file.Path storedFile;
String mimeType; String mimeType;
boolean decrypt; boolean decrypt;
@ -605,4 +614,34 @@ public class FileResource extends BaseResource {
.header("Content-Disposition", "attachment; filename=\"" + documentDto.getTitle().replaceAll("\\W+", "_") + ".zip\"") .header("Content-Disposition", "attachment; filename=\"" + documentDto.getTitle().replaceAll("\\W+", "_") + ".zip\"")
.build(); .build();
} }
/**
* Find a file with access rights checking.
*
* @param fileId File ID
* @param shareId Share ID
* @return File
*/
private File findFile(String fileId, String shareId) {
FileDao fileDao = new FileDao();
File file = fileDao.getFile(fileId);
if (file == null) {
throw new NotFoundException();
}
if (file.getDocumentId() == null) {
// It's an orphan file
if (!file.getUserId().equals(principal.getId())) {
// But not ours
throw new ForbiddenClientException();
}
} else {
// Check document accessibility
AclDao aclDao = new AclDao();
if (!aclDao.checkPermission(file.getDocumentId(), PermType.READ, getTargetIdList(shareId))) {
throw new ForbiddenClientException();
}
}
return file;
}
} }

View File

@ -103,7 +103,7 @@ angular.module('docs').controller('DocumentEdit', function($rootScope, $scope, $
var attachOrphanFiles = function(data) { var attachOrphanFiles = function(data) {
var promises = []; var promises = [];
_.each($scope.orphanFiles, function(fileId) { _.each($scope.orphanFiles, function(fileId) {
promises.push(Restangular.one('file/' + fileId).post('', { id: data.id })); promises.push(Restangular.one('file/' + fileId).post('attach', { id: data.id }));
}); });
$scope.orphanFiles = []; $scope.orphanFiles = [];
return $q.all(promises); return $q.all(promises);

View File

@ -3,7 +3,7 @@
/** /**
* Document view content controller. * Document view content controller.
*/ */
angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, Upload, $translate) { angular.module('docs').controller('DocumentViewContent', function ($scope, $rootScope, $stateParams, Restangular, $dialog, $state, Upload, $translate, $uibModal) {
/** /**
* Configuration for file sorting. * Configuration for file sorting.
*/ */
@ -131,4 +131,30 @@ angular.module('docs').controller('DocumentViewContent', function ($scope, $root
} }
}); });
}; };
/**
* Rename a file.
*/
$scope.renameFile = function (file) {
$uibModal.open({
templateUrl: 'partial/docs/file.rename.html',
controller: 'FileRename',
resolve: {
file: function () {
return angular.copy(file);
}
}
}).result.then(function (fileUpdated) {
if (fileUpdated === null) {
return;
}
// Rename the file
Restangular.one('file/' + file.id).post('', {
name: fileUpdated.name
}).then(function () {
file.name = fileUpdated.name;
})
});
};
}); });

View File

@ -0,0 +1,11 @@
'use strict';
/**
* File rename controller.
*/
angular.module('docs').controller('FileRename', function ($scope, file, $uibModalInstance) {
$scope.file = file;
$scope.close = function(file) {
$uibModalInstance.close(file);
}
});

View File

@ -69,6 +69,7 @@
<script src="app/docs/controller/document/DocumentModalAddTag.js" type="text/javascript"></script> <script src="app/docs/controller/document/DocumentModalAddTag.js" type="text/javascript"></script>
<script src="app/docs/controller/document/FileView.js" type="text/javascript"></script> <script src="app/docs/controller/document/FileView.js" type="text/javascript"></script>
<script src="app/docs/controller/document/FileModalView.js" type="text/javascript"></script> <script src="app/docs/controller/document/FileModalView.js" type="text/javascript"></script>
<script src="app/docs/controller/document/FileRename.js" type="text/javascript"></script>
<script src="app/docs/controller/tag/Tag.js" type="text/javascript"></script> <script src="app/docs/controller/tag/Tag.js" type="text/javascript"></script>
<script src="app/docs/controller/tag/TagEdit.js" type="text/javascript"></script> <script src="app/docs/controller/tag/TagEdit.js" type="text/javascript"></script>
<script src="app/docs/controller/settings/Settings.js" type="text/javascript"></script> <script src="app/docs/controller/settings/Settings.js" type="text/javascript"></script>

View File

@ -194,6 +194,10 @@
"previous": "Previous", "previous": "Previous",
"next": "Next", "next": "Next",
"not_found": "File not found" "not_found": "File not found"
},
"edit": {
"title": "Edit file",
"name": "Filename"
} }
}, },
"tag": { "tag": {
@ -531,6 +535,7 @@
"export": "Export", "export": "Export",
"edit": "Edit", "edit": "Edit",
"delete": "Delete", "delete": "Delete",
"rename": "Rename",
"loading": "Loading...", "loading": "Loading...",
"send": "Send", "send": "Send",
"enabled": "Enabled", "enabled": "Enabled",

View File

@ -13,7 +13,7 @@
<img ng-src="../api/file/{{ file.id }}/data?size=thumb" uib-tooltip="{{ file.mimetype }} | {{ file.size | filesize }}" tooltip-placement="top" /> <img ng-src="../api/file/{{ file.id }}/data?size=thumb" uib-tooltip="{{ file.mimetype }} | {{ file.size | filesize }}" tooltip-placement="top" />
</a> </a>
<div class="file-info"> <div class="file-info">
<div class="caption caption-hover-inverse file-name" ng-click="openFile(file)" ng-if="file.name">{{ file.name }}</div> <div class="v-align caption caption-hover-inverse file-name" ng-click="openFile(file)" ng-if="file.name">{{ file.name }}</div>
<div class="caption caption-hover"> <div class="caption caption-hover">
<button class="btn btn-danger" ng-click="deleteFile($event, file)"><span class="fas fa-trash"></span></button> <button class="btn btn-danger" ng-click="deleteFile($event, file)"><span class="fas fa-trash"></span></button>
</div> </div>

View File

@ -55,7 +55,7 @@
</a> </a>
<div class="file-info"> <div class="file-info">
<div class="caption caption-hover-inverse file-name" ng-click="openFile(file)" ng-if="file.name">{{ file.name }}</div> <div class="v-align caption caption-hover-inverse file-name" ng-click="openFile(file)" ng-if="file.name">{{ file.name }}</div>
<div class="caption caption-hover" ng-show="document.writable"> <div class="caption caption-hover" ng-show="document.writable">
<div class="btn btn-default handle"><span class="fas fa-arrows-alt-h"></span></div> <div class="btn btn-default handle"><span class="fas fa-arrows-alt-h"></span></div>
</div> </div>
@ -67,9 +67,13 @@
</button> </button>
<ul class="dropdown-menu" uib-dropdown-menu> <ul class="dropdown-menu" uib-dropdown-menu>
<li> <li>
<a href ng-click="renameFile(file)">
<span class="fas fa-pencil-alt"></span>
{{ 'rename' | translate }}
</a>
<a href ng-click="deleteFile(file)"> <a href ng-click="deleteFile(file)">
<span class="fas fa-trash"></span> <span class="fas fa-trash"></span>
Delete {{ 'delete' | translate }}
</a> </a>
</li> </li>
</ul> </ul>

View File

@ -0,0 +1,16 @@
<div class="modal-header">
<h3>{{ 'file.edit.title' | translate }}</h3>
</div>
<form name="fileForm" novalidate>
<div class="modal-body">
<input type="text" name="name" ng-attr-placeholder="{{ 'file.edit.name' | translate }}" class="form-control"
ng-maxlength="200" required ng-model="file.name">
<span class="text-danger" ng-show="fileForm.name.$error.maxlength && fileForm.$dirty">{{ 'validation.too_long' | translate }}</span>
</div>
<div class="modal-footer">
<button ng-click="close(file)" ng-disabled="!fileForm.$valid" class="btn btn-primary">
<span class="fas fa-pencil-alt"></span> {{ 'save' | translate }}
</button>
<button ng-click="close(null)" class="btn btn-default">{{ 'cancel' | translate }}</button>
</div>
</form>

View File

@ -207,6 +207,8 @@ ul.tag-tree {
white-space: normal; white-space: normal;
display: block; display: block;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-right: auto !important;
margin-left: auto !important;
} }
.file-info { .file-info {

View File

@ -136,6 +136,24 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertEquals(file2Id, files.getJsonObject(1).getString("id")); Assert.assertEquals(file2Id, files.getJsonObject(1).getString("id"));
Assert.assertEquals("PIA00452.jpg", files.getJsonObject(1).getString("name")); Assert.assertEquals("PIA00452.jpg", files.getJsonObject(1).getString("name"));
// Rename a file
target().path("file/" + file1Id)
.request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token)
.post(Entity.form(new Form()
.param("name", "Pale Blue Dot")), JsonObject.class);
// Get all files from a document
json = target().path("/file/list")
.queryParam("id", document1Id)
.request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token)
.get(JsonObject.class);
files = json.getJsonArray("files");
Assert.assertEquals(2, files.size());
Assert.assertEquals(file1Id, files.getJsonObject(0).getString("id"));
Assert.assertEquals("Pale Blue Dot", files.getJsonObject(0).getString("name"));
// Reorder files // Reorder files
target().path("/file/reorder").request() target().path("/file/reorder").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file1Token)
@ -301,7 +319,7 @@ public class TestFileResource extends BaseJerseyTest {
Assert.assertNotNull(document1Id); Assert.assertNotNull(document1Id);
// Attach a file to a document // Attach a file to a document
target().path("/file/" + file1Id).request() target().path("/file/" + file1Id + "/attach").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token) .cookie(TokenBasedSecurityFilter.COOKIE_NAME, file3Token)
.post(Entity.form(new Form() .post(Entity.form(new Form()
.param("id", document1Id)), JsonObject.class); .param("id", document1Id)), JsonObject.class);