diff --git a/src/main/java/org/codelibs/fess/app/web/admin/storage/AdminStorageAction.java b/src/main/java/org/codelibs/fess/app/web/admin/storage/AdminStorageAction.java index 9846c40c3a5c8fcd5b712a7fd020eef4768918e5..8321b0640a907ab7504eba9ef3462bb703c16bb6 100644 --- a/src/main/java/org/codelibs/fess/app/web/admin/storage/AdminStorageAction.java +++ b/src/main/java/org/codelibs/fess/app/web/admin/storage/AdminStorageAction.java @@ -73,17 +73,25 @@ public class AdminStorageAction extends FessAdminAction { if (form.uploadFile == null) { throwValidationError(messages -> messages.addErrorsStorageNoUploadFile(GLOBAL), () -> asListHtml(form.path)); } + logger.debug("form.path = {}", form.path); verifyToken(() -> asListHtml(form.path)); - final String fileName = form.uploadFile.getFileName(); + final String objectName = getObjectName(form.path, form.uploadFile.getFileName()); try (final InputStream in = form.uploadFile.getInputStream()) { final MinioClient minioClient = createClient(fessConfig); - minioClient.putObject(fessConfig.getStorageBucket(), form.uploadFile.getFileName(), in, (long) form.uploadFile.getFileSize(), - null, null, "application/octet-stream"); + minioClient.putObject(fessConfig.getStorageBucket(), objectName, in, (long) form.uploadFile.getFileSize(), null, null, + "application/octet-stream"); } catch (final Exception e) { - throwValidationError(messages -> messages.addErrorsStorageFileUploadFailure(GLOBAL, fileName), () -> asListHtml(form.path)); + if (logger.isDebugEnabled()) { + logger.debug("Failed to upload {}", objectName, e); + } + throwValidationError(messages -> messages.addErrorsStorageFileUploadFailure(GLOBAL, e.getLocalizedMessage()), + () -> asListHtml(form.path)); + } + saveInfo(messages -> messages.addSuccessUploadFileToStorage(GLOBAL, form.uploadFile.getFileName())); + if (StringUtil.isEmpty(form.path)) { + return redirect(getClass()); } - saveInfo(messages -> messages.addSuccessUploadFileToStorage(GLOBAL, fileName)); - return redirect(getClass()); // no-op + return redirectWith(getClass(), moreUrl("list/" + encodeId(form.path))); } @Execute @@ -96,7 +104,7 @@ public class AdminStorageAction extends FessAdminAction { public ActionResponse download(final String id) { final String[] values = decodeId(id); if (StringUtil.isEmpty(values[1])) { - throwValidationError(messages -> messages.addErrorsStorageFileNotFound(GLOBAL), () -> asListHtml(values[0])); + throwValidationError(messages -> messages.addErrorsStorageFileNotFound(GLOBAL), () -> asListHtml(encodeId(values[0]))); } return asStream(values[1]).contentTypeOctetStream().stream( out -> { @@ -107,11 +115,43 @@ public class AdminStorageAction extends FessAdminAction { logger.debug("Failed to access {}", fessConfig.getStorageEndpoint(), e); } throwValidationError(messages -> messages.addErrorsStorageAccessError(GLOBAL, e.getLocalizedMessage()), - () -> asListHtml(values[0])); + () -> asListHtml(encodeId(values[0]))); } }); } + @Execute + public HtmlResponse delete(final String id) { + final String[] values = decodeId(id); + if (StringUtil.isEmpty(values[1])) { + throwValidationError(messages -> messages.addErrorsStorageFileNotFound(GLOBAL), () -> asListHtml(encodeId(values[0]))); + } + logger.debug("values[0] = {}, values[1] = {}", values[0], values[1]); + final String objectName = getObjectName(values[0], values[1]); + try { + final MinioClient minioClient = createClient(fessConfig); + minioClient.removeObject(fessConfig.getStorageBucket(), objectName); + } catch (final Exception e) { + logger.debug("Failed to delete {}", values[1], e); + throwValidationError(messages -> messages.addErrorsFailedToDeleteFile(GLOBAL, e.getLocalizedMessage()), + () -> asListHtml(encodeId(values[0]))); + } + saveInfo(messages -> messages.addSuccessDeleteFile(GLOBAL, values[1])); + if (StringUtil.isEmpty(values[0])) { + return redirect(getClass()); + } + return redirectWith(getClass(), moreUrl("list/" + encodeId(values[0]))); + } + + @Execute + public HtmlResponse createDir(final ItemForm form) { + validate(form, messages -> {}, () -> asListHtml(form.path)); + if (StringUtil.isBlank(form.name)) { + throwValidationError(messages -> messages.addErrorsStorageDirectoryNameIsInvalid(GLOBAL), () -> asListHtml(form.path)); + } + return redirectWith(getClass(), moreUrl("list/" + encodeId(getObjectName(form.path, form.name)))); + } + public static List<Map<String, Object>> getFileItems(final String prefix) { final FessConfig fessConfig = ComponentUtil.getFessConfig(); final ArrayList<Map<String, Object>> list = new ArrayList<>(); @@ -124,7 +164,8 @@ public class AdminStorageAction extends FessAdminAction { final String objectName = item.objectName(); map.put("id", URLEncoder.encode(objectName, Constants.UTF_8_CHARSET)); map.put("name", getName(objectName)); - map.put("size", item.size()); + map.put("hashCode", item.hashCode()); + map.put("size", item.objectSize()); map.put("directory", item.isDir()); if (!item.isDir()) { map.put("lastModified", item.lastModified()); @@ -201,7 +242,7 @@ public class AdminStorageAction extends FessAdminAction { } buf.append(values[i]); } - return URLEncoder.encode(buf.toString(), Constants.UTF_8_CHARSET); + return urlEncode(buf.toString()); } return StringUtil.EMPTY; } @@ -215,13 +256,32 @@ public class AdminStorageAction extends FessAdminAction { } buf.append(s); final Map<String, String> map = new HashMap<>(); - map.put("id", URLEncoder.encode(buf.toString(), Constants.UTF_8_CHARSET)); + map.put("id", urlEncode(buf.toString())); map.put("name", s); list.add(map); })); return list; } + protected static String getPathPrefix(final String path) { + return StringUtil.isEmpty(path) ? StringUtil.EMPTY : path + "/"; + } + + protected static String getObjectName(final String path, final String name) { + return getPathPrefix(path) + name; + } + + protected static String urlEncode(final String str) { + if (str == null) { + return null; + } + return URLEncoder.encode(str, Constants.UTF_8_CHARSET); + } + + protected static String encodeId(final String str) { + return urlEncode(urlEncode(str)); + } + private HtmlResponse asListHtml(final String prefix) { return asHtml(path_AdminStorage_AdminStorageJsp).useForm(ItemForm.class).renderWith(data -> { RenderDataUtil.register(data, "endpoint", fessConfig.getStorageEndpoint()); diff --git a/src/main/java/org/codelibs/fess/app/web/admin/storage/ItemForm.java b/src/main/java/org/codelibs/fess/app/web/admin/storage/ItemForm.java index 3b6a6a90e3ed8e7f59e12d144da143ccc05f4f68..a2ea1577bb64f7c76dec1a21c280747be0eca7e5 100644 --- a/src/main/java/org/codelibs/fess/app/web/admin/storage/ItemForm.java +++ b/src/main/java/org/codelibs/fess/app/web/admin/storage/ItemForm.java @@ -18,10 +18,9 @@ package org.codelibs.fess.app.web.admin.storage; import javax.validation.constraints.Size; import org.lastaflute.web.ruts.multipart.MultipartFormFile; -import org.lastaflute.web.validation.Required; public class ItemForm { - @Required + public String path; @Size(max = 100) diff --git a/src/main/java/org/codelibs/fess/mylasta/action/FessMessages.java b/src/main/java/org/codelibs/fess/mylasta/action/FessMessages.java index 3a0cd40ad19a4d7e0d17fbc736b4131d1a9d2b88..4bf3a75f13a9ef04842a8bca4cf10a480702b807 100644 --- a/src/main/java/org/codelibs/fess/mylasta/action/FessMessages.java +++ b/src/main/java/org/codelibs/fess/mylasta/action/FessMessages.java @@ -419,6 +419,9 @@ public class FessMessages extends FessLabels { /** The key of the message: Upload file is required. */ public static final String ERRORS_storage_no_upload_file = "{errors.storage_no_upload_file}"; + /** The key of the message: Directory name is invalid. */ + public static final String ERRORS_storage_directory_name_is_invalid = "{errors.storage_directory_name_is_invalid}"; + /** The key of the message: Updated parameters. */ public static final String SUCCESS_update_crawler_params = "{success.update_crawler_params}"; @@ -2414,6 +2417,20 @@ public class FessMessages extends FessLabels { return this; } + /** + * Add the created action message for the key 'errors.storage_directory_name_is_invalid' with parameters. + * <pre> + * message: Directory name is invalid. + * </pre> + * @param property The property name for the message. (NotNull) + * @return this. (NotNull) + */ + public FessMessages addErrorsStorageDirectoryNameIsInvalid(String property) { + assertPropertyNotNull(property); + add(property, new UserMessage(ERRORS_storage_directory_name_is_invalid)); + return this; + } + /** * Add the created action message for the key 'success.update_crawler_params' with parameters. * <pre> diff --git a/src/main/resources/fess_message.properties b/src/main/resources/fess_message.properties index 3f0c3c7034486ae3a748b2dcc2055aa2b21dbcd3..c9bed1343ad4422c6537e747da7cd19ec7fd245d 100644 --- a/src/main/resources/fess_message.properties +++ b/src/main/resources/fess_message.properties @@ -165,6 +165,7 @@ errors.storage_file_not_found=The target file is not found in Storage. errors.storage_file_download_failure=Failed to download {0}. errors.storage_access_error=Storage access error: {0} errors.storage_no_upload_file=Upload file is required. +errors.storage_directory_name_is_invalid=Directory name is invalid. success.update_crawler_params=Updated parameters. success.delete_doc_from_index=Started a process to delete the document from index. diff --git a/src/main/webapp/WEB-INF/view/admin/storage/admin_storage.jsp b/src/main/webapp/WEB-INF/view/admin/storage/admin_storage.jsp index 6666a627323de06c6c1bc70d806080cf2b93e267..8270a7228ce3a9f73d2caec62d41447e1ce04745 100644 --- a/src/main/webapp/WEB-INF/view/admin/storage/admin_storage.jsp +++ b/src/main/webapp/WEB-INF/view/admin/storage/admin_storage.jsp @@ -42,20 +42,100 @@ <div class="data-wrapper"> <div class="row"> <div class="col-sm-12"> - Path: ${f:h(endpoint)}/${f:h(bucket)}<c:forEach var="item" varStatus="s" items="${pathItems}">/<a href="${contextPath}/admin/storage/list/${f:u(item.id)}/">${f:h(item.name)}</a></c:forEach> + <a class="fa fa-home" aria-hidden="true" href="${contextPath}/admin/storage/">(Bucket: ${f:h(endpoint)}/${f:h(bucket)})</a> + <c:forEach var="item" varStatus="s" items="${pathItems}"> + <i class="fa fa-chevron-right" aria-hidden="true"></i> + <span><a href="${contextPath}/admin/storage/list/${f:u(item.id)}/">${f:h(item.name)}</a></span> + </c:forEach> + <i class="fa fa-chevron-right" aria-hidden="true"></i> + + <div type="button" class="btn btn-success btn-xs" name="createDir" data-toggle="modal" + data-target="#createDir"> + <i class="fa fa-plus" aria-hidden="true"></i> + </div> + + <div class="modal modal-primary" id="createDir" + tabindex="-1" role="dialog" + > + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + <h4 class="modal-title"> + <la:message key="labels.crud_title_create" /> + </h4> + </div> + <div class="modal-body col-sm-12"> + <la:form action="/admin/storage/createDir/" enctype="multipart/form-data" styleClass="form-inline"> + <div class="form-group"> + <input type="text" name="name" class="form-control" /> + </div> + <input type="hidden" name="path" value="${path}" /> + <button type="submit" class="btn btn-success" name="createDir"> + <em class="fa fa-make"></em> + <la:message key="labels.crud_button_create" /> + </button> + </la:form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-outline pull-left" data-dismiss="modal"> + <la:message key="labels.crud_button_cancel" /> + </button> + </div> + </div> + </div> + </div> </div> + </div> + + <div class="row"> <div class="col-sm-12"> - <la:form action="/admin/storage/upload/" enctype="multipart/form-data" styleClass="form-inline"> - <div class="form-group"> - <label for="uploadFile"> <la:message key="labels.storage_upload_file" /> - </label> <input type="file" name="uploadFile" class="form-control" /> + <div type="button" class="btn btn-success pull-right" name="upload" data-toggle="modal" + data-target="#uploadeFile" + value="<la:message key="labels.storage_button_upload" />" + > + <em class="fa fa-upload"></em> + <la:message key="labels.storage_button_upload" /> + </div> + <div class="modal modal-primary" id="uploadeFile" + tabindex="-1" role="dialog" + > + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + <h4 class="modal-title"> + <la:message key="labels.storage_upload_file" /> + </h4> + </div> + <div class="modal-body col-sm-12"> + <la:form action="/admin/storage/upload/" enctype="multipart/form-data" styleClass="form-inline"> + <div class="form-group"> + <input type="file" name="uploadFile" class="form-control" /> + </div> + <input type="hidden" name="path" value="${path}" /> + <button type="submit" class="btn btn-success" name="upload"> + <em class="fa fa-upload"></em> + <la:message key="labels.storage_button_upload" /> + </button> + </la:form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-outline pull-left" data-dismiss="modal"> + <la:message key="labels.crud_button_cancel" /> + </button> + </div> + </div> </div> - <button type="submit" class="btn btn-success" name="upload"> - <em class="fa fa-upload"></em> - <la:message key="labels.storage_button_upload" /> - </button> - </la:form> + </div> </div> + </div> + + <div class="row"> <div class="col-sm-12"> <table class="table table-bordered table-striped dataTable"> <tbody> @@ -77,23 +157,81 @@ <td>..</td> <td></td> <td></td> + <td></td> </tr></c:if> <c:forEach var="data" varStatus="s" items="${fileItems}"> <c:if test="${not data.directory}"> - <tr - data-href="${contextPath}/admin/storage/download/${f:u(data.id)}/"> - <td>${f:h(data.name)}</td> + <tr> + <td> + <em class="fa fa-file"></em> + ${f:h(data.name)} + </td> <td>${f:h(data.size)}</td> - <td>${f:h(data.lastModifed)}</td> - </tr> - </c:if><c:if test="${data.directory.booleanValue()}"> + <td>${f:h(data.lastModified)}</td> + </c:if> + <c:if test="${data.directory.booleanValue()}"> <tr data-href="${contextPath}/admin/storage/list/${f:u(data.id)}/"> - <td>${f:h(data.name)}</td> + <td> + <em class="fa fa-folder-open"></em> + ${f:h(data.name)} + </td> <td></td> <td></td> - </tr> </c:if> + <td> + <c:if test="${not data.directory}"> + <a class="btn btn-primary btn-xs" role="button" name="download" data-toggle="modal" + href="${contextPath}/admin/storage/download/${f:u(data.id)}/" download="${f:u(data.name)}" + value="<la:message key="labels.design_download_button" />" + > + <em class="fa fa-download"></em> + <la:message key="labels.design_download_button" /> + </a> + <button type="button" class="btn btn-danger btn-xs" name="delete" data-toggle="modal" + data-target="#confirmToDelete-${f:h(data.hashCode)}" + value="<la:message key="labels.design_delete_button" />" + > + <em class="fa fa-times"></em> + <la:message key="labels.design_delete_button" /> + </button> + <div class="modal modal-danger fade" id="confirmToDelete-${f:h(data.hashCode)}" + tabindex="-1" role="dialog" + > + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + <h4 class="modal-title"> + <la:message key="labels.crud_title_delete" /> : ${f:h(data.name)} + </h4> + </div> + <div class="modal-body"> + <p> + <la:message key="labels.crud_delete_confirmation" /> + </p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-outline pull-left" data-dismiss="modal"> + <la:message key="labels.crud_button_cancel" /> + </button> + <la:form action="${contextPath}/admin/storage/delete/${f:u(data.id)}/" styleClass="form-horizontal"> + <button type="submit" class="btn btn-outline btn-danger" name="delete" + value="<la:message key="labels.crud_button_delete" />" + > + <em class="fa fa-trash"></em> + <la:message key="labels.crud_button_delete" /> + </button> + </la:form> + </div> + </div> + </div> + </div> + </c:if> + </td> + </tr> </c:forEach> </tbody> </table> @@ -114,4 +252,3 @@ <jsp:include page="/WEB-INF/view/common/admin/foot.jsp"></jsp:include> </body> </html> -