From 471df1e6016328ef652eb808d0c2f4141b01545c Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 19 Mar 2018 23:27:50 +0100 Subject: [PATCH 1/6] #29 - fix: redirect with context path + error handling --- .../AdminToolFilebrowserController.java | 484 +++++++++--------- 1 file changed, 254 insertions(+), 230 deletions(-) diff --git a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserController.java b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserController.java index 7495b69..e6b05c5 100644 --- a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserController.java +++ b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserController.java @@ -1,230 +1,254 @@ -package de.chandre.admintool.filebrowser; - -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.List; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.servlet.ModelAndView; - -import de.chandre.admintool.core.AdminTool; -import de.chandre.admintool.core.controller.AbstractAdminController; - -/** - * Filebrowser controller
- * requires admintool-core 1.0.1
- * @author Andre - * @since 1.0.1 - */ -@Controller -@RequestMapping(AdminTool.ROOTCONTEXT + "/filebrowser") -public class AdminToolFilebrowserController extends AbstractAdminController { - - private static final Log LOGGER = LogFactory.getLog(AdminToolFilebrowserController.class); - - @Autowired - private AdminToolFilebrowserService filebrowserService; - - @Autowired - private AdminToolFilebrowserConfig filebrowserConfig; - - @RequestMapping(value = {"", "/","/dir"}, method={RequestMethod.GET, RequestMethod.POST}) - public String showDirectory(@RequestParam(name = "dir", required = false) String dirPath, - @RequestParam(name = "sortCol", required = false) String sortCol, - @RequestParam(name = "sortAsc", required = false, defaultValue = "true") boolean sortType, - @RequestParam(name = "filter", required = false) String filter, - ModelMap model, HttpServletRequest request) throws UnsupportedEncodingException { - if (!filebrowserConfig.isEnabled()) { - return null; - } - String currentDir = StringUtils.isEmpty(dirPath) ? filebrowserConfig.getStartDir().getAbsolutePath() : URLDecoder.decode(dirPath, "UTF-8"); - if(LOGGER.isTraceEnabled()) LOGGER.trace("show directory: " + currentDir); - String templatePath = addCommonContextVars(model, request, "filebrowser", null); - model.put("currentDir", currentDir); - model.put("sortCol", SortColumn.fromIndex(sortCol)); - model.put("sortAsc", sortType); - model.put("filter", filter); - - return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + templatePath; - } - - @RequestMapping(value = {"/info"}, method={RequestMethod.GET, RequestMethod.POST}) - public String info(@RequestParam("file") String filePath, ModelMap model, HttpServletRequest request, - HttpServletResponse response) throws IOException, DownloadNotAllowedException, GenericFilebrowserException { - if (!filebrowserConfig.isEnabled()) { - return null; - } - String decodedPath = URLDecoder.decode(filePath, "UTF-8"); - if(LOGGER.isTraceEnabled()) LOGGER.trace("info: " + decodedPath); - addCommonContextVars(model, request); - model.addAttribute("infos", filebrowserService.getFileInfo(decodedPath)); - return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + "filebrowser/includes/fileInfo.inc"; - } - - @RequestMapping(value = {"/file"}, method={RequestMethod.GET, RequestMethod.POST}) - public void showFile(@RequestParam("file") String filePath, ModelMap model, HttpServletRequest request, - HttpServletResponse response) throws IOException, DownloadNotAllowedException, GenericFilebrowserException { - if (!filebrowserConfig.isEnabled()) { - return; - } - if (!filebrowserConfig.isDownloadAllowed()) { - throw new DownloadNotAllowedException("file download is deactivated by configuration"); - } - String decodedPath = URLDecoder.decode(filePath, "UTF-8"); - if(LOGGER.isTraceEnabled()) LOGGER.trace("download file: " + decodedPath); - filebrowserService.downloadFile(decodedPath, response, false); - } - - @RequestMapping(value = {"/download"}, method={RequestMethod.GET, RequestMethod.POST}) - public void download(@RequestParam("file") String filePath, ModelMap model, HttpServletRequest request, - HttpServletResponse response) throws IOException, DownloadNotAllowedException, GenericFilebrowserException { - if (!filebrowserConfig.isEnabled()) { - return; - } - if (!filebrowserConfig.isDownloadAllowed()) { - throw new DownloadNotAllowedException("file download is deactivated by configuration"); - } - String decodedPath = URLDecoder.decode(filePath, "UTF-8"); - if(LOGGER.isTraceEnabled()) LOGGER.trace("download file: " + decodedPath); - filebrowserService.downloadFile(decodedPath, response, true); - } - - @RequestMapping(value = {"/zip"}, method={RequestMethod.GET, RequestMethod.POST}) - public void downloadAsZip(@RequestParam("selectedFile") List filePaths, ModelMap model, HttpServletRequest request, - HttpServletResponse response) throws IOException, DownloadNotAllowedException, GenericFilebrowserException { - if (!filebrowserConfig.isEnabled()) { - return; - } - if (!filebrowserConfig.isDownloadCompressedAllowed()) { - throw new DownloadNotAllowedException("compressed file download is deactivated by configuration"); - } - List decodedPaths = new ArrayList<>(); - if (null != filePaths) { - filePaths.forEach(filePath ->{ - try { - decodedPaths.add(URLDecoder.decode(filePath, "UTF-8")); - } catch (UnsupportedEncodingException e) { - LOGGER.error(e.getMessage(), e); - } - }); - } - - if(LOGGER.isTraceEnabled()) LOGGER.trace("downloadAsZip file: " + decodedPaths.size()); - filebrowserService.downloadFilesAsZip(decodedPaths, response); - } - - @RequestMapping(value = {"/upload"}, method={RequestMethod.POST}) - @ResponseBody - public FileUploadResponse upload(@RequestParam("qqfile") MultipartFile qqfile, - @RequestParam("qqfilename") String qqfilename, - @RequestParam("qquuid") String qquuid, - @RequestParam("currentDir") String currentDir, - HttpServletRequest request, HttpServletResponse response) - throws IOException, GenericFilebrowserException { - if (!filebrowserConfig.isEnabled()) { - return null; - } - if(LOGGER.isTraceEnabled()) LOGGER.trace("upload file: " + qqfilename); - FileUploadResponse fur = new FileUploadResponse(); - if (!filebrowserConfig.isUploadAllowed()) { - fur.setError("file upload is not allowed"); - return fur; - } - String decodedPath = URLDecoder.decode(currentDir, "UTF-8"); - - try { - fur.setSuccess(filebrowserService.saveFile(decodedPath, qqfile)); - if (!fur.isSuccess()) { - fur.setError("unable to opload file"); - } - } catch (Exception e) { - fur.setError(e.getMessage()); - } - - return fur; - } - - @RequestMapping(value = {"/createFolder"}, method={RequestMethod.POST}) - public void createFolder(@RequestParam("folderName") String folderPath, @RequestParam("currentDir") String currentDir, - ModelMap model, HttpServletRequest request, HttpServletResponse response) - throws IOException, GenericFilebrowserException { - if (!filebrowserConfig.isEnabled()) { - return; - } - if (!filebrowserConfig.isCreateFolderAllowed()) { - throw new GenericFilebrowserException("folder creation not allowed"); - } - String decodedPath = URLDecoder.decode(currentDir, "UTF-8"); - String decodedFolder = URLDecoder.decode(folderPath, "UTF-8"); - if(LOGGER.isTraceEnabled()) LOGGER.trace("create folder: " + decodedFolder + " in path: " + decodedPath); - String path = filebrowserService.createFolder(decodedPath, decodedFolder); - if (null != path) { - path = URLEncoder.encode(path, "UTF-8"); - } else { - path = folderPath; - } - response.sendRedirect(AdminTool.ROOTCONTEXT + "/filebrowser?dir=" + path); - } - - @RequestMapping(value = {"/delete"}, method={RequestMethod.POST}) - public void deleteResource( - @RequestParam(name ="file", required=false) String filePath, - ModelMap model, HttpServletRequest request, HttpServletResponse response) - throws IOException, DownloadNotAllowedException, GenericFilebrowserException { - if (!filebrowserConfig.isEnabled()) { - return; - } - String decodedPath = URLDecoder.decode(filePath, "UTF-8"); - if(LOGGER.isTraceEnabled()) LOGGER.trace("delete resource: " + decodedPath); - - String path = filebrowserService.deleteResource(decodedPath); - if (null != path) { - path = URLEncoder.encode(path, "UTF-8"); - } else { - path = filePath; - } - response.sendRedirect(AdminTool.ROOTCONTEXT + "/filebrowser?dir=" + path); - } - - @ExceptionHandler({DownloadNotAllowedException.class, GenericFilebrowserException.class}) - public ModelAndView handleException(Exception exception, HttpServletRequest request) throws IOException { - if(LOGGER.isTraceEnabled()) LOGGER.trace("handleException: " + exception.getMessage()); - - ModelAndView mv = new ModelAndView(AdminTool.GENERIC_ERROR_TPL_PATH); - addCommonContextVars(mv.getModelMap(), request, "filebrowser", null); - - String lastFile = request.getParameter("file"); - if (StringUtils.isEmpty(lastFile)) { - lastFile = request.getParameter("selectedFile"); - } - String decodedPath = URLDecoder.decode(lastFile, "UTF-8"); - LOGGER.info("lastFile: " + lastFile); - if (StringUtils.hasLength(decodedPath) && - filebrowserService.isAllowed(new File(decodedPath).getParentFile(), false, filebrowserConfig.isReadOnly()) ) { - mv.getModelMap().put("currentDir", new File(decodedPath).getParent()); - } else { - mv.getModelMap().put("currentDir", filebrowserConfig.getStartDir().getAbsolutePath()); - } - mv.getModelMap().put("exceptionMessage", exception.getMessage()); - return mv; - } - -} +package de.chandre.admintool.filebrowser; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; + +import de.chandre.admintool.core.AdminTool; +import de.chandre.admintool.core.controller.AbstractAdminController; + +/** + * Filebrowser controller
+ * requires admintool-core 1.0.1
+ * @author Andre + * @since 1.0.1 + */ +@Controller +@RequestMapping(AdminTool.ROOTCONTEXT + "/filebrowser") +public class AdminToolFilebrowserController extends AbstractAdminController { + + private static final Log LOGGER = LogFactory.getLog(AdminToolFilebrowserController.class); + + @Autowired + private AdminToolFilebrowserService filebrowserService; + + @Autowired + private AdminToolFilebrowserConfig filebrowserConfig; + + @RequestMapping(value = {"", "/","/dir"}, method={RequestMethod.GET, RequestMethod.POST}) + public String showDirectory(@RequestParam(name = "dir", required = false) String dirPath, + @RequestParam(name = "sortCol", required = false) String sortCol, + @RequestParam(name = "sortAsc", required = false, defaultValue = "true") boolean sortType, + @RequestParam(name = "filter", required = false) String filter, + ModelMap model, HttpServletRequest request) throws UnsupportedEncodingException { + if (!filebrowserConfig.isEnabled()) { + return null; + } + String currentDir = StringUtils.isEmpty(dirPath) ? filebrowserConfig.getStartDir().getAbsolutePath() : URLDecoder.decode(dirPath, "UTF-8"); + if(LOGGER.isTraceEnabled()) LOGGER.trace("show directory: " + currentDir); + String templatePath = addCommonContextVars(model, request, "filebrowser", null); + model.put("currentDir", currentDir); + model.put("sortCol", SortColumn.fromIndex(sortCol)); + model.put("sortAsc", sortType); + model.put("filter", filter); + + return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + templatePath; + } + + @RequestMapping(value = {"/info"}, method={RequestMethod.GET, RequestMethod.POST}) + public String info(@RequestParam("file") String filePath, ModelMap model, HttpServletRequest request, + HttpServletResponse response) throws IOException, DownloadNotAllowedException, GenericFilebrowserException { + if (!filebrowserConfig.isEnabled()) { + return null; + } + String decodedPath = URLDecoder.decode(filePath, "UTF-8"); + if(LOGGER.isTraceEnabled()) LOGGER.trace("info: " + decodedPath); + addCommonContextVars(model, request); + model.addAttribute("infos", filebrowserService.getFileInfo(decodedPath)); + return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + "filebrowser/includes/fileInfo.inc"; + } + + @RequestMapping(value = {"/file"}, method={RequestMethod.GET, RequestMethod.POST}) + public void showFile(@RequestParam("file") String filePath, ModelMap model, HttpServletRequest request, + HttpServletResponse response) throws IOException, DownloadNotAllowedException, GenericFilebrowserException { + if (!filebrowserConfig.isEnabled()) { + return; + } + if (!filebrowserConfig.isDownloadAllowed()) { + throw new DownloadNotAllowedException("file download is deactivated by configuration"); + } + String decodedPath = URLDecoder.decode(filePath, "UTF-8"); + if(LOGGER.isTraceEnabled()) LOGGER.trace("download file: " + decodedPath); + filebrowserService.downloadFile(decodedPath, response, false); + } + + @RequestMapping(value = {"/download"}, method={RequestMethod.GET, RequestMethod.POST}) + public void download(@RequestParam("file") String filePath, ModelMap model, HttpServletRequest request, + HttpServletResponse response) throws IOException, DownloadNotAllowedException, GenericFilebrowserException { + if (!filebrowserConfig.isEnabled()) { + return; + } + if (!filebrowserConfig.isDownloadAllowed()) { + throw new DownloadNotAllowedException("file download is deactivated by configuration"); + } + String decodedPath = URLDecoder.decode(filePath, "UTF-8"); + if(LOGGER.isTraceEnabled()) LOGGER.trace("download file: " + decodedPath); + filebrowserService.downloadFile(decodedPath, response, true); + } + + @RequestMapping(value = {"/zip"}, method={RequestMethod.GET, RequestMethod.POST}) + public void downloadAsZip(@RequestParam("selectedFile") List filePaths, ModelMap model, HttpServletRequest request, + HttpServletResponse response) throws IOException, DownloadNotAllowedException, GenericFilebrowserException { + if (!filebrowserConfig.isEnabled()) { + return; + } + if (!filebrowserConfig.isDownloadCompressedAllowed()) { + throw new DownloadNotAllowedException("compressed file download is deactivated by configuration"); + } + List decodedPaths = new ArrayList<>(); + if (null != filePaths) { + filePaths.forEach(filePath ->{ + try { + decodedPaths.add(URLDecoder.decode(filePath, "UTF-8")); + } catch (UnsupportedEncodingException e) { + LOGGER.error(e.getMessage(), e); + } + }); + } + + if(LOGGER.isTraceEnabled()) LOGGER.trace("downloadAsZip file: " + decodedPaths.size()); + filebrowserService.downloadFilesAsZip(decodedPaths, response); + } + + @RequestMapping(value = {"/upload"}, method={RequestMethod.POST}) + @ResponseBody + public FileUploadResponse upload(@RequestParam("qqfile") MultipartFile qqfile, + @RequestParam("qqfilename") String qqfilename, + @RequestParam("qquuid") String qquuid, + @RequestParam("currentDir") String currentDir, + HttpServletRequest request, HttpServletResponse response) + throws IOException, GenericFilebrowserException { + if (!filebrowserConfig.isEnabled()) { + return null; + } + if(LOGGER.isTraceEnabled()) LOGGER.trace("upload file: " + qqfilename); + FileUploadResponse fur = new FileUploadResponse(); + if (!filebrowserConfig.isUploadAllowed()) { + fur.setError("file upload is not allowed"); + return fur; + } + String decodedPath = URLDecoder.decode(currentDir, "UTF-8"); + + try { + fur.setSuccess(filebrowserService.saveFile(decodedPath, qqfile)); + if (!fur.isSuccess()) { + fur.setError("unable to opload file"); + } + } catch (Exception e) { + fur.setError(e.getMessage()); + } + + return fur; + } + + @RequestMapping(value = {"/createFolder"}, method={RequestMethod.POST}) + public String createFolder(@RequestParam("folderName") String folderPath, @RequestParam("currentDir") String currentDir, + ModelMap model, HttpServletRequest request, HttpServletResponse response) + throws IOException, GenericFilebrowserException { + if (!filebrowserConfig.isEnabled()) { + return null; + } + if (!filebrowserConfig.isCreateFolderAllowed()) { + throw new GenericFilebrowserException("folder creation not allowed"); + } + String decodedPath = URLDecoder.decode(currentDir, "UTF-8"); + String decodedFolder = URLDecoder.decode(folderPath, "UTF-8"); + if(LOGGER.isTraceEnabled()) LOGGER.trace("create folder: " + decodedFolder + " in path: " + decodedPath); + + String path = null; + try { + path = filebrowserService.createFolder(decodedPath, decodedFolder); + } catch (GenericFilebrowserException e) { + String templatePath = addCommonContextVars(model, request, "filebrowser", null); + model.put("exceptionMessage", e.getMessage()); + model.put("currentDir", decodedPath); + return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + templatePath; + } + if (null != path) { + path = URLEncoder.encode(path, "UTF-8"); + } else { + path = folderPath; + } + response.sendRedirect(getRootContext(request)+ "/filebrowser?dir=" + path); + return null; + } + + @RequestMapping(value = {"/delete"}, method={RequestMethod.POST}) + public String deleteResource( + @RequestParam(name ="file", required=false) String filePath, + ModelMap model, HttpServletRequest request, HttpServletResponse response) + throws IOException { + if (!filebrowserConfig.isEnabled()) { + return null; + } + + String decodedPath = URLDecoder.decode(filePath, "UTF-8"); + LOGGER.info("delete resource: " + decodedPath); + + String path = null; + try { + path = filebrowserService.deleteResource(decodedPath); + } catch (GenericFilebrowserException e) { + String templatePath = addCommonContextVars(model, request, "filebrowser", null); + model.put("exceptionMessage", e.getMessage()); + File currentPath = StringUtils.isEmpty(decodedPath) ? filebrowserConfig.getStartDir() : new File(decodedPath); + model.put("currentDir", currentPath.isDirectory() ? currentPath : currentPath.getParent()); + return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + templatePath; + } + if (null != path) { + path = URLEncoder.encode(path, "UTF-8"); + } else { + path = filePath; + } + response.sendRedirect(getRootContext(request) + "/filebrowser?dir=" + path); + return null; + } + + @ExceptionHandler({DownloadNotAllowedException.class, GenericFilebrowserException.class}) + public ModelAndView handleException(Exception exception, HttpServletRequest request) throws IOException { + if(LOGGER.isTraceEnabled()) LOGGER.trace("handleException: " + exception.getMessage()); + + ModelAndView mv = new ModelAndView(AdminTool.GENERIC_ERROR_TPL_PATH); + addCommonContextVars(mv.getModelMap(), request, "filebrowser", null); + + String lastFile = request.getParameter("file"); + if (StringUtils.isEmpty(lastFile)) { + lastFile = request.getParameter("selectedFile"); + } + String decodedPath = null; + if (StringUtils.hasLength(lastFile)) { + decodedPath = URLDecoder.decode(lastFile, "UTF-8"); + } + LOGGER.info("lastFile: " + lastFile); + if (StringUtils.hasLength(decodedPath) && + filebrowserService.isAllowed(new File(decodedPath).getParentFile(), false, filebrowserConfig.isReadOnly()) ) { + mv.getModelMap().put("currentDir", new File(decodedPath).getParent()); + } else { + mv.getModelMap().put("currentDir", filebrowserConfig.getStartDir().getAbsolutePath()); + } + mv.getModelMap().put("exceptionMessage", exception.getMessage()); + return mv; + } + +} From 1b07f74ed730a770f56e2b1b1b68c08ba28ed14a Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 19 Mar 2018 23:42:06 +0100 Subject: [PATCH 2/6] #30 - allow to display readOnly file in write mode + exception handling --- .../AdminToolFileviewerController.java | 202 +++++++++++------- .../AdminToolFileviewerServiceImpl.java | 155 +++++++------- 2 files changed, 198 insertions(+), 159 deletions(-) diff --git a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/fileviewer/AdminToolFileviewerController.java b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/fileviewer/AdminToolFileviewerController.java index 05effde..f3bdc16 100644 --- a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/fileviewer/AdminToolFileviewerController.java +++ b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/fileviewer/AdminToolFileviewerController.java @@ -1,82 +1,120 @@ -package de.chandre.admintool.fileviewer; - -import java.io.File; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; - -import javax.servlet.http.HttpServletRequest; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.ui.ModelMap; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; - -import de.chandre.admintool.core.AdminTool; -import de.chandre.admintool.core.controller.AbstractAdminController; -import de.chandre.admintool.filebrowser.AdminToolFilebrowserLoader; -import de.chandre.admintool.filebrowser.GenericFilebrowserException; - -/** - * Fileviewer controller
- * requires admintool-core 1.0.1
- * @author Andre - * @since 1.0.1 - */ -@Controller -@RequestMapping(AdminTool.ROOTCONTEXT + "/fileviewer") -public class AdminToolFileviewerController extends AbstractAdminController { - - private static final Log LOGGER = LogFactory.getLog(AdminToolFileviewerController.class); - - @Autowired - private AdminToolFileviewerService filebrowserService; - - @Autowired - private AdminToolFileviewerConfig fileviewerConfig; - - - @RequestMapping(value = {"/show",}, method={RequestMethod.GET, RequestMethod.POST}) - public String loadFile(@RequestParam("file") String file, @RequestParam(name="encoding", required=false) String encoding, - ModelMap model, HttpServletRequest request) throws GenericFilebrowserException, UnsupportedEncodingException { - if (!fileviewerConfig.isEnabled()) { - return null; - } - if(LOGGER.isTraceEnabled()) LOGGER.trace("serving file viewer page for file: " + file + ", encoding: " + encoding); - String templatePath = addCommonContextVars(model, request, "filebrowser", AdminToolFilebrowserLoader.TARGET_FILEVIEWER); - - String decodedPath = URLDecoder.decode(file, "UTF-8"); - File currentFile = new File(decodedPath); - model.put("currentDir", currentFile.getParent()); - - filebrowserService.isFileAllowed(currentFile, false); - - model.put("currentFile", currentFile); - model.put("selEncoding", StringUtils.isEmpty(encoding) ? fileviewerConfig.getDefaultEncoding() : encoding); - - return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + templatePath; - } - - @RequestMapping(value = {"/update",}, method={RequestMethod.POST}) - public String updateFile(@RequestParam("file") String file, @RequestParam(name="encoding", required=false) String encoding, - @RequestParam("fileContent") String fileContent, - ModelMap model, HttpServletRequest request) throws GenericFilebrowserException { - if (!fileviewerConfig.isEnabled()) { - return null; - } - if(LOGGER.isTraceEnabled()) LOGGER.trace("updating file: " + file + ", encoding: " + encoding); - String templatePath = addCommonContextVars(model, request, "filebrowser", null); - - File currentFile = new File(file); - model.put("currentDir", currentFile.getParent()); - - filebrowserService.writeStringToFile(currentFile, encoding, fileContent); - - return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + templatePath; - } - -} +package de.chandre.admintool.fileviewer; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.ModelAndView; + +import de.chandre.admintool.core.AdminTool; +import de.chandre.admintool.core.controller.AbstractAdminController; +import de.chandre.admintool.filebrowser.AdminToolFilebrowserConfig; +import de.chandre.admintool.filebrowser.AdminToolFilebrowserLoader; +import de.chandre.admintool.filebrowser.AdminToolFilebrowserService; +import de.chandre.admintool.filebrowser.DownloadNotAllowedException; +import de.chandre.admintool.filebrowser.GenericFilebrowserException; + +/** + * Fileviewer controller
+ * requires admintool-core 1.0.1
+ * @author Andre + * @since 1.0.1 + */ +@Controller +@RequestMapping(AdminTool.ROOTCONTEXT + "/fileviewer") +public class AdminToolFileviewerController extends AbstractAdminController { + + private static final Log LOGGER = LogFactory.getLog(AdminToolFileviewerController.class); + + @Autowired + private AdminToolFileviewerService fileviewerService; + + @Autowired + private AdminToolFilebrowserService filebrowserService; + + @Autowired + private AdminToolFileviewerConfig fileviewerConfig; + + @Autowired + private AdminToolFilebrowserConfig filebrowserConfig; + + + @RequestMapping(value = {"/show",}, method={RequestMethod.GET, RequestMethod.POST}) + public String loadFile(@RequestParam("file") String file, @RequestParam(name="encoding", required=false) String encoding, + ModelMap model, HttpServletRequest request) throws GenericFilebrowserException, UnsupportedEncodingException { + if (!fileviewerConfig.isEnabled()) { + return null; + } + if(LOGGER.isTraceEnabled()) LOGGER.trace("serving file viewer page for file: " + file + ", encoding: " + encoding); + String templatePath = addCommonContextVars(model, request, "filebrowser", AdminToolFilebrowserLoader.TARGET_FILEVIEWER); + + String decodedPath = URLDecoder.decode(file, "UTF-8"); + File currentFile = new File(decodedPath); + model.put("currentDir", currentFile.getParent()); + + fileviewerService.isFileAllowed(currentFile, false); + + model.put("currentFile", currentFile); + model.put("selEncoding", StringUtils.isEmpty(encoding) ? fileviewerConfig.getDefaultEncoding() : encoding); + + return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + templatePath; + } + + @RequestMapping(value = {"/update",}, method={RequestMethod.POST}) + public String updateFile(@RequestParam("file") String file, @RequestParam(name="encoding", required=false) String encoding, + @RequestParam("fileContent") String fileContent, + ModelMap model, HttpServletRequest request) throws GenericFilebrowserException { + if (!fileviewerConfig.isEnabled()) { + return null; + } + if(LOGGER.isTraceEnabled()) LOGGER.trace("updating file: " + file + ", encoding: " + encoding); + String templatePath = addCommonContextVars(model, request, "filebrowser", null); + + File currentFile = new File(file); + model.put("currentDir", currentFile.getParent()); + + fileviewerService.writeStringToFile(currentFile, encoding, fileContent); + + return AdminTool.ROOTCONTEXT_NAME + AdminTool.SLASH + templatePath; + } + + @ExceptionHandler({DownloadNotAllowedException.class, GenericFilebrowserException.class}) + public ModelAndView handleException(Exception exception, HttpServletRequest request) throws IOException { + if(LOGGER.isTraceEnabled()) LOGGER.trace("handleException: " + exception.getMessage()); + + ModelAndView mv = new ModelAndView(AdminTool.GENERIC_ERROR_TPL_PATH); + addCommonContextVars(mv.getModelMap(), request, "filebrowser", null); + + String lastFile = request.getParameter("file"); + if (StringUtils.isEmpty(lastFile)) { + lastFile = request.getParameter("selectedFile"); + } + String decodedPath = null; + if (StringUtils.hasLength(lastFile)) { + decodedPath = URLDecoder.decode(lastFile, "UTF-8"); + } + LOGGER.info("lastFile: " + lastFile); + if (StringUtils.hasLength(decodedPath) && + filebrowserService.isAllowed(new File(decodedPath).getParentFile(), false, filebrowserConfig.isReadOnly()) ) { + mv.getModelMap().put("currentDir", new File(decodedPath).getParent()); + } else { + mv.getModelMap().put("currentDir", filebrowserConfig.getStartDir().getAbsolutePath()); + } + mv.getModelMap().put("exceptionMessage", exception.getMessage()); + return mv; + } + +} diff --git a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/fileviewer/AdminToolFileviewerServiceImpl.java b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/fileviewer/AdminToolFileviewerServiceImpl.java index e29f36f..cab012a 100644 --- a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/fileviewer/AdminToolFileviewerServiceImpl.java +++ b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/fileviewer/AdminToolFileviewerServiceImpl.java @@ -1,77 +1,78 @@ -package de.chandre.admintool.fileviewer; - -import java.io.File; -import java.io.IOException; - -import org.apache.commons.io.FileUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import de.chandre.admintool.filebrowser.AbstractFileBrowserService; -import de.chandre.admintool.filebrowser.GenericFilebrowserException; - -/** - * - * @author Andre - * @since 1.0.1 - */ -@Service("adminToolFileviewerService") -public class AdminToolFileviewerServiceImpl extends AbstractFileBrowserService implements AdminToolFileviewerService { - - @Autowired - private AdminToolFileviewerConfig config; - - @Override - public void isFileAllowed(File file, boolean write) throws GenericFilebrowserException { - try { - if (!config.isEnabled() || !isAllowed(file, write, config.isReadOnly()) || !isExtensionAllowedAndWriteable(file)) { - throw new GenericFilebrowserException("insufficient file permissions"); - } - } catch (IOException e) { - throw new GenericFilebrowserException("Error while try to check file permission: " + e.getMessage(), e); - } - } - - @Override - public boolean isExtensionAllowedAndReadable(File file) { - if (null == file || !config.isEnabled() || file.isDirectory() || !file.canRead()) { - return false; - } - if (config.getAllowedExtensions().contains(getExtension(file))) { - return true; - } - return false; - } - - @Override - public boolean isExtensionAllowedAndWriteable(File file) { - if (!config.isReadOnly() && isExtensionAllowedAndReadable(file) && file.canWrite()) { - return config.getAllowedExtensionsToEdit().contains(getExtension(file)); - } - return false; - } - - @Override - public String readFileToString(File file, String encoding) throws IOException { - return FileUtils.readFileToString(file, (StringUtils.isEmpty(encoding) ? config.getDefaultEncoding() : encoding)); - } - - @Override - public boolean isChangeable(File file) { - if (isExtensionAllowedAndWriteable(file)) { - return true; - } - return false; - } - - @Override - public void writeStringToFile(File file, String encoding, String fileContent) throws GenericFilebrowserException { - isFileAllowed(file, true); - try { - FileUtils.writeStringToFile(file, fileContent, encoding, false); - } catch (Exception e) { - throw new GenericFilebrowserException("could not write content to file: " + e.getMessage(), e); - } - } -} +package de.chandre.admintool.fileviewer; + +import java.io.File; +import java.io.IOException; + +import org.apache.commons.io.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import de.chandre.admintool.filebrowser.AbstractFileBrowserService; +import de.chandre.admintool.filebrowser.GenericFilebrowserException; + +/** + * + * @author Andre + * @since 1.0.1 + */ +@Service("adminToolFileviewerService") +public class AdminToolFileviewerServiceImpl extends AbstractFileBrowserService implements AdminToolFileviewerService { + + @Autowired + private AdminToolFileviewerConfig config; + + @Override + public void isFileAllowed(File file, boolean write) throws GenericFilebrowserException { + try { + if (!config.isEnabled() || !isAllowed(file, write, config.isReadOnly()) || !isExtensionAllowedAndReadable(file) + || (write && !isExtensionAllowedAndWriteable(file))) { + throw new GenericFilebrowserException("insufficient file permissions"); + } + } catch (IOException e) { + throw new GenericFilebrowserException("Error while try to check file permission: " + e.getMessage(), e); + } + } + + @Override + public boolean isExtensionAllowedAndReadable(File file) { + if (null == file || !config.isEnabled() || file.isDirectory() || !file.canRead()) { + return false; + } + if (config.getAllowedExtensions().contains(getExtension(file))) { + return true; + } + return false; + } + + @Override + public boolean isExtensionAllowedAndWriteable(File file) { + if (!config.isReadOnly() && isExtensionAllowedAndReadable(file) && file.canWrite()) { + return config.getAllowedExtensionsToEdit().contains(getExtension(file)); + } + return false; + } + + @Override + public String readFileToString(File file, String encoding) throws IOException { + return FileUtils.readFileToString(file, (StringUtils.isEmpty(encoding) ? config.getDefaultEncoding() : encoding)); + } + + @Override + public boolean isChangeable(File file) { + if (isExtensionAllowedAndWriteable(file)) { + return true; + } + return false; + } + + @Override + public void writeStringToFile(File file, String encoding, String fileContent) throws GenericFilebrowserException { + isFileAllowed(file, true); + try { + FileUtils.writeStringToFile(file, fileContent, encoding, false); + } catch (Exception e) { + throw new GenericFilebrowserException("could not write content to file: " + e.getMessage(), e); + } + } +} From 580f6a7de4e0a43614119fdfcb512c2af148c0d5 Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 19 Mar 2018 23:45:01 +0100 Subject: [PATCH 3/6] #31 - enhancements for folder creation and resource deletion --- .../AbstractFileBrowserService.java | 137 +- .../AdminToolFilebrowserConfig.java | 830 +++++----- .../AdminToolFilebrowserServiceImpl.java | 1372 +++++++++-------- .../admintool/filebrowser/js/filebrowser.js | 139 +- 4 files changed, 1257 insertions(+), 1221 deletions(-) diff --git a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AbstractFileBrowserService.java b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AbstractFileBrowserService.java index 7545395..5e54071 100644 --- a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AbstractFileBrowserService.java +++ b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AbstractFileBrowserService.java @@ -1,63 +1,74 @@ -package de.chandre.admintool.filebrowser; - -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; - -/** - * - * @author Andre - * - */ -public abstract class AbstractFileBrowserService { - - @Autowired - private AdminToolFilebrowserConfig config; - - public String encodeURL(String path) throws UnsupportedEncodingException { - return URLEncoder.encode(path, "UTF-8"); - } - - /** - * - * @param path - * @param write - * @param configReadOnly - * @return - * @throws IOException - */ - public boolean isAllowed(File path, boolean write, boolean configReadOnly) throws IOException { - try { - if (configReadOnly && write) return false; - if (config.isRestrictedBrowsing()) { - if (null != config.getRestrictedPaths() && null != path) { - for (String restricedPath : config.getRestrictedPaths()) { - if(path.getCanonicalPath().startsWith(restricedPath)) { - return config.isRestrictedBrowsingIsWhitelist(); - } - } - } - return !config.isRestrictedBrowsingIsWhitelist(); - } - } catch (IOException e) { - throw new IOException("Could not check if path '" + path.getAbsolutePath() + "' is allowed ", e); - } - return true; - } - - public String getExtension(File file) { - return getExtension(file.getName()); - } - - public String getExtension(String fileName) { - if (fileName.lastIndexOf('.') > -1) { - return (fileName.substring(fileName.lastIndexOf('.') + 1, fileName.length())).toLowerCase(); - } - return null; - } - -} +package de.chandre.admintool.filebrowser; + +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.springframework.beans.factory.annotation.Autowired; + +/** + * + * @author Andre + * + */ +public abstract class AbstractFileBrowserService { + + @Autowired + private AdminToolFilebrowserConfig config; + + public String encodeURL(String path) throws UnsupportedEncodingException { + return URLEncoder.encode(path, "UTF-8"); + } + + /** + * checks file against configured path restrictions and read-write configuration
+ * doesn't check file permissions + * + * @param path the file to show/manipulate + * @param write if current action want to change the file + * @param configReadOnly if configuration value read-only is true + * @return true if accessible + * @throws IOException + */ + public boolean isAllowed(File path, boolean write, boolean configReadOnly) throws IOException { + try { + if (configReadOnly && write) return false; + if (config.isRestrictedBrowsing()) { + if (null != config.getRestrictedPaths() && null != path) { + for (String restricedPath : config.getRestrictedPaths()) { + if(path.getCanonicalPath().startsWith(restricedPath)) { + return config.isRestrictedBrowsingIsWhitelist(); + } + } + } + return !config.isRestrictedBrowsingIsWhitelist(); + } + } catch (IOException e) { + throw new IOException("Could not check if path '" + path.getAbsolutePath() + "' is allowed ", e); + } + return true; + } + + /** + * returns the file extension by filename separated by last dot + * @param file + * @return null or extension + */ + public String getExtension(File file) { + return getExtension(file.getName()); + } + + /** + * returns the file extension by filename separated by last dot + * @param fileName + * @return null or extension + */ + public String getExtension(String fileName) { + if (fileName.lastIndexOf('.') > -1) { + return (fileName.substring(fileName.lastIndexOf('.') + 1, fileName.length())).toLowerCase(); + } + return null; + } + +} diff --git a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserConfig.java b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserConfig.java index d6cbb29..79d092d 100644 --- a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserConfig.java +++ b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserConfig.java @@ -1,409 +1,421 @@ -package de.chandre.admintool.filebrowser; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.annotation.PostConstruct; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import de.chandre.admintool.core.AdminToolConfig; - -/** - * configuration class for file uploader - * - * @author André - * @since 1.0.0 - */ -@Component("adminToolFilebrowserConfig") -public class AdminToolFilebrowserConfig implements AdminToolConfig { - - private static final Log LOGGER = LogFactory.getLog(AdminToolFilebrowserConfig.class); - - @Value("${admintool.filebrowser.enabled:true}") - private boolean enabled; - - @Value("${admintool.filebrowser.hideMenuItem:false}") - private boolean hideMenuItem; - - @Value("${admintool.filebrowser.startDir:}") - private String startDir; - - @Value("#{'${admintool.filebrowser.forbiddenDrives:}'.split(';')}") - private List forbiddenDrives= new ArrayList<>(); - - @Value("${admintool.filebrowser.readOnly:false}") - private boolean readOnly; - - @Value("${admintool.filebrowser.restrictedBrowsing:false}") - private boolean restrictedBrowsing; - - @Value("${admintool.filebrowser.restrictedBrowsingIsWhitelist:true}") - private boolean restrictedBrowsingIsWhitelist; - - @Value("#{'${admintool.filebrowser.restrictedPaths:}'.split(';')}") - private List restrictedPaths = new ArrayList<>(); - - @Value("${admintool.filebrowser.sizeDivisorMultiplicator:1000}") - private long sizeDivisorMultiplicator; - - @Value("${admintool.filebrowser.fileSizeDisplayScale:2}") - private byte fileSizeDisplayScale; - - @Value("${admintool.filebrowser.zipUseTempFile:true}") - private boolean zipUseTempFile; - - @Value("${admintool.filebrowser.zipCompessionLevel:1}") - private byte zipCompessionLevel; - - @Value("${admintool.filebrowser.zipTempDir:'sys:java.io.tmpdir'}") - private String zipTempDir; - - @Value("${admintool.filebrowser.downloadAllowed:true}") - private boolean downloadAllowed; - - @Value("${admintool.filebrowser.downloadCompressedAllowed:true}") - private boolean downloadCompressedAllowed; - - @Value("#{'${admintool.filebrowser.securityRoles:}'.split(';')}") - private Set securityRoles = new HashSet<>(); - - @Value("${admintool.filebrowser.componentPosition:}") - private Integer componentPosition; - - @Value("${admintool.filebrowser.uploadAllowed:false}") - private boolean uploadAllowed; - - @Value("${admintool.filebrowser.createFolderAllowed:false}") - private boolean createFolderAllowed; - - @Value("${admintool.filebrowser.delteFolderAllowed:false}") - private boolean delteFolderAllowed; - - @Value("${admintool.filebrowser.delteFileAllowed:false}") - private boolean delteFileAllowed; - - @Value("${admintool.filebrowser.info.crc32:true}") - private boolean infoCrc32; - - @Value("${admintool.filebrowser.info.md5:true}") - private boolean infoMD5; - - @Value("${admintool.filebrowser.info.sha1:true}") - private boolean infoSha1; - - @Value("${admintool.filebrowser.info.sha256:false}") - private boolean infoSha256; - - @Value("${admintool.filebrowser.info.maxFilesizeForHashes:1000000000}") - private long maxFilesizeForHashes; - - @Value("${admintool.filebrowser.info.countFolderSize:true}") - private boolean countFolderSize; - - @Override - public boolean isEnabled() { - return this.enabled; - } - - /** - * @return the hideMenuItem - */ - public boolean isHideMenuItem() { - return hideMenuItem; - } - - /** - * @return the startDir - */ - public File getStartDir() { - if (StringUtils.isEmpty(this.startDir)) { - return new File(this.getClass().getClassLoader().getResource("").getPath()); - } - File start = new File(this.startDir); - return start.isFile() ? start.getParentFile() : start; - } - - /** - * @return the forbiddenDrives - */ - public List getForbiddenDrives() { - return forbiddenDrives; - } - - /** - * @return the readOnly - */ - public boolean isReadOnly() { - return isEnabled() && readOnly; - } - - /** - * @return the restrictBrowsing - */ - public boolean isRestrictedBrowsing() { - return restrictedBrowsing; - } - - /** - * @return the restrictedBrowsingIsWhitelist - */ - public boolean isRestrictedBrowsingIsWhitelist() { - return restrictedBrowsingIsWhitelist; - } - - /** - * @return the restrictedPaths - */ - public List getRestrictedPaths() { - return restrictedPaths; - } - - /** - * @return the sizeDivisorMultiplicator - */ - public long getSizeDivisorMultiplicator() { - return sizeDivisorMultiplicator; - } - - /** - * @return the sizeDivisorMultiplicator - */ - public void setSizeDivisorMultiplicator(long sizeDivisorMultiplicator) { - this.sizeDivisorMultiplicator = sizeDivisorMultiplicator; - } - - /** - * @return the fileSizeDisplayScale - */ - public byte getFileSizeDisplayScale() { - return fileSizeDisplayScale; - } - - public void setFileSizeDisplayScale(byte fileSizeDisplayScale) { - this.fileSizeDisplayScale = fileSizeDisplayScale; - } - - /** - * @return the zipUseTempFile - */ - public boolean isZipUseTempFile() { - return zipUseTempFile; - } - - /** - * @return the zipTempDir - */ - public String getZipTempDir() { - if (this.zipTempDir.charAt(0) == '\''){ - return zipTempDir.substring(1).substring(0, this.zipTempDir.length() -2); - } - return zipTempDir; - } - - /** - * @return the zipCompessionLevel - */ - public byte getZipCompessionLevel() { - return zipCompessionLevel; - } - - /** - * @return the downloadAllowed - */ - public boolean isDownloadAllowed() { - return isEnabled() && downloadAllowed; - } - - /** - * - * @return - * @since 1.0.6 - */ - public boolean isDownloadCompressedAllowed() { - return isEnabled() && downloadCompressedAllowed; - } - - /** - * @return the securityRoles - * @since 1.0.1 - */ - public Set getSecurityRoles() { - return securityRoles; - } - - /** - * @return the componentPosition - * @since 1.0.1 - */ - public Integer getComponentPosition() { - return componentPosition; - } - - /** - * @param componentPosition the componentPosition to set - * @since 1.0.1 - */ - public void setComponentPosition(Integer componentPosition) { - this.componentPosition = componentPosition; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isManipulationAllowed() { - return !readOnly && (uploadAllowed || createFolderAllowed); - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isUploadAllowed() { - return isEnabled() && uploadAllowed; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isCreateFolderAllowed() { - return isEnabled() && createFolderAllowed; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isDelteFolderAllowed() { - return isEnabled() && delteFolderAllowed; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isDelteFileAllowed() { - return isEnabled() && delteFileAllowed; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isInfoCrc32() { - return infoCrc32; - } - - public void setInfoCrc32(boolean infoCrc32) { - this.infoCrc32 = infoCrc32; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isInfoMD5() { - return infoMD5; - } - - public void setInfoMD5(boolean infoMD5) { - this.infoMD5 = infoMD5; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isInfoSha1() { - return infoSha1; - } - - public void setInfoSha1(boolean infoSha1) { - this.infoSha1 = infoSha1; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isInfoSha256() { - return infoSha256; - } - - public void setInfoSha256(boolean infoSha256) { - this.infoSha256 = infoSha256; - } - - /** - * - * @return - * @since 1.0.6 - */ - public long getMaxFilesizeForHashes() { - return maxFilesizeForHashes; - } - - public void setMaxFilesizeForHashes(long maxFilesizeForHashes) { - this.maxFilesizeForHashes = maxFilesizeForHashes; - } - - /** - * - * @return - * @since 1.1.6 - */ - public boolean isCountFolderSize() { - return countFolderSize; - } - - public void setCountFolderSize(boolean countFolderSize) { - this.countFolderSize = countFolderSize; - } - - @Override - @PostConstruct - public void printConfig() { - LOGGER.debug(toString()); - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("AdminToolFilebrowserConfig [enabled=").append(enabled).append(", hideMenuItem=") - .append(hideMenuItem).append(", startDir=").append(startDir).append(", forbiddenDrives=") - .append(forbiddenDrives).append(", readOnly=").append(readOnly).append(", restrictedBrowsing=") - .append(restrictedBrowsing).append(", restrictedBrowsingIsWhitelist=") - .append(restrictedBrowsingIsWhitelist).append(", restrictedPaths=").append(restrictedPaths) - .append(", sizeDivisorMultiplicator=").append(sizeDivisorMultiplicator) - .append(", fileSizeDisplayScale=").append(fileSizeDisplayScale).append(", zipUseTempFile=") - .append(zipUseTempFile).append(", zipCompessionLevel=").append(zipCompessionLevel) - .append(", zipTempDir=").append(zipTempDir).append(", downloadAllowed=").append(downloadAllowed) - .append(", downloadCompressedAllowed=").append(downloadCompressedAllowed).append(", securityRoles=") - .append(securityRoles).append(", componentPosition=").append(componentPosition) - .append(", uploadAllowed=").append(uploadAllowed).append(", createFolderAllowed=") - .append(createFolderAllowed).append(", delteFolderAllowed=").append(delteFolderAllowed) - .append(", delteFileAllowed=").append(delteFileAllowed).append(", infoCrc32=").append(infoCrc32) - .append(", infoMD5=").append(infoMD5).append(", infoSha1=").append(infoSha1).append(", infoSha256=") - .append(infoSha256).append(", maxFilesizeForHashes=").append(maxFilesizeForHashes) - .append(", countFolderSize=").append(countFolderSize).append("]"); - return builder.toString(); - } -} +package de.chandre.admintool.filebrowser; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.PostConstruct; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import de.chandre.admintool.core.AdminToolConfig; + +/** + * configuration class for file uploader + * + * @author André + * @since 1.0.0 + */ +@Component("adminToolFilebrowserConfig") +public class AdminToolFilebrowserConfig implements AdminToolConfig { + + private static final Log LOGGER = LogFactory.getLog(AdminToolFilebrowserConfig.class); + + @Value("${admintool.filebrowser.enabled:true}") + private boolean enabled; + + @Value("${admintool.filebrowser.hideMenuItem:false}") + private boolean hideMenuItem; + + @Value("${admintool.filebrowser.startDir:}") + private String startDir; + + @Value("#{'${admintool.filebrowser.forbiddenDrives:}'.split(';')}") + private List forbiddenDrives= new ArrayList<>(); + + @Value("${admintool.filebrowser.readOnly:false}") + private boolean readOnly; + + @Value("${admintool.filebrowser.restrictedBrowsing:false}") + private boolean restrictedBrowsing; + + @Value("${admintool.filebrowser.restrictedBrowsingIsWhitelist:true}") + private boolean restrictedBrowsingIsWhitelist; + + @Value("#{'${admintool.filebrowser.restrictedPaths:}'.split(';')}") + private List restrictedPaths = new ArrayList<>(); + + @Value("${admintool.filebrowser.sizeDivisorMultiplicator:1000}") + private long sizeDivisorMultiplicator; + + @Value("${admintool.filebrowser.fileSizeDisplayScale:2}") + private byte fileSizeDisplayScale; + + @Value("${admintool.filebrowser.zipUseTempFile:true}") + private boolean zipUseTempFile; + + @Value("${admintool.filebrowser.zipCompessionLevel:1}") + private byte zipCompessionLevel; + + @Value("${admintool.filebrowser.zipTempDir:'sys:java.io.tmpdir'}") + private String zipTempDir; + + @Value("${admintool.filebrowser.downloadAllowed:true}") + private boolean downloadAllowed; + + @Value("${admintool.filebrowser.downloadCompressedAllowed:true}") + private boolean downloadCompressedAllowed; + + @Value("#{'${admintool.filebrowser.securityRoles:}'.split(';')}") + private Set securityRoles = new HashSet<>(); + + @Value("${admintool.filebrowser.componentPosition:}") + private Integer componentPosition; + + @Value("${admintool.filebrowser.uploadAllowed:false}") + private boolean uploadAllowed; + + @Value("${admintool.filebrowser.createFolderAllowed:false}") + private boolean createFolderAllowed; + + @Value("${admintool.filebrowser.delteFolderAllowed:false}") + private boolean delteFolderAllowed; + + @Value("${admintool.filebrowser.delteFileAllowed:false}") + private boolean delteFileAllowed; + + @Value("${admintool.filebrowser.notDeletableIfNotWriteable:true}") + private boolean notDeletableIfNotWriteable; + + @Value("${admintool.filebrowser.info.crc32:true}") + private boolean infoCrc32; + + @Value("${admintool.filebrowser.info.md5:true}") + private boolean infoMD5; + + @Value("${admintool.filebrowser.info.sha1:true}") + private boolean infoSha1; + + @Value("${admintool.filebrowser.info.sha256:false}") + private boolean infoSha256; + + @Value("${admintool.filebrowser.info.maxFilesizeForHashes:1000000000}") + private long maxFilesizeForHashes; + + @Value("${admintool.filebrowser.info.countFolderSize:true}") + private boolean countFolderSize; + + @Override + public boolean isEnabled() { + return this.enabled; + } + + /** + * @return the hideMenuItem + */ + public boolean isHideMenuItem() { + return hideMenuItem; + } + + /** + * @return the startDir + */ + public File getStartDir() { + if (StringUtils.isEmpty(this.startDir)) { + return new File(this.getClass().getClassLoader().getResource("").getPath()); + } + File start = new File(this.startDir); + return start.isFile() ? start.getParentFile() : start; + } + + /** + * @return the forbiddenDrives + */ + public List getForbiddenDrives() { + return forbiddenDrives; + } + + /** + * @return the readOnly + */ + public boolean isReadOnly() { + return isEnabled() && readOnly; + } + + /** + * @return the restrictBrowsing + */ + public boolean isRestrictedBrowsing() { + return restrictedBrowsing; + } + + /** + * @return the restrictedBrowsingIsWhitelist + */ + public boolean isRestrictedBrowsingIsWhitelist() { + return restrictedBrowsingIsWhitelist; + } + + /** + * @return the restrictedPaths + */ + public List getRestrictedPaths() { + return restrictedPaths; + } + + /** + * @return the sizeDivisorMultiplicator + */ + public long getSizeDivisorMultiplicator() { + return sizeDivisorMultiplicator; + } + + /** + * @return the sizeDivisorMultiplicator + */ + public void setSizeDivisorMultiplicator(long sizeDivisorMultiplicator) { + this.sizeDivisorMultiplicator = sizeDivisorMultiplicator; + } + + /** + * @return the fileSizeDisplayScale + */ + public byte getFileSizeDisplayScale() { + return fileSizeDisplayScale; + } + + public void setFileSizeDisplayScale(byte fileSizeDisplayScale) { + this.fileSizeDisplayScale = fileSizeDisplayScale; + } + + /** + * @return the zipUseTempFile + */ + public boolean isZipUseTempFile() { + return zipUseTempFile; + } + + /** + * @return the zipTempDir + */ + public String getZipTempDir() { + if (this.zipTempDir.charAt(0) == '\''){ + return zipTempDir.substring(1).substring(0, this.zipTempDir.length() -2); + } + return zipTempDir; + } + + /** + * @return the zipCompessionLevel + */ + public byte getZipCompessionLevel() { + return zipCompessionLevel; + } + + /** + * @return the downloadAllowed + */ + public boolean isDownloadAllowed() { + return isEnabled() && downloadAllowed; + } + + /** + * + * @return + * @since 1.0.6 + */ + public boolean isDownloadCompressedAllowed() { + return isEnabled() && downloadCompressedAllowed; + } + + /** + * @return the securityRoles + * @since 1.0.1 + */ + public Set getSecurityRoles() { + return securityRoles; + } + + /** + * @return the componentPosition + * @since 1.0.1 + */ + public Integer getComponentPosition() { + return componentPosition; + } + + /** + * @param componentPosition the componentPosition to set + * @since 1.0.1 + */ + public void setComponentPosition(Integer componentPosition) { + this.componentPosition = componentPosition; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isManipulationAllowed() { + return !readOnly && (uploadAllowed || createFolderAllowed); + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isUploadAllowed() { + return isEnabled() && uploadAllowed; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isCreateFolderAllowed() { + return isEnabled() && createFolderAllowed; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isDelteFolderAllowed() { + return isEnabled() && delteFolderAllowed; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isDelteFileAllowed() { + return isEnabled() && delteFileAllowed; + } + + /** + * + * @return + * @since 1.1.6.2 + */ + public boolean isNotDeletableIfNotWriteable() { + return notDeletableIfNotWriteable; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isInfoCrc32() { + return infoCrc32; + } + + public void setInfoCrc32(boolean infoCrc32) { + this.infoCrc32 = infoCrc32; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isInfoMD5() { + return infoMD5; + } + + public void setInfoMD5(boolean infoMD5) { + this.infoMD5 = infoMD5; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isInfoSha1() { + return infoSha1; + } + + public void setInfoSha1(boolean infoSha1) { + this.infoSha1 = infoSha1; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isInfoSha256() { + return infoSha256; + } + + public void setInfoSha256(boolean infoSha256) { + this.infoSha256 = infoSha256; + } + + /** + * + * @return + * @since 1.0.6 + */ + public long getMaxFilesizeForHashes() { + return maxFilesizeForHashes; + } + + public void setMaxFilesizeForHashes(long maxFilesizeForHashes) { + this.maxFilesizeForHashes = maxFilesizeForHashes; + } + + /** + * + * @return + * @since 1.1.6 + */ + public boolean isCountFolderSize() { + return countFolderSize; + } + + public void setCountFolderSize(boolean countFolderSize) { + this.countFolderSize = countFolderSize; + } + + @Override + @PostConstruct + public void printConfig() { + LOGGER.debug(toString()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("AdminToolFilebrowserConfig [enabled=").append(enabled).append(", hideMenuItem=") + .append(hideMenuItem).append(", startDir=").append(startDir).append(", forbiddenDrives=") + .append(forbiddenDrives).append(", readOnly=").append(readOnly).append(", restrictedBrowsing=") + .append(restrictedBrowsing).append(", restrictedBrowsingIsWhitelist=") + .append(restrictedBrowsingIsWhitelist).append(", restrictedPaths=").append(restrictedPaths) + .append(", sizeDivisorMultiplicator=").append(sizeDivisorMultiplicator) + .append(", fileSizeDisplayScale=").append(fileSizeDisplayScale).append(", zipUseTempFile=") + .append(zipUseTempFile).append(", zipCompessionLevel=").append(zipCompessionLevel) + .append(", zipTempDir=").append(zipTempDir).append(", downloadAllowed=").append(downloadAllowed) + .append(", downloadCompressedAllowed=").append(downloadCompressedAllowed).append(", securityRoles=") + .append(securityRoles).append(", componentPosition=").append(componentPosition) + .append(", uploadAllowed=").append(uploadAllowed).append(", createFolderAllowed=") + .append(createFolderAllowed).append(", delteFolderAllowed=").append(delteFolderAllowed) + .append(", delteFileAllowed=").append(delteFileAllowed).append(", infoCrc32=").append(infoCrc32) + .append(", infoMD5=").append(infoMD5).append(", infoSha1=").append(infoSha1).append(", infoSha256=") + .append(infoSha256).append(", maxFilesizeForHashes=").append(maxFilesizeForHashes) + .append(", countFolderSize=").append(countFolderSize).append("]"); + return builder.toString(); + } +} diff --git a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserServiceImpl.java b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserServiceImpl.java index e7a0553..af37451 100644 --- a/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserServiceImpl.java +++ b/admin-tools-filebrowser/src/main/java/de/chandre/admintool/filebrowser/AdminToolFilebrowserServiceImpl.java @@ -1,681 +1,691 @@ -package de.chandre.admintool.filebrowser; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.DosFileAttributes; -import java.nio.file.attribute.FileTime; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeMap; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.codec.digest.DigestUtils; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - -import de.chandre.admintool.core.utils.RegexUtil; - -/** - * - * @author Andre - * @since 1.0.1 - */ -@Service("adminToolFilebrowserService") -public class AdminToolFilebrowserServiceImpl extends AbstractFileBrowserService implements AdminToolFilebrowserService { - - private static final Log LOGGER = LogFactory.getLog(AdminToolFilebrowserServiceImpl.class); - - private static final Map FILE_SIZE_EXP = new TreeMap<>(); - static { - FILE_SIZE_EXP.put(Integer.valueOf(1), " B"); - FILE_SIZE_EXP.put(Integer.valueOf(2), " KB"); - FILE_SIZE_EXP.put(Integer.valueOf(3), " MB"); - FILE_SIZE_EXP.put(Integer.valueOf(4), " GB"); - FILE_SIZE_EXP.put(Integer.valueOf(5), " TB"); - FILE_SIZE_EXP.put(Integer.valueOf(6), " PB"); - FILE_SIZE_EXP.put(Integer.valueOf(7), " EB"); - } - - @Autowired - private AdminToolFilebrowserConfig config; - - @Autowired - private Environment env; - - private Set rootDirsCache = Collections.newSetFromMap(new ConcurrentHashMap<>()); - - - @Override - public Set getRootDirs() { - File[] roots = File.listRoots(); - - if (this.rootDirsCache.isEmpty()) { - for (File file : roots) { - if(!config.getForbiddenDrives().contains(file.getAbsolutePath().toLowerCase())) { - // if not forbidden add it to result - this.rootDirsCache.add(file.getAbsolutePath()); - } - } - } - return this.rootDirsCache; - } - - @Override - public boolean isRootActive(String rootDir, String currentDir) { - if (!StringUtils.isEmpty(currentDir) && currentDir.toLowerCase().startsWith(rootDir.toLowerCase())) { - return true; - } - return false; - } - - @Override - public String getParent(String dir) throws IOException { - File file = new File(dir); - if (null != file.getParent() && isAllowed(file.getParentFile(), false)) { - return file.getParent(); - } - return ""; - } - - protected List sort(File[] fileAr, final SortColumn sortCol, Boolean sortAsc) { - List files = Arrays.asList(fileAr); - if (null == sortCol) { - return files; - } - if (null == sortAsc) { - sortAsc = Boolean.TRUE; - } - final int direction = sortAsc.booleanValue() ? 1 : -1; - - Collections.sort(files, new Comparator() { - @Override - public int compare(File o1, File o2) { - try { - switch (sortCol) { - case DATE: - return getLastChange(o1).compareTo(getLastChange(o2)) * direction; - case SIZE: - return Long.valueOf(o1.length()).compareTo(Long.valueOf(o2.length())) * direction; - case TYPE: - return getFileType(o1).compareTo(getFileType(o2)) * direction; - case NAME: - default: - return o1.getName().compareTo(o2.getName()) * direction; - } - } catch (Exception ignore) { - } - - return 0; - } - }); - return files; - } - - @Override - public String getSortDirection(int current, SortColumn sortCol, Boolean sortAsc) { - if (current == sortCol.getIndex() && sortAsc != null) { - return sortAsc ? "up" : "down"; - } - return ""; - } - - @Override - public List getDirectories(String currentDir, SortColumn sortCol, Boolean sortAsc, String filter) throws IOException { - File file = new File(currentDir); - if (null != file && isAllowed(file, false)) { - final Pattern fileNamePattern = getFileNamePattern(filter); - File[] files = file.listFiles(new FileFilter() { - @Override - public boolean accept(File dir) { - try { - if (isAllowed(dir, false) && dir.isDirectory()) { - if (null == fileNamePattern) { - return true; - } else if (null != fileNamePattern && fileNamePattern.matcher(dir.getName()).matches()) { - return true; - } - } - } catch (IOException e) { - LOGGER.debug(e.getMessage(), e); - } - return false; - } - }); - if (files != null) { - return sort(files, sortCol, sortAsc); - } - } - return Collections.emptyList(); - } - - @Override - public List getFiles(String currentDir, SortColumn sortCol, Boolean sortAsc, String filter) throws IOException { - File file = new File(currentDir); - if (null != file && isAllowed(file, false)) { - final Pattern fileNamePattern = getFileNamePattern(filter); - File[] files = file.listFiles(new FileFilter() { - @Override - public boolean accept(File dir) { - try { - if (isAllowed(dir, false) && dir.isFile()) { - if (null == fileNamePattern) { - return true; - } else if (null != fileNamePattern && fileNamePattern.matcher(dir.getName()).matches()) { - return true; - } - } - } catch (IOException e) { - LOGGER.debug(e.getMessage(), e); - } - return false; - } - }); - if (files != null) { - return sort(files, sortCol, sortAsc); - } - } - return Collections.emptyList(); - } - - private Pattern getFileNamePattern(String filter) { - if (!StringUtils.isEmpty(filter)) { - return Pattern.compile(RegexUtil.wildcardToRegex(filter), Pattern.CASE_INSENSITIVE); - } - return null; - } - - @Override - public String getDirOrRootName(File currentDir) { - if (StringUtils.isEmpty(currentDir.getName())) { - return currentDir.getAbsolutePath(); - } - return currentDir.getName(); - } - - @Override - public List getBreadcrumb(String currentDir) { - if (null == currentDir) { - return Collections.emptyList(); - } - List result = new ArrayList<>(); - File file = new File(currentDir); - getParentsRecursive(file, result); - return result; - } - - private void getParentsRecursive(File actual, List files) { - if (null != actual.getParentFile()) { - getParentsRecursive(actual.getParentFile(), files); - } - if (actual.isDirectory()) { - files.add(actual); - } - } - - - - @Override - public String getFileSizeSum(String dir, String filter) throws IOException { - List files = getFiles(dir, null, true, filter); - Long res = files.stream().collect(Collectors.summingLong(File::length)); - return String.format("%s in %s files", getFileSize(res), files.size()); - } - - @Override - public Date getLastChange(File file) throws IOException { - if (null != file) { - FileTime time = Files.getLastModifiedTime(file.toPath(), new LinkOption[]{}); - return new Date(time.toMillis()); - } - return new Date(); - } - - @Override - public String getFileType(File file) { - if (null == file) { - return ""; - } - if (file.isDirectory()) { - return "DIR"; - } - if (file.getName().lastIndexOf('.') == -1) { - return ""; - } - return getExtension(file); - } - - @Override - public String getFileSize(File file) { - return getFileSize(file.length()); - } - - /** - * returns the the size in B, KB, MB or GB depending on the length - * - * @param fileLength - * @return - */ - @Override - public String getFileSize(long fileLength) { - return calculateDisplayFileSize(fileLength, config.getSizeDivisorMultiplicator()); - } - - @Override - public String getNormalFileSize(long fileLength) { - return calculateDisplayFileSize(fileLength, FileUtils.ONE_KB); - } - - protected String calculateDisplayFileSize(long fileLength, long sizeDivisorMultiplicator) { - BigInteger multiplicator = BigInteger.valueOf(sizeDivisorMultiplicator); - BigInteger size = BigInteger.valueOf(fileLength); - - for (Entry entry : FILE_SIZE_EXP.entrySet()) { - BigInteger divisor = multiplicator.pow(entry.getKey().intValue()); - if (fileLength < divisor.longValue()) { - if (entry.getKey().intValue() == 1) { - return String.valueOf(size) + entry.getValue(); - } - return formatFileSize(size, multiplicator.pow(entry.getKey().intValue() - 1), entry.getValue()); - } - } - //should not happen - return FileUtils.byteCountToDisplaySize(fileLength); - } - - /** - * calculates the and formats files size - * @see #getFileSize(long) - * @param fileLength - * @param divisor - * @param unit the Unit for the divisor - * @return - */ - protected String formatFileSize(BigInteger fileLength, BigInteger divisor, String unit) { - BigDecimal size = new BigDecimal(fileLength); - size = size.setScale(config.getFileSizeDisplayScale()).divide(new BigDecimal(divisor), BigDecimal.ROUND_HALF_EVEN); - return String.format("%s %s", size.doubleValue(), unit); - } - - /** - * - * @param fileName the name - * @param size (optional) size/length of content - * @param response the servlet response - */ - protected void prepareDownloadResponse(String fileName, Long size, HttpServletResponse response, boolean asAttachment) { - - String mimeType = MimeTypes.getMimeType(getExtension(fileName)); - LOGGER.info("following mimeType:" + mimeType); - if (asAttachment || null == mimeType) { - response.setContentType("application/octet-stream"); - response.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\""); - } else { - response.setContentType(mimeType); - response.setHeader("Content-Disposition", "inline;filename=\"" + fileName + "\""); - } - if (null != size) { - response.setContentLength(size.intValue()); - } - } - - @Override - public void downloadFile(String filePath, HttpServletResponse response, boolean asAttachment) throws DownloadNotAllowedException, GenericFilebrowserException { - downloadFile(filePath, response, null, asAttachment); - } - - @Override - public void downloadFile(String filePath, HttpServletResponse response, String alternativeFileName, boolean asAttachment) throws DownloadNotAllowedException, GenericFilebrowserException { - File file = new File(filePath); - try { - if (!isAllowed(file, false)) { - throw new DownloadNotAllowedException(); - } - } catch (IOException e) { - throw new GenericFilebrowserException(e); - } - - prepareDownloadResponse(StringUtils.isEmpty(alternativeFileName) ? file.getName() : alternativeFileName, - Long.valueOf(file.length()), response, asAttachment); - - InputStream in = null; - BufferedInputStream fileInput = null; - try { - in = new FileInputStream(file); - fileInput = new BufferedInputStream(in); - ServletOutputStream out = response.getOutputStream(); - IOUtils.copy(fileInput, out, 8192); - out.flush(); - } catch (IOException e) { - throw new GenericFilebrowserException("could not prepare file for downloading", e); - } - finally { - IOUtils.closeQuietly(fileInput); - IOUtils.closeQuietly(in); - } - } - - @Override - public void downloadFilesAsZip(List filePaths, HttpServletResponse response) throws GenericFilebrowserException { - - File tempFile = null; - try { - OutputStream out = null; - - try { - if (config.isZipUseTempFile()) { - String tempDirConf = config.getZipTempDir(); - File tempDir = null; - if (StringUtils.isEmpty(tempDirConf)) { - tempFile = File.createTempFile("zip", null); - } - else if (tempDirConf.startsWith("sys")) { - tempDir = new File(System.getProperty(tempDirConf.substring(4, tempDirConf.length()))); - } - else if (tempDirConf.startsWith("env")) { - tempDir = new File(env.getProperty(tempDirConf.substring(4, tempDirConf.length()))); - } - else { - tempDir = new File(tempDirConf); - } - if (null == tempFile) { - tempFile = File.createTempFile("zip", null, tempDir); - } - out = new FileOutputStream(tempFile); - } - } catch (Exception e) { - LOGGER.warn("could not create temporary file, using servlet outputstream for serving ZIP"); - } - - if (null == out) { - tempFile = null; - prepareDownloadResponse("rename_me.zip", null, response, true); - out = response.getOutputStream(); - } - - ZipOutputStream zos = new ZipOutputStream(out); - - zos.setLevel(config.getZipCompessionLevel()); - try { - for (String filePathStr : filePaths) { - File orgfile = new File(filePathStr); - Files.walkFileTree(orgfile.toPath(), new SimpleFileVisitor() { - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (!isAllowed(file.toFile(), false)) { - LOGGER.debug("ZIP creation: skipping not allowed file: " + file.toAbsolutePath()); - return FileVisitResult.CONTINUE; - } - String entry = orgfile.getParentFile().toPath().relativize(file).toString(); - LOGGER.trace("creating entry: " + entry); - zos.putNextEntry(new ZipEntry(entry)); - Files.copy(file, zos); - zos.closeEntry(); - return FileVisitResult.CONTINUE; - } - - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - if (!isAllowed(dir.toFile(), false)) { - LOGGER.debug("ZIP creation: skipping not allowed directory and subtree for: " + dir.toAbsolutePath()); - return FileVisitResult.SKIP_SUBTREE; - } - String entry = orgfile.getParentFile().toPath().relativize(dir).toString() + "/"; - LOGGER.trace("creating dir: " + entry); - zos.putNextEntry(new ZipEntry(entry)); - zos.closeEntry(); - return FileVisitResult.CONTINUE; - } - }); - } - - zos.finish(); - - } catch (IOException e) { - IOUtils.closeQuietly(zos); - throw new GenericFilebrowserException("Could not create zip file is allowed ", e); - } - // flushing the output - out.flush(); - - if (null != tempFile && config.isZipUseTempFile()) { - IOUtils.closeQuietly(out); - downloadFile(tempFile.getAbsolutePath(), response, "rename_me.zip", true); - } - - } catch (Exception e) { - LOGGER.error(e.getMessage(), e); - throw new GenericFilebrowserException(e); - } finally { - if (null != tempFile && config.isZipUseTempFile()) { - try { - if(!tempFile.delete()) { - tempFile.deleteOnExit(); - } - } catch (Exception e2) { - LOGGER.warn("could not delete tempfile " + tempFile.getAbsolutePath()); - tempFile.deleteOnExit(); - } - } - } - } - - /** - * checks if file is allowed for access - * - * @param path - * @param write - * @return - * @throws IOException - */ - protected boolean isAllowed(File path, boolean write) throws IOException { - return isAllowed(path, write, config.isReadOnly()); - } - - @Override - public String accessibleCSS(File file) { - String res = ""; - if (!file.canRead()) { - res += "not-readable"; - } - if (!file.canWrite()) { - res += " not-writeable"; - } - if (file.isHidden()) { - res += " file-hidden"; - } - return res.trim(); - } - - @Override - public String createFolder(String path, String folderName) throws IOException, GenericFilebrowserException { - File file = new File(path); - if (file.exists()) { - if (isAllowed(file, true)) { - file = new File(file, folderName); - if(file.mkdirs()) { - return file.getAbsolutePath(); - } - throw new GenericFilebrowserException("could not create directories"); - } - } else { - LOGGER.warn("[createFolder] folder already exists: " + path); - } - return null; - } - - @Override - public String deleteResource(String path) throws IOException, GenericFilebrowserException { - File file = new File(path); - - if (file.isDirectory() && !config.isDelteFolderAllowed()) { - throw new GenericFilebrowserException("delete folder is not allowed"); - } - if (file.isFile() && !config.isDelteFileAllowed()) { - throw new GenericFilebrowserException("delete file is not allowed"); - } - if (!isAllowed(file, true)) { - throw new GenericFilebrowserException("delete "+(file.isDirectory() ? "folder" : "file")+" is not allowed"); - } - - String parent = file.getParent(); - - if (file.isFile()) { - FileUtils.deleteQuietly(file); - } else { - try { - FileUtils.deleteDirectory(file); - } catch (Exception e) { - LOGGER.error("error deleting folder", e); - } - } - return parent; - } - - @Override - public Map getFileInfo(String path) throws IOException { - File file = new File(path); - Map result = new TreeMap<>(); - result.put("file", file); - - if (file.isFile() && file.canRead() && config.getMaxFilesizeForHashes() > file.length()) { - if (config.isInfoCrc32()) { - result.put("file.checksumCRC32", FileUtils.checksumCRC32(file)); - } - - FileInputStream fis = new FileInputStream(file); - if (config.isInfoMD5()) { - result.put("file.md5Hex", DigestUtils.md5Hex(fis)); - } - if (config.isInfoSha1()) { - result.put("file.sha1Hex", DigestUtils.sha1Hex(fis)); - } - if (config.isInfoSha256()) { - result.put("file.sha256Hex", DigestUtils.sha256Hex(fis)); - } -// result.put("file.sha384Hex", DigestUtils.sha384Hex(fis)); -// result.put("file.sha512Hex", DigestUtils.sha512Hex(fis)); - - IOUtils.closeQuietly(fis); - } - - result.put("file.lastModified", file.lastModified()); - result.put("file.canWrite", file.canWrite()); - result.put("file.canRead", file.canRead()); - result.put("file.canExecute", file.canExecute()); - result.put("file.isHidden", file.isHidden()); - - result.put("disk.totalSpace", file.getTotalSpace()); - result.put("disk.usableSpace", file.getUsableSpace()); - result.put("disk.freeSpace", file.getFreeSpace()); - result.put("disk.totalSpace.coreFormat", getFileSize(file.getTotalSpace())); - result.put("disk.usableSpace.coreFormat", getFileSize(file.getUsableSpace())); - result.put("disk.freeSpace.coreFormat", getFileSize(file.getFreeSpace())); - result.put("disk.totalSpace.commonFormat", getNormalFileSize(file.getTotalSpace())); - result.put("disk.usableSpace.commonFormat", getNormalFileSize(file.getUsableSpace())); - result.put("disk.freeSpace.commonFormat", getNormalFileSize(file.getFreeSpace())); - - String os = System.getProperty("os.name").toLowerCase(); - result.put("system.operationSystem", os); - - Class attributesClass = - isWindows(os) ? DosFileAttributes.class : BasicFileAttributes.class; - BasicFileAttributes attr = Files.readAttributes(file.toPath(), attributesClass); - - result.put("file.attr.creationTime", attr.creationTime()); - result.put("file.attr.lastAccessTime", attr.lastAccessTime()); - result.put("file.attr.lastModifiedTime", attr.lastModifiedTime()); - - result.put("file.attr.isDirectory", attr.isDirectory()); - result.put("file.attr.isOther", attr.isOther()); - result.put("file.attr.isRegularFile", attr.isRegularFile()); - result.put("file.attr.isSymbolicLink", attr.isSymbolicLink()); - result.put("file.attr.size", attr.size()); - - - if (DosFileAttributes.class.isAssignableFrom(attr.getClass())) { - result.put("file.attr.isArchive", ((DosFileAttributes)attr).isArchive()); - result.put("file.attr.isHidden", ((DosFileAttributes)attr).isHidden()); - result.put("file.attr.isReadOnly", ((DosFileAttributes)attr).isReadOnly()); - result.put("file.attr.isSystem", ((DosFileAttributes)attr).isSystem()); - } - - long size = file.length(); - if (file.isDirectory() && !attr.isSymbolicLink() && config.isCountFolderSize()) { - size = FileUtils.sizeOfDirectory(file); - } - result.put("file.size", size); - result.put("file.size.coreFormat", getFileSize(size)); - result.put("file.size.commonFormat", getNormalFileSize(size)); - - if (!isWindows(os)) { - Set permissions = Files.getPosixFilePermissions(file.toPath(), LinkOption.NOFOLLOW_LINKS); - result.put("file.permissions", PosixFilePermissions.toString(permissions)); - } - - return result; - } - - public static boolean isWindows(String os) { - return (os.indexOf("win") >= 0); - } - - @Override - public boolean saveFile(String decodedPath, MultipartFile upload) throws IOException, GenericFilebrowserException { - if (StringUtils.isEmpty(decodedPath)) { - return false; - } - File uploadFolder = new File(decodedPath); - if (uploadFolder.isDirectory() && isAllowed(uploadFolder, true)) { - - OutputStream fos = null; - try { - File file = new File(uploadFolder, upload.getOriginalFilename()); - fos = new FileOutputStream(file); - long result = IOUtils.copyLarge(upload.getInputStream(), fos); - LOGGER.info(String.format("uploaded %s bytes (%s)", result, getNormalFileSize(result))); - } catch (Exception e) { - LOGGER.error(e.getMessage(), e); - return false; - } finally { - IOUtils.closeQuietly(fos); - } - return true; - } - throw new GenericFilebrowserException("upload not allowed"); - } -} +package de.chandre.admintool.filebrowser; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.DosFileAttributes; +import java.nio.file.attribute.FileTime; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import de.chandre.admintool.core.utils.RegexUtil; + +/** + * + * @author Andre + * @since 1.0.1 + */ +@Service("adminToolFilebrowserService") +public class AdminToolFilebrowserServiceImpl extends AbstractFileBrowserService implements AdminToolFilebrowserService { + + private static final Log LOGGER = LogFactory.getLog(AdminToolFilebrowserServiceImpl.class); + + private static final Map FILE_SIZE_EXP = new TreeMap<>(); + static { + FILE_SIZE_EXP.put(Integer.valueOf(1), " B"); + FILE_SIZE_EXP.put(Integer.valueOf(2), " KB"); + FILE_SIZE_EXP.put(Integer.valueOf(3), " MB"); + FILE_SIZE_EXP.put(Integer.valueOf(4), " GB"); + FILE_SIZE_EXP.put(Integer.valueOf(5), " TB"); + FILE_SIZE_EXP.put(Integer.valueOf(6), " PB"); + FILE_SIZE_EXP.put(Integer.valueOf(7), " EB"); + } + + @Autowired + private AdminToolFilebrowserConfig config; + + @Autowired + private Environment env; + + private Set rootDirsCache = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + + @Override + public Set getRootDirs() { + File[] roots = File.listRoots(); + + if (this.rootDirsCache.isEmpty()) { + for (File file : roots) { + if(!config.getForbiddenDrives().contains(file.getAbsolutePath().toLowerCase())) { + // if not forbidden add it to result + this.rootDirsCache.add(file.getAbsolutePath()); + } + } + } + return this.rootDirsCache; + } + + @Override + public boolean isRootActive(String rootDir, String currentDir) { + if (!StringUtils.isEmpty(currentDir) && currentDir.toLowerCase().startsWith(rootDir.toLowerCase())) { + return true; + } + return false; + } + + @Override + public String getParent(String dir) throws IOException { + File file = new File(dir); + if (null != file.getParent() && isAllowed(file.getParentFile(), false)) { + return file.getParent(); + } + return ""; + } + + protected List sort(File[] fileAr, final SortColumn sortCol, Boolean sortAsc) { + List files = Arrays.asList(fileAr); + if (null == sortCol) { + return files; + } + if (null == sortAsc) { + sortAsc = Boolean.TRUE; + } + final int direction = sortAsc.booleanValue() ? 1 : -1; + + Collections.sort(files, new Comparator() { + @Override + public int compare(File o1, File o2) { + try { + switch (sortCol) { + case DATE: + return getLastChange(o1).compareTo(getLastChange(o2)) * direction; + case SIZE: + return Long.valueOf(o1.length()).compareTo(Long.valueOf(o2.length())) * direction; + case TYPE: + return getFileType(o1).compareTo(getFileType(o2)) * direction; + case NAME: + default: + return o1.getName().compareTo(o2.getName()) * direction; + } + } catch (Exception ignore) { + } + + return 0; + } + }); + return files; + } + + @Override + public String getSortDirection(int current, SortColumn sortCol, Boolean sortAsc) { + if (current == sortCol.getIndex() && sortAsc != null) { + return sortAsc ? "up" : "down"; + } + return ""; + } + + @Override + public List getDirectories(String currentDir, SortColumn sortCol, Boolean sortAsc, String filter) throws IOException { + File file = new File(currentDir); + if (null != file && isAllowed(file, false)) { + final Pattern fileNamePattern = getFileNamePattern(filter); + File[] files = file.listFiles(new FileFilter() { + @Override + public boolean accept(File dir) { + try { + if (isAllowed(dir, false) && dir.isDirectory()) { + if (null == fileNamePattern) { + return true; + } else if (null != fileNamePattern && fileNamePattern.matcher(dir.getName()).matches()) { + return true; + } + } + } catch (IOException e) { + LOGGER.debug(e.getMessage(), e); + } + return false; + } + }); + if (files != null) { + return sort(files, sortCol, sortAsc); + } + } + return Collections.emptyList(); + } + + @Override + public List getFiles(String currentDir, SortColumn sortCol, Boolean sortAsc, String filter) throws IOException { + File file = new File(currentDir); + if (null != file && isAllowed(file, false)) { + final Pattern fileNamePattern = getFileNamePattern(filter); + File[] files = file.listFiles(new FileFilter() { + @Override + public boolean accept(File dir) { + try { + if (isAllowed(dir, false) && dir.isFile()) { + if (null == fileNamePattern) { + return true; + } else if (null != fileNamePattern && fileNamePattern.matcher(dir.getName()).matches()) { + return true; + } + } + } catch (IOException e) { + LOGGER.debug(e.getMessage(), e); + } + return false; + } + }); + if (files != null) { + return sort(files, sortCol, sortAsc); + } + } + return Collections.emptyList(); + } + + private Pattern getFileNamePattern(String filter) { + if (!StringUtils.isEmpty(filter)) { + return Pattern.compile(RegexUtil.wildcardToRegex(filter), Pattern.CASE_INSENSITIVE); + } + return null; + } + + @Override + public String getDirOrRootName(File currentDir) { + if (StringUtils.isEmpty(currentDir.getName())) { + return currentDir.getAbsolutePath(); + } + return currentDir.getName(); + } + + @Override + public List getBreadcrumb(String currentDir) { + if (null == currentDir) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + File file = new File(currentDir); + getParentsRecursive(file, result); + return result; + } + + private void getParentsRecursive(File actual, List files) { + if (null != actual.getParentFile()) { + getParentsRecursive(actual.getParentFile(), files); + } + if (actual.isDirectory()) { + files.add(actual); + } + } + + + + @Override + public String getFileSizeSum(String dir, String filter) throws IOException { + List files = getFiles(dir, null, true, filter); + Long res = files.stream().collect(Collectors.summingLong(File::length)); + return String.format("%s in %s files", getFileSize(res), files.size()); + } + + @Override + public Date getLastChange(File file) throws IOException { + if (null != file) { + FileTime time = Files.getLastModifiedTime(file.toPath(), new LinkOption[]{}); + return new Date(time.toMillis()); + } + return new Date(); + } + + @Override + public String getFileType(File file) { + if (null == file) { + return ""; + } + if (file.isDirectory()) { + return "DIR"; + } + if (file.getName().lastIndexOf('.') == -1) { + return ""; + } + return getExtension(file); + } + + @Override + public String getFileSize(File file) { + return getFileSize(file.length()); + } + + /** + * returns the the size in B, KB, MB or GB depending on the length + * + * @param fileLength + * @return + */ + @Override + public String getFileSize(long fileLength) { + return calculateDisplayFileSize(fileLength, config.getSizeDivisorMultiplicator()); + } + + @Override + public String getNormalFileSize(long fileLength) { + return calculateDisplayFileSize(fileLength, FileUtils.ONE_KB); + } + + protected String calculateDisplayFileSize(long fileLength, long sizeDivisorMultiplicator) { + BigInteger multiplicator = BigInteger.valueOf(sizeDivisorMultiplicator); + BigInteger size = BigInteger.valueOf(fileLength); + + for (Entry entry : FILE_SIZE_EXP.entrySet()) { + BigInteger divisor = multiplicator.pow(entry.getKey().intValue()); + if (fileLength < divisor.longValue()) { + if (entry.getKey().intValue() == 1) { + return String.valueOf(size) + entry.getValue(); + } + return formatFileSize(size, multiplicator.pow(entry.getKey().intValue() - 1), entry.getValue()); + } + } + //should not happen + return FileUtils.byteCountToDisplaySize(fileLength); + } + + /** + * calculates the and formats files size + * @see #getFileSize(long) + * @param fileLength + * @param divisor + * @param unit the Unit for the divisor + * @return + */ + protected String formatFileSize(BigInteger fileLength, BigInteger divisor, String unit) { + BigDecimal size = new BigDecimal(fileLength); + size = size.setScale(config.getFileSizeDisplayScale()).divide(new BigDecimal(divisor), BigDecimal.ROUND_HALF_EVEN); + return String.format("%s %s", size.doubleValue(), unit); + } + + /** + * + * @param fileName the name + * @param size (optional) size/length of content + * @param response the servlet response + */ + protected void prepareDownloadResponse(String fileName, Long size, HttpServletResponse response, boolean asAttachment) { + + String mimeType = MimeTypes.getMimeType(getExtension(fileName)); + LOGGER.info("following mimeType:" + mimeType); + if (asAttachment || null == mimeType) { + response.setContentType("application/octet-stream"); + response.setHeader("Content-Disposition", "attachment;filename=\"" + fileName + "\""); + } else { + response.setContentType(mimeType); + response.setHeader("Content-Disposition", "inline;filename=\"" + fileName + "\""); + } + if (null != size) { + response.setContentLength(size.intValue()); + } + } + + @Override + public void downloadFile(String filePath, HttpServletResponse response, boolean asAttachment) throws DownloadNotAllowedException, GenericFilebrowserException { + downloadFile(filePath, response, null, asAttachment); + } + + @Override + public void downloadFile(String filePath, HttpServletResponse response, String alternativeFileName, boolean asAttachment) throws DownloadNotAllowedException, GenericFilebrowserException { + File file = new File(filePath); + try { + if (!isAllowed(file, false)) { + throw new DownloadNotAllowedException(); + } + } catch (IOException e) { + throw new GenericFilebrowserException(e); + } + + prepareDownloadResponse(StringUtils.isEmpty(alternativeFileName) ? file.getName() : alternativeFileName, + Long.valueOf(file.length()), response, asAttachment); + + InputStream in = null; + BufferedInputStream fileInput = null; + try { + in = new FileInputStream(file); + fileInput = new BufferedInputStream(in); + ServletOutputStream out = response.getOutputStream(); + IOUtils.copy(fileInput, out, 8192); + out.flush(); + } catch (IOException e) { + throw new GenericFilebrowserException("could not prepare file for downloading", e); + } + finally { + IOUtils.closeQuietly(fileInput); + IOUtils.closeQuietly(in); + } + } + + @Override + public void downloadFilesAsZip(List filePaths, HttpServletResponse response) throws GenericFilebrowserException { + + File tempFile = null; + try { + OutputStream out = null; + + try { + if (config.isZipUseTempFile()) { + String tempDirConf = config.getZipTempDir(); + File tempDir = null; + if (StringUtils.isEmpty(tempDirConf)) { + tempFile = File.createTempFile("zip", null); + } + else if (tempDirConf.startsWith("sys")) { + tempDir = new File(System.getProperty(tempDirConf.substring(4, tempDirConf.length()))); + } + else if (tempDirConf.startsWith("env")) { + tempDir = new File(env.getProperty(tempDirConf.substring(4, tempDirConf.length()))); + } + else { + tempDir = new File(tempDirConf); + } + if (null == tempFile) { + tempFile = File.createTempFile("zip", null, tempDir); + } + out = new FileOutputStream(tempFile); + } + } catch (Exception e) { + LOGGER.warn("could not create temporary file, using servlet outputstream for serving ZIP"); + } + + if (null == out) { + tempFile = null; + prepareDownloadResponse("rename_me.zip", null, response, true); + out = response.getOutputStream(); + } + + ZipOutputStream zos = new ZipOutputStream(out); + + zos.setLevel(config.getZipCompessionLevel()); + try { + for (String filePathStr : filePaths) { + File orgfile = new File(filePathStr); + Files.walkFileTree(orgfile.toPath(), new SimpleFileVisitor() { + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (!isAllowed(file.toFile(), false)) { + LOGGER.debug("ZIP creation: skipping not allowed file: " + file.toAbsolutePath()); + return FileVisitResult.CONTINUE; + } + String entry = orgfile.getParentFile().toPath().relativize(file).toString(); + LOGGER.trace("creating entry: " + entry); + zos.putNextEntry(new ZipEntry(entry)); + Files.copy(file, zos); + zos.closeEntry(); + return FileVisitResult.CONTINUE; + } + + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + if (!isAllowed(dir.toFile(), false)) { + LOGGER.debug("ZIP creation: skipping not allowed directory and subtree for: " + dir.toAbsolutePath()); + return FileVisitResult.SKIP_SUBTREE; + } + String entry = orgfile.getParentFile().toPath().relativize(dir).toString() + "/"; + LOGGER.trace("creating dir: " + entry); + zos.putNextEntry(new ZipEntry(entry)); + zos.closeEntry(); + return FileVisitResult.CONTINUE; + } + }); + } + + zos.finish(); + + } catch (IOException e) { + IOUtils.closeQuietly(zos); + throw new GenericFilebrowserException("Could not create zip file is allowed ", e); + } + // flushing the output + out.flush(); + + if (null != tempFile && config.isZipUseTempFile()) { + IOUtils.closeQuietly(out); + downloadFile(tempFile.getAbsolutePath(), response, "rename_me.zip", true); + } + + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + throw new GenericFilebrowserException(e); + } finally { + if (null != tempFile && config.isZipUseTempFile()) { + try { + if(!tempFile.delete()) { + tempFile.deleteOnExit(); + } + } catch (Exception e2) { + LOGGER.warn("could not delete tempfile " + tempFile.getAbsolutePath()); + tempFile.deleteOnExit(); + } + } + } + } + + /** + * checks if file is allowed for access + * + * @param path + * @param write + * @return + * @throws IOException + */ + protected boolean isAllowed(File path, boolean write) throws IOException { + return isAllowed(path, write, config.isReadOnly()); + } + + @Override + public String accessibleCSS(File file) { + String res = ""; + if (!file.canRead()) { + res += "not-readable"; + } + if (!file.canWrite()) { + res += " not-writeable"; + } + if (file.isHidden()) { + res += " file-hidden"; + } + return res.trim(); + } + + @Override + public String createFolder(String path, String folderName) throws IOException, GenericFilebrowserException { + if (StringUtils.isEmpty(path) && StringUtils.isEmpty(folderName)) { + throw new GenericFilebrowserException("Nothing to create."); + } + File file = new File(path); + if (file.exists()) { + if (isAllowed(file, true)) { + file = new File(file, folderName); + if(file.mkdirs()) { + return file.getAbsolutePath(); + } + throw new GenericFilebrowserException("Could not create directories."); + } + } else { + LOGGER.warn("[createFolder] folder already exists: " + path); + } + return null; + } + + @Override + public String deleteResource(String path) throws IOException, GenericFilebrowserException { + if (StringUtils.isEmpty(path)) { + throw new GenericFilebrowserException("Nothing to delete."); + } + File file = new File(path); + + if (file.isDirectory() && !config.isDelteFolderAllowed()) { + throw new GenericFilebrowserException("Delete folders functionality is deactivated."); + } + if (file.isFile() && !config.isDelteFileAllowed()) { + throw new GenericFilebrowserException("Delete files functionality is deactivated."); + } + if (!isAllowed(file, true)) { + throw new GenericFilebrowserException("Delete "+(file.isDirectory() ? "folder" : "file")+" is not allowed."); + } + if (!file.canWrite() && config.isNotDeletableIfNotWriteable()) { + throw new GenericFilebrowserException( + "Deleting " +(file.isDirectory() ? "folder" : "file") + " '" + file.getName() + "' is not allowed."); + } + + String parent = file.getParent(); + + if (file.isFile()) { + FileUtils.deleteQuietly(file); + } else { + try { + FileUtils.deleteDirectory(file); + } catch (Exception e) { + LOGGER.error("error deleting folder", e); + } + } + return parent; + } + + @Override + public Map getFileInfo(String path) throws IOException { + File file = new File(path); + Map result = new TreeMap<>(); + result.put("file", file); + + if (file.isFile() && file.canRead() && config.getMaxFilesizeForHashes() > file.length()) { + if (config.isInfoCrc32()) { + result.put("file.checksumCRC32", FileUtils.checksumCRC32(file)); + } + + FileInputStream fis = new FileInputStream(file); + if (config.isInfoMD5()) { + result.put("file.md5Hex", DigestUtils.md5Hex(fis)); + } + if (config.isInfoSha1()) { + result.put("file.sha1Hex", DigestUtils.sha1Hex(fis)); + } + if (config.isInfoSha256()) { + result.put("file.sha256Hex", DigestUtils.sha256Hex(fis)); + } +// result.put("file.sha384Hex", DigestUtils.sha384Hex(fis)); +// result.put("file.sha512Hex", DigestUtils.sha512Hex(fis)); + + IOUtils.closeQuietly(fis); + } + + result.put("file.lastModified", file.lastModified()); + result.put("file.canWrite", file.canWrite()); + result.put("file.canRead", file.canRead()); + result.put("file.canExecute", file.canExecute()); + result.put("file.isHidden", file.isHidden()); + + result.put("disk.totalSpace", file.getTotalSpace()); + result.put("disk.usableSpace", file.getUsableSpace()); + result.put("disk.freeSpace", file.getFreeSpace()); + result.put("disk.totalSpace.coreFormat", getFileSize(file.getTotalSpace())); + result.put("disk.usableSpace.coreFormat", getFileSize(file.getUsableSpace())); + result.put("disk.freeSpace.coreFormat", getFileSize(file.getFreeSpace())); + result.put("disk.totalSpace.commonFormat", getNormalFileSize(file.getTotalSpace())); + result.put("disk.usableSpace.commonFormat", getNormalFileSize(file.getUsableSpace())); + result.put("disk.freeSpace.commonFormat", getNormalFileSize(file.getFreeSpace())); + + String os = System.getProperty("os.name").toLowerCase(); + result.put("system.operationSystem", os); + + Class attributesClass = + isWindows(os) ? DosFileAttributes.class : BasicFileAttributes.class; + BasicFileAttributes attr = Files.readAttributes(file.toPath(), attributesClass); + + result.put("file.attr.creationTime", attr.creationTime()); + result.put("file.attr.lastAccessTime", attr.lastAccessTime()); + result.put("file.attr.lastModifiedTime", attr.lastModifiedTime()); + + result.put("file.attr.isDirectory", attr.isDirectory()); + result.put("file.attr.isOther", attr.isOther()); + result.put("file.attr.isRegularFile", attr.isRegularFile()); + result.put("file.attr.isSymbolicLink", attr.isSymbolicLink()); + result.put("file.attr.size", attr.size()); + + + if (DosFileAttributes.class.isAssignableFrom(attr.getClass())) { + result.put("file.attr.isArchive", ((DosFileAttributes)attr).isArchive()); + result.put("file.attr.isHidden", ((DosFileAttributes)attr).isHidden()); + result.put("file.attr.isReadOnly", ((DosFileAttributes)attr).isReadOnly()); + result.put("file.attr.isSystem", ((DosFileAttributes)attr).isSystem()); + } + + long size = file.length(); + if (file.isDirectory() && !attr.isSymbolicLink() && config.isCountFolderSize()) { + size = FileUtils.sizeOfDirectory(file); + } + result.put("file.size", size); + result.put("file.size.coreFormat", getFileSize(size)); + result.put("file.size.commonFormat", getNormalFileSize(size)); + + if (!isWindows(os)) { + Set permissions = Files.getPosixFilePermissions(file.toPath(), LinkOption.NOFOLLOW_LINKS); + result.put("file.permissions", PosixFilePermissions.toString(permissions)); + } + + return result; + } + + public static boolean isWindows(String os) { + return (os.indexOf("win") >= 0); + } + + @Override + public boolean saveFile(String decodedPath, MultipartFile upload) throws IOException, GenericFilebrowserException { + if (StringUtils.isEmpty(decodedPath)) { + return false; + } + File uploadFolder = new File(decodedPath); + if (uploadFolder.isDirectory() && isAllowed(uploadFolder, true)) { + + OutputStream fos = null; + try { + File file = new File(uploadFolder, upload.getOriginalFilename()); + fos = new FileOutputStream(file); + long result = IOUtils.copyLarge(upload.getInputStream(), fos); + LOGGER.info(String.format("uploaded %s bytes (%s)", result, getNormalFileSize(result))); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + return false; + } finally { + IOUtils.closeQuietly(fos); + } + return true; + } + throw new GenericFilebrowserException("upload not allowed"); + } +} diff --git a/admin-tools-filebrowser/src/main/resources/static/admintool/filebrowser/js/filebrowser.js b/admin-tools-filebrowser/src/main/resources/static/admintool/filebrowser/js/filebrowser.js index dfe3965..fbef258 100644 --- a/admin-tools-filebrowser/src/main/resources/static/admintool/filebrowser/js/filebrowser.js +++ b/admin-tools-filebrowser/src/main/resources/static/admintool/filebrowser/js/filebrowser.js @@ -1,69 +1,72 @@ -var allFilesSelected = false; -var allDirsSelected = false; - -$( document ).ready(function() { - - $('#selectFiles').on('click', function() { - $('input.file').each(function() { - $(this).prop( "checked", !allFilesSelected ) - }); - allFilesSelected = !allFilesSelected; - }); - - $('#selectDirs').on('click', function() { - $('input.dir').each(function() { - $(this).prop( "checked", !allDirsSelected ) - }); - allDirsSelected = !allDirsSelected; - }); - - $('#createDir').on('click', function() { - - getByID("createFolderModal").modal(); - }); - - $('#uploadFile').on('click', function() { - - var csrf = {}; - csrf[getCSRFHeader()] = getCSRFToken(); - var uploader = null; - uploader = new qq.FineUploader({ - element: $("#fine-uploader")[0], - request: { - endpoint: getWebContext() + '/admintool/filebrowser/upload', - customHeaders: csrf, - params: { - "currentDir" : $("#currentDir").text() - } - } - }); - - getByID("uploadModal").modal(); - }); - - $('.delete').each(function() { - var btn = $(this); - btn.on('click', function() { - var clickedBtn = $(this); - $("#resourceToDeleteShown").text(decodeURIComponent(decodeURI(clickedBtn.data("resource")))); - $("#resourceToDelete").val(clickedBtn.data("resource")); - getByID("deleteConfirmModal").modal(); - }); - }); - - $('.infoBtn').each(function() { - var btn = $(this); - btn.on('click', function() { - var link = $(this); - - sendRequest("/admintool/filebrowser/info?file=" + link.data('path'), "GET", "text", function(data) { - var div = getByID('infoModals'); - div.html(data); - div.modal(); - }); - - }); - }); - - +var allFilesSelected = false; +var allDirsSelected = false; + +$( document ).ready(function() { + + $('#selectFiles').on('click', function() { + $('input.file').each(function() { + $(this).prop( "checked", !allFilesSelected ) + }); + allFilesSelected = !allFilesSelected; + }); + + $('#selectDirs').on('click', function() { + $('input.dir').each(function() { + $(this).prop( "checked", !allDirsSelected ) + }); + allDirsSelected = !allDirsSelected; + }); + + $('#createDir').on('click', function() { + + getByID("createFolderModal").modal(); + }); + + $('#uploadFile').on('click', function() { + + var csrf = {}; + csrf[getCSRFHeader()] = getCSRFToken(); + var uploader = null; + uploader = new qq.FineUploader({ + element: $("#fine-uploader")[0], + request: { + endpoint: getWebContext() + '/admintool/filebrowser/upload', + customHeaders: csrf, + params: { + "currentDir" : $("#currentDir").text() + } + } + }); + var uploadModal = getByID("uploadModal"); + uploadModal.modal(); + uploadModal.on('hidden.bs.modal', function () { + location.reload(); + }); + }); + + $('.delete').each(function() { + var btn = $(this); + btn.on('click', function() { + var clickedBtn = $(this); + $("#resourceToDeleteShown").text(decodeURIComponent(decodeURI(clickedBtn.data("resource")))); + $("#resourceToDelete").val(clickedBtn.data("resource")); + getByID("deleteConfirmModal").modal(); + }); + }); + + $('.infoBtn').each(function() { + var btn = $(this); + btn.on('click', function() { + var link = $(this); + + sendRequest("/admintool/filebrowser/info?file=" + link.data('path'), "GET", "text", function(data) { + var div = getByID('infoModals'); + div.html(data); + div.modal(); + }); + + }); + }); + + }); \ No newline at end of file From 858fa3aef879b1cb8ae9ae68ec89b6b7a0de835f Mon Sep 17 00:00:00 2001 From: Andre Date: Mon, 19 Mar 2018 23:46:13 +0100 Subject: [PATCH 4/6] change version to 1.1.6.2 --- admin-tools-core-security/pom.xml | 2 +- admin-tools-core/pom.xml | 2 +- admin-tools-dbbrowser/pom.xml | 2 +- admin-tools-demo-core/pom.xml | 2 +- admin-tools-demo-jar/pom.xml | 2 +- admin-tools-demo-war/pom.xml | 2 +- admin-tools-filebrowser/pom.xml | 2 +- admin-tools-jminix/pom.xml | 2 +- admin-tools-log4j2/pom.xml | 2 +- admin-tools-melody/pom.xml | 2 +- admin-tools-properties/pom.xml | 2 +- admin-tools-quartz/pom.xml | 2 +- pom.xml | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/admin-tools-core-security/pom.xml b/admin-tools-core-security/pom.xml index 7163627..3a0b5be 100644 --- a/admin-tools-core-security/pom.xml +++ b/admin-tools-core-security/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-core/pom.xml b/admin-tools-core/pom.xml index b736e28..457efc2 100644 --- a/admin-tools-core/pom.xml +++ b/admin-tools-core/pom.xml @@ -7,7 +7,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-dbbrowser/pom.xml b/admin-tools-dbbrowser/pom.xml index d6c5382..93b1386 100644 --- a/admin-tools-dbbrowser/pom.xml +++ b/admin-tools-dbbrowser/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-demo-core/pom.xml b/admin-tools-demo-core/pom.xml index 71791e4..1b5f8f0 100644 --- a/admin-tools-demo-core/pom.xml +++ b/admin-tools-demo-core/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-demo-jar/pom.xml b/admin-tools-demo-jar/pom.xml index 7489a2b..de22d78 100644 --- a/admin-tools-demo-jar/pom.xml +++ b/admin-tools-demo-jar/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-demo-war/pom.xml b/admin-tools-demo-war/pom.xml index 08094fd..4fd5066 100644 --- a/admin-tools-demo-war/pom.xml +++ b/admin-tools-demo-war/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-filebrowser/pom.xml b/admin-tools-filebrowser/pom.xml index adb856e..d25d08e 100644 --- a/admin-tools-filebrowser/pom.xml +++ b/admin-tools-filebrowser/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-jminix/pom.xml b/admin-tools-jminix/pom.xml index e5a334d..ffc17da 100644 --- a/admin-tools-jminix/pom.xml +++ b/admin-tools-jminix/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-log4j2/pom.xml b/admin-tools-log4j2/pom.xml index ad37f27..3fdf750 100644 --- a/admin-tools-log4j2/pom.xml +++ b/admin-tools-log4j2/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-melody/pom.xml b/admin-tools-melody/pom.xml index d0c8b8f..991a64d 100644 --- a/admin-tools-melody/pom.xml +++ b/admin-tools-melody/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-properties/pom.xml b/admin-tools-properties/pom.xml index da3b367..a626a8c 100644 --- a/admin-tools-properties/pom.xml +++ b/admin-tools-properties/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/admin-tools-quartz/pom.xml b/admin-tools-quartz/pom.xml index 8117278..aac048b 100644 --- a/admin-tools-quartz/pom.xml +++ b/admin-tools-quartz/pom.xml @@ -6,7 +6,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 ../ diff --git a/pom.xml b/pom.xml index 56ffabd..03ad9b6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ de.chandre.admin-tools admin-tools - 1.1.6.1 + 1.1.6.2 pom Admin tools From 6a7a0940211b51788b6239f7c6aa4725f1daa64e Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 20 Mar 2018 00:04:46 +0100 Subject: [PATCH 5/6] update README.md's --- README.md | 5 +++-- admin-tools-core-security/README.md | 4 ++-- admin-tools-core/README.md | 2 +- admin-tools-dbbrowser/README.md | 4 ++-- admin-tools-filebrowser/README.md | 8 ++++++-- admin-tools-jminix/README.md | 4 ++-- admin-tools-log4j2/README.md | 4 ++-- admin-tools-melody/README.md | 4 ++-- admin-tools-properties/README.md | 4 ++-- admin-tools-quartz/README.md | 4 ++-- 10 files changed, 24 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c6982ac..d1d5ce1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ This is just a spare-time project. The usage of this tool (especially in production systems) is at your own risk. -Last Release: 1.1.6 - 02.10.2017 +Prev Release: 1.1.6.1 - 18.01.2018 +Last Release: 1.1.6.2 - 19.03.2018 [![Maven Central](https://img.shields.io/maven-central/v/de.chandre.admin-tools/admin-tools-core.svg)](https://mvnrepository.com/artifact/de.chandre.admin-tools) [![GitHub issues](https://img.shields.io/github/issues/andrehertwig/admintool.svg)](https://github.com/andrehertwig/admintool/issues) @@ -79,7 +80,7 @@ Include the dependencies in your dependency management. You can find it in [Mave de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 ... ``` diff --git a/admin-tools-core-security/README.md b/admin-tools-core-security/README.md index 4a3bd4e..0bc6916 100644 --- a/admin-tools-core-security/README.md +++ b/admin-tools-core-security/README.md @@ -19,12 +19,12 @@ de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 de.chandre.admin-tools admin-tools-core-security - 1.1.6 + 1.1.6.2 ``` diff --git a/admin-tools-core/README.md b/admin-tools-core/README.md index 4386b6f..4412a67 100644 --- a/admin-tools-core/README.md +++ b/admin-tools-core/README.md @@ -20,7 +20,7 @@ de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 ``` diff --git a/admin-tools-dbbrowser/README.md b/admin-tools-dbbrowser/README.md index b329020..cdb3736 100644 --- a/admin-tools-dbbrowser/README.md +++ b/admin-tools-dbbrowser/README.md @@ -32,12 +32,12 @@ Result will be displayed via jquery.datatables de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 de.chandre.admin-tools admin-tools-dbbrowser - 1.1.6 + 1.1.6.2 ``` diff --git a/admin-tools-filebrowser/README.md b/admin-tools-filebrowser/README.md index 494c689..ec5e6ea 100644 --- a/admin-tools-filebrowser/README.md +++ b/admin-tools-filebrowser/README.md @@ -34,12 +34,12 @@ de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 de.chandre.admin-tools admin-tools-filebrowser - 1.1.6 + 1.1.6.2 ``` @@ -118,6 +118,10 @@ admintool.filebrowser.createFolderAllowed=false # boolean: to enable action to delete folders admintool.filebrowser.delteFolderAllowed=false +#since 1.1.6.2 +# boolean: if set to true and file/folder to delete has no write permission, it's not allowed to delete them +admintool.filebrowser.notDeletableIfNotWriteable=true + #since 1.1.6 # boolean: to enable action to delete files admintool.filebrowser.delteFileAllowed=false diff --git a/admin-tools-jminix/README.md b/admin-tools-jminix/README.md index bde4ed8..5df30ac 100644 --- a/admin-tools-jminix/README.md +++ b/admin-tools-jminix/README.md @@ -12,12 +12,12 @@ de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 de.chandre.admin-tools admin-tools-jminix - 1.1.6 + 1.1.6.2 ``` diff --git a/admin-tools-log4j2/README.md b/admin-tools-log4j2/README.md index 0235b14..60e0861 100644 --- a/admin-tools-log4j2/README.md +++ b/admin-tools-log4j2/README.md @@ -15,12 +15,12 @@ de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 de.chandre.admin-tools admin-tools-log4j2 - 1.1.6 + 1.1.6.2 ``` diff --git a/admin-tools-melody/README.md b/admin-tools-melody/README.md index 99e8e02..4fb6298 100644 --- a/admin-tools-melody/README.md +++ b/admin-tools-melody/README.md @@ -22,12 +22,12 @@ http de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 de.chandre.admin-tools admin-tools-melody - 1.1.6 + 1.1.6.2 ``` diff --git a/admin-tools-properties/README.md b/admin-tools-properties/README.md index fddbcbc..1ac7275 100644 --- a/admin-tools-properties/README.md +++ b/admin-tools-properties/README.md @@ -12,12 +12,12 @@ de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 de.chandre.admin-tools admin-tools-properties - 1.1.6 + 1.1.6.2 ``` diff --git a/admin-tools-quartz/README.md b/admin-tools-quartz/README.md index 82eab35..0f32d93 100644 --- a/admin-tools-quartz/README.md +++ b/admin-tools-quartz/README.md @@ -22,12 +22,12 @@ de.chandre.admin-tools admin-tools-core - 1.1.6 + 1.1.6.2 de.chandre.admin-tools admin-tools-quartz - 1.1.6 + 1.1.6.2 ``` From c802d13418eb5326ba92aa96c4694de8bb0b1c04 Mon Sep 17 00:00:00 2001 From: Andre Date: Tue, 20 Mar 2018 00:14:23 +0100 Subject: [PATCH 6/6] release 1.1.6.2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d1d5ce1..fc73697 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is just a spare-time project. The usage of this tool (especially in production systems) is at your own risk. Prev Release: 1.1.6.1 - 18.01.2018 -Last Release: 1.1.6.2 - 19.03.2018 +Last Release: 1.1.6.2 - 20.03.2018 [![Maven Central](https://img.shields.io/maven-central/v/de.chandre.admin-tools/admin-tools-core.svg)](https://mvnrepository.com/artifact/de.chandre.admin-tools) [![GitHub issues](https://img.shields.io/github/issues/andrehertwig/admintool.svg)](https://github.com/andrehertwig/admintool/issues)