package com.boco.nbd.wios.downloadfile.controller; import com.boco.nbd.cams.core.config.RedisClient; import com.boco.nbd.cams.core.constant.CamsConstant; import com.boco.nbd.wios.downloadfile.fileio.FileIoEntity; import com.boco.nbd.wios.downloadfile.service.FileSupportService; import com.boco.nbd.wios.manage.contants.WiosConstant; import com.ihidea.core.base.CoreController; import com.ihidea.core.support.servlet.ServletHolderFilter; import com.ihidea.core.util.FileUtilsEx; import com.ihidea.core.util.ImageUtilsEx; import com.ihidea.core.util.JSONUtilsEx; import com.ihidea.core.util.ServletUtilsEx; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.apache.catalina.connector.ClientAbortException; import org.apache.commons.fileupload.disk.DiskFileItem; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author haica */ @Controller("fileStoreController") @Api(tags = "上传文件") public class FileController extends CoreController { @Autowired private RedisClient redisClient; @Autowired private FileSupportService service; private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES"; /** * 2:手机端上传,返回手机端需要格式 * 3:如果是kind editor */ private static final String RESULT_FLAG = "resultFlag"; private static final Integer PHONE_FLAG = 2; private static final Integer KIND_FLAG = 3; private static Map<String, String> contentTypeMap = new HashMap<>(70); static { contentTypeMap.put("html", "text/html"); contentTypeMap.put("htm", "text/html"); contentTypeMap.put("shtml", "text/html"); contentTypeMap.put("apk", "application/vnd.android.package-archive"); contentTypeMap.put("sis", "application/vnd.symbian.install"); contentTypeMap.put("sisx", "application/vnd.symbian.install"); contentTypeMap.put("exe", "application/x-msdownload"); contentTypeMap.put("msi", "application/x-msdownload"); contentTypeMap.put("css", "text/css"); contentTypeMap.put("xml", "text/xml"); contentTypeMap.put("gif", "image/gif"); contentTypeMap.put("jpeg", "image/jpeg"); contentTypeMap.put("jpg", "image/jpeg"); contentTypeMap.put("js", "application/x-javascript"); contentTypeMap.put("atom", "application/atom+xml"); contentTypeMap.put("rss", "application/rss+xml"); contentTypeMap.put("mml", "text/mathml"); contentTypeMap.put("txt", "text/plain"); contentTypeMap.put("jad", "text/vnd.sun.j2me.app-descriptor"); contentTypeMap.put("wml", "text/vnd.wap.wml"); contentTypeMap.put("htc", "text/x-component"); contentTypeMap.put("png", "image/png"); contentTypeMap.put("tif", "image/tiff"); contentTypeMap.put("tiff", "image/tiff"); contentTypeMap.put("wbmp", "image/vnd.wap.wbmp"); contentTypeMap.put("ico", "image/x-icon"); contentTypeMap.put("jng", "image/x-jng"); contentTypeMap.put("bmp", "image/x-ms-bmp"); contentTypeMap.put("svg", "image/svg+xml"); contentTypeMap.put("jar", "application/java-archive"); contentTypeMap.put("war", "application/java-archive"); contentTypeMap.put("ear", "application/java-archive"); contentTypeMap.put("doc", "application/msword"); contentTypeMap.put("pdf", "application/pdf"); contentTypeMap.put("rtf", "application/rtf"); contentTypeMap.put("xls", "application/vnd.ms-excel"); contentTypeMap.put("ppt", "application/vnd.ms-powerpoint"); contentTypeMap.put("7z", "application/x-7z-compressed"); contentTypeMap.put("rar", "application/x-rar-compressed"); contentTypeMap.put("swf", "application/x-shockwave-flash"); contentTypeMap.put("rpm", "application/x-redhat-package-manager"); contentTypeMap.put("der", "application/x-x509-ca-cert"); contentTypeMap.put("pem", "application/x-x509-ca-cert"); contentTypeMap.put("crt", "application/x-x509-ca-cert"); contentTypeMap.put("xhtml", "application/xhtml+xml"); contentTypeMap.put("zip", "application/zip"); contentTypeMap.put("mid", "audio/midi"); contentTypeMap.put("midi", "audio/midi"); contentTypeMap.put("kar", "audio/midi"); contentTypeMap.put("mp3", "audio/mpeg"); contentTypeMap.put("ogg", "audio/ogg"); contentTypeMap.put("m4a", "audio/x-m4a"); contentTypeMap.put("ra", "audio/x-realaudio"); contentTypeMap.put("3gpp", "video/3gpp"); contentTypeMap.put("3gp", "video/3gpp"); contentTypeMap.put("mp4", "video/mp4"); contentTypeMap.put("mpeg", "video/mpeg"); contentTypeMap.put("mpg", "video/mpeg"); contentTypeMap.put("mov", "video/quicktime"); contentTypeMap.put("flv", "video/x-flv"); contentTypeMap.put("m4v", "video/x-m4v"); contentTypeMap.put("mng", "video/x-mng"); contentTypeMap.put("asx", "video/x-ms-asf"); contentTypeMap.put("asf", "video/x-ms-asf"); contentTypeMap.put("wmv", "video/x-ms-wmv"); contentTypeMap.put("avi", "video/x-msvideo"); } /** * 上传文件 * * @param request * @param response * @param resultFlag 返回结果,2:手机端上传,返回手机端需要格式 */ @SuppressWarnings("unchecked") @RequestMapping(value = "/uploadFile.do") @ApiOperation(value = "上传文件") @ApiImplicitParams({ @ApiImplicitParam(name = "fileImgSize", value = "fileImgSize", dataType = "String", paramType = "query", required = true), @ApiImplicitParam(name = "storeName", value = "storeName", dataType = "String", paramType = "query", required = true), @ApiImplicitParam(name = "resultFlag", value = "resultFlag", dataType = "String", paramType = "query", required = false),}) public void upload(HttpServletRequest request, HttpServletResponse response, String fileImgSize, String storeName, Integer resultFlag) { Map<String, Object> result = new HashMap<>(8); try { Map<String, Object> param = ServletHolderFilter.getContext().getParamMap(); if (param.containsKey(RESULT_FLAG)) { resultFlag = Integer.valueOf(String.valueOf(param.get(RESULT_FLAG))); } List<Object[]> fileList = new ArrayList<>(); // 根据里面参数自动判断 for (String nameKey : param.keySet()) { Object _obj = param.get(nameKey); if (_obj != null && _obj instanceof List && ((List) _obj).size() > 0) { Object fileItem = ((List) _obj).get(0); if (fileItem instanceof DiskFileItem) { // 如果是servlet2上传的文件 List<DiskFileItem> diskFileItemList = (List<DiskFileItem>) _obj; for (DiskFileItem file : diskFileItemList) { String fileName = FileUtilsEx.getFileNameByPath(file.getName()); byte[] fileContent = file.get(); fileList.add(new Object[]{fileName, fileContent}); } } else if (fileItem instanceof Object[] && ((Object[]) fileItem).length == 2 && ((Object[]) fileItem)[0] instanceof String && ((Object[]) fileItem)[1] instanceof byte[]) { // 如果是servlet3上传的文件 List<Object[]> diskFileItemList = (List<Object[]>) _obj; for (Object[] file : diskFileItemList) { fileList.add(file); } } } } if (fileList.size() > 0) { List<String> fileIdList = new ArrayList<>(2); for (Object[] file : fileList) { fileIdList.add(service.add((String) file[0], (byte[]) file[1], StringUtils.isBlank(storeName) ? WiosConstant.OSS_DATA_STORE_NAME : storeName, fileImgSize)); } // 返回文件id result.put("fileIdList", fileIdList); // TODO 如果是kindeditor,则需要返回以下2个参数,后期考虑分离开 // 只能用数字 if (resultFlag != null && KIND_FLAG.equals(resultFlag)) { result.put("error", 0); result.put("url", "download/" + fileIdList.get(0)); } } else { logger.error("上传文件没有接受到文件!"); } } catch (Exception e) { logger.error(e.getMessage(), e); result.put("errorMsg", e.getMessage()); } // TODO 后面这里需要统一 if (resultFlag == null) { ServletUtilsEx.renderJson(response, result); } else if (PHONE_FLAG.equals(resultFlag)) { ServletUtilsEx.renderText(response, "{\"code\":\"200\",\"text\":null,\"data\":" + JSONUtilsEx.serialize(result) + "}"); } else if (KIND_FLAG.equals(resultFlag)) { ServletUtilsEx.renderJson(response, result); } } /** * 删除文件 * * @param fileId */ @RequestMapping(value = "/deleteFile.do") public void delete(String fileId) { service.remove(fileId); } /** * <pre> * 文件下载页面 * * 使用方式: * 页面定义<a href="downloadFile.do?id="+文件id target="blank">文件名</a> * * </pre> * * @param id blob表id * @param downloadFlag 默认下载 * @param response * @throws Exception */ @Deprecated @RequestMapping("/downloadFile.do") public void downloadFile(HttpServletRequest request, String id, String downloadFlag, HttpServletResponse response, String fileImgSize, String mineType) throws Exception { download(request, response, id, downloadFlag, fileImgSize, mineType); } private List<Range> getRange(HttpServletRequest request, HttpServletResponse response, FileIoEntity file) throws IOException { long length = file.getDataInfo().getFileSize().longValue(); long lastModified = file.getDataInfo().getCreateTime().getTime(); String eTag = file.getDataInfo().getFileName() + CamsConstant.UNDER_LINE + length + CamsConstant.UNDER_LINE + lastModified; // Prepare some variables. The full Range represents the complete file. Range full = new Range(0, length - 1, length); List<Range> ranges = new ArrayList<Range>(); // Validate and process Range and If-Range headers. String range = request.getHeader("Range"); if (range != null) { // Range header should match format "bytes=n-n,n-n,n-n...". If not, // then return 416. if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { response.setHeader("Content-Range", "bytes */" + length); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return null; } // If-Range header should either match ETag or be greater then // LastModified. If not, // then return full file. String ifRange = request.getHeader("If-Range"); if (ifRange != null && !ifRange.equals(eTag)) { try { long ifRangeTime = request.getDateHeader("If-Range"); if (ifRangeTime != -1 && ifRangeTime + 1000 < lastModified) { ranges.add(full); } } catch (IllegalArgumentException ignore) { ranges.add(full); } } // If any valid If-Range header, then process each part of byte // range. if (ranges.isEmpty()) { for (String part : range.substring(6).split(CamsConstant.COMMA)) { // Assuming a file with length of 100, the following // examples returns bytes at: // 50-80 (50 to 80), 40- (40 to length=100), -20 // (length-20=80 to length=100). long start = sublong(part, 0, part.indexOf("-")); long end = sublong(part, part.indexOf("-") + 1, part.length()); if (start == -1) { start = length - end; end = length - 1; } else if (end == -1 || end > length - 1) { end = length - 1; } else if (start == end) { start = 0; } // Check if Range is syntactically valid. If not, then // return 416. if (start > end) { response.setHeader("Content-Range", "bytes */" + length); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); return null; } // Add range. ranges.add(new Range(start, end, length)); } } } return ranges; } private String getContentType(String fileName) { String contentType = "application/octet-stream"; if (fileName.lastIndexOf(".") < 0) { return contentType; } fileName = FileUtilsEx.getSuffix(fileName).toLowerCase(); if (contentTypeMap.containsKey(fileName)) { contentType = contentTypeMap.get(fileName); } return contentType; } private static long sublong(String value, int beginIndex, int endIndex) { String substring = value.substring(beginIndex, endIndex); return (substring.length() > 0) ? Long.parseLong(substring) : -1; } protected class Range { long start; long end; long length; long total; public Range(long start, long end, long total) { this.start = start; this.end = end; this.length = end - start + 1; this.total = total; } } protected void download(final HttpServletRequest request, final HttpServletResponse response, String id, final String downloadFlag, final String fileImgSize, final String mineType) throws Exception { service.execute(id, (entity, is) -> { response.reset(); ServletOutputStream os = response.getOutputStream(); try { if (entity != null && is != null) { // TODO String previousToken = // servletRequest.getHeader("If-None-Match"); response.setHeader("ETag", entity.getDataInfo().getId() + CamsConstant.UNDER_LINE + entity.getDataInfo().getCreateTime().getTime()); response.setDateHeader("Last-Modified", entity.getDataInfo().getCreateTime().getTime()); // 1周失效 response.setDateHeader("Expires", System.currentTimeMillis() + 604800000L); String contentType = StringUtils.isNotBlank(mineType) ? mineType : getContentType(entity.getDataInfo().getFileName()); // 如果没有设置flag或为0,则下载,否则直接打印在窗口上 String disposition = (StringUtils.isBlank(downloadFlag) || "true".equals(downloadFlag)) ? "attachment" : ""; response.setHeader("Content-Disposition", disposition + ";filename=" + URLEncoder.encode(entity.getDataInfo().getFileName(), CamsConstant.UTF_8)); // 支持range List<Range> ranges = getRange(request, response, entity); try { // 如果启用压缩图片,则先从缓存中获取 if (StringUtils.isNotBlank(fileImgSize)) { byte[] imgBytes = redisClient.get("imageCache:" + entity.getDataInfo().getId() + "#" + fileImgSize, byte[].class); if (ArrayUtils.isEmpty(imgBytes)) { String[] sizeArray = fileImgSize.replace(",", "|").replace("x", "|").split("\\|"); imgBytes = ImageUtilsEx.resizeImage(is, Integer.valueOf(sizeArray[0]), Integer.valueOf(sizeArray[1]), FileUtilsEx.getSuffix(entity.getDataInfo().getFileName())); // 缓存10天 redisClient.put("imageCache:" + entity.getDataInfo().getId() + "#" + fileImgSize, imgBytes, 864000); } response.setContentType(contentType); response.setHeader("Content-Range", "bytes " + 0L + "-" + imgBytes.length + "/" + imgBytes.length); response.addHeader("Content-Length", String.valueOf(imgBytes.length)); IOUtils.write(imgBytes, os); } else if (ranges.size() == 0) { response.setContentType(contentType); response.setHeader("Content-Range", "bytes " + 0L + "-" + entity.getDataInfo().getFileSize().longValue() + "/" + entity.getDataInfo().getFileSize().longValue()); response.addHeader("Content-Length", String.valueOf(entity.getDataInfo().getFileSize().longValue())); byte[] buff = new byte[307200]; int cnt = 0; while ((cnt = is.read(buff)) != -1) { IOUtils.write(buff, os); } } else if (ranges.size() == 1) { response.setContentType(contentType); Range r = ranges.get(0); response.setHeader("Content-Range", "bytes " + r.start + "-" + r.end + "/" + r.total); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); if (r.length < 2147483647L) { response.setContentLength((int) r.length); } else { response.setHeader("Content-Length", String.valueOf(r.length)); } is.skip(r.start); byte[] buff = new byte[307200]; int cnt = 0; while ((cnt = is.read(buff)) != -1) { IOUtils.write(buff, os); } } else { // TODO 未测试 response.setContentType("multipart/byteranges; boundary=" + MULTIPART_BOUNDARY); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); for (Range r : ranges) { os.println(); os.println("--" + MULTIPART_BOUNDARY); os.println("Content-Type: " + contentType); os.println("Content-Range: bytes " + r.start + "-" + r.end + "/" + r.total); is.skip(r.start); byte[] buff = new byte[307200]; int cnt = 0; while ((cnt = is.read(buff)) != -1) { IOUtils.write(buff, os); } } os.println(); os.println("--" + MULTIPART_BOUNDARY + "--"); } os.flush(); } catch (ClientAbortException e) { logger.error("ClientAbortException err", e); } } else { response.sendError(404); } } catch (Exception e) { logger.debug(e.getMessage(), e); } catch (Error e) { logger.debug(e.getMessage(), e); } finally { if (os != null) { os.close(); } } }); } }