新增了minio文件存储服务器对接功能,新增系统错误页面,

This commit is contained in:
wanggeng 2021-10-18 00:11:38 +08:00
parent cb02db9c29
commit 05dbe8f0d4
19 changed files with 1188 additions and 501 deletions

View File

@ -0,0 +1,48 @@
package ink.wgink.properties;
/**
* @ClassName: FileMinIOProperties
* @Description: minio文件属性
* @Author: wanggeng
* @Date: 2021/10/17 5:04 下午
* @Version: 1.0
*/
public class FileMinIoProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private Boolean secure;
public String getEndpoint() {
return endpoint == null ? "" : endpoint.trim();
}
public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}
public String getAccessKey() {
return accessKey == null ? "" : accessKey.trim();
}
public void setAccessKey(String accessKey) {
this.accessKey = accessKey;
}
public String getSecretKey() {
return secretKey == null ? "" : secretKey.trim();
}
public void setSecretKey(String secretKey) {
this.secretKey = secretKey;
}
public Boolean getSecure() {
return secure == null ? false : secure;
}
public void setSecure(Boolean secure) {
this.secure = secure;
}
}

View File

@ -22,6 +22,8 @@ public class FileProperties {
private Integer maxFileCount;
private Double imageOutputQuality;
private FileMediaMaxDurationProperties mediaMaxDuration;
private Boolean useMinIo;
private FileMinIoProperties minIo;
public String getUploadPath() {
return uploadPath;
@ -87,6 +89,22 @@ public class FileProperties {
this.mediaMaxDuration = mediaMaxDuration;
}
public Boolean getUseMinIo() {
return useMinIo == null ? false : useMinIo;
}
public void setUseMinIo(Boolean useMinIo) {
this.useMinIo = useMinIo;
}
public FileMinIoProperties getMinIo() {
return minIo;
}
public void setMinIo(FileMinIoProperties minIo) {
this.minIo = minIo;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("{");
@ -106,6 +124,10 @@ public class FileProperties {
.append(imageOutputQuality);
sb.append(",\"mediaMaxDuration\":")
.append(mediaMaxDuration);
sb.append(",\"useMinIo\":")
.append(useMinIo);
sb.append(",\"minIo\":")
.append(minIo);
sb.append('}');
return sb.toString();
}

View File

@ -24,6 +24,8 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.sql.SQLSyntaxErrorException;
import java.util.ArrayList;
import java.util.List;
/**
@ -109,6 +111,20 @@ public class ResponseAdvice {
response.getWriter().write(JSON.toJSONString(result));
} else {
request.getSession().setAttribute("errorMessage", JSON.toJSONString(result));
if (!response.isCommitted()) {
StackTraceElement[] stackTraces = e.getStackTrace();
List<String> errorStackTraces = new ArrayList();
for(StackTraceElement stackTraceElement: stackTraces) {
errorStackTraces.add(stackTraceElement.toString());
}
try {
request.getSession().setAttribute("errorStackTraces", errorStackTraces);
response.sendRedirect(String.format("%s/system-error", request.getContextPath()));
} catch (IOException ioException) {
ioException.printStackTrace();
}
return;
}
}
}

View File

@ -1,38 +1,20 @@
package ink.wgink.common.base;
import ink.wgink.exceptions.ParamsException;
import ink.wgink.exceptions.SearchException;
import ink.wgink.interfaces.menu.IMenuBaseService;
import ink.wgink.interfaces.user.IUserCheckService;
import ink.wgink.pojo.dtos.menu.MenuDTO;
import ink.wgink.pojo.dtos.permission.SystemApiDTO;
import ink.wgink.util.RegexUtil;
import ink.wgink.util.map.HashMapUtil;
import ink.wgink.util.request.RequestUtil;
import io.swagger.models.Operation;
import io.swagger.models.Path;
import io.swagger.models.Swagger;
import io.swagger.models.Tag;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import springfox.documentation.service.Documentation;
import springfox.documentation.spring.web.DocumentationCache;
import springfox.documentation.swagger2.mappers.ServiceModelToSwagger2Mapper;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -53,6 +35,11 @@ public class DefaultBaseController {
@Autowired
protected HttpServletResponse httpServletResponse;
@GetMapping("system-error")
public ModelAndView error() {
return new ModelAndView("error/error");
}
/**
* 获取请求参数
*

View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<base th:href="${#request.getContextPath() + '/'}">
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, appversion-scalable=0">
<link rel="stylesheet" href="assets/layuiadmin/layui/css/layui.css" media="all">
<link rel="stylesheet" href="assets/layuiadmin/style/admin.css" media="all">
</head>
<body>
<div class="layui-fluid">
<div class="layadmin-tips">
<i class="layui-icon" face>&#xe664;</i>
<div class="layui-text" th:text="${session.errorMessage}"></div>
<button id="showErrorBtn" class="layui-btn layui-btn-xs" onclick="showError()">查看详情</button>
<ul id="errorStackTraces" style="display: none;">
<li th:each="errorStackTrace: ${session.errorStackTraces}" th:text="${errorStackTrace}"></li>
</ul>
</div>
<script>
function showError() {
document.getElementById('errorStackTraces').style.display = 'block';
document.getElementById('showErrorBtn').style.display = 'none';
}
</script>
</div>
</body>
</html>

View File

@ -60,7 +60,7 @@
{{# for(var i = 0, item = files[i]; item = files[i++];) { }}
<div class="upload-image-box">
<span class="upload-image-span">
<img src="route/file/download/false/{{item.fileId}}" align="加载失败">
<img src="route/file/download/true/{{item.fileId}}" align="加载失败">
</span>
<a class="layui-btn layui-btn-xs layui-btn-danger text-danger remove-image" href="javascript:void(0);" lay-form-button data-id="{{item.fileId}}" data-name="{{fileName}}" lay-filter="previewRemoveFile">
<i class="fa fa-trash-o"></i>

View File

@ -60,7 +60,7 @@
{{# for(var i = 0, item = files[i]; item = files[i++];) { }}
<div class="upload-image-box">
<span class="upload-image-span">
<img src="route/file/download/false/{{item.fileId}}" align="加载失败">
<img src="route/file/download/true/{{item.fileId}}" align="加载失败">
</span>
<a class="layui-btn layui-btn-xs layui-btn-danger text-danger remove-image" href="javascript:void(0);" lay-form-button data-id="{{item.fileId}}" data-name="{{fileName}}" lay-filter="previewRemoveFile">
<i class="fa fa-trash-o"></i>

View File

@ -32,6 +32,13 @@
<artifactId>thumbnailator</artifactId>
</dependency>
<!-- thumbnailator end -->
<!-- minio start -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
</dependency>
<!-- minio end -->
</dependencies>
</project>

View File

@ -3,9 +3,9 @@ package ink.wgink.module.file.controller.route;
import ink.wgink.common.base.DefaultBaseController;
import ink.wgink.exceptions.ParamsException;
import ink.wgink.interfaces.consts.ISystemConstant;
import ink.wgink.properties.FileProperties;
import ink.wgink.module.file.service.IFileService;
import ink.wgink.pojo.result.ErrorResult;
import ink.wgink.properties.FileProperties;
import io.swagger.annotations.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -15,11 +15,9 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* @ClassName: FileController
@ -121,23 +119,6 @@ public class FileRouteController extends DefaultBaseController {
@ApiResponses({@ApiResponse(code = 400, message = "请求失败", response = ErrorResult.class)})
@GetMapping("download/{isOpen}/{fileId}")
public void downLoad(HttpServletRequest request, HttpServletResponse response, @PathVariable("isOpen") Boolean isOpen, @PathVariable("fileId") String fileId) {
// 开启异步处理
AsyncContext asyncContext = request.startAsync(request, response);
CompletableFuture.runAsync(() -> {
asyncDownload(asyncContext, (HttpServletRequest) asyncContext.getRequest(), (HttpServletResponse) asyncContext.getResponse(), isOpen, fileId);
});
}
/**
* 异步下载文件
*
* @param asyncContext
* @param request
* @param response
* @param isOpen
* @param fileId
*/
private void asyncDownload(AsyncContext asyncContext, HttpServletRequest request, HttpServletResponse response, Boolean isOpen, String fileId) {
if (isOpen == null) {
throw new ParamsException("参数错误");
}
@ -148,7 +129,6 @@ public class FileRouteController extends DefaultBaseController {
params.put("fileId", fileId);
params.put("isOpen", isOpen);
fileService.downLoadFile(request, response, params);
asyncContext.complete();
}
/**

View File

@ -91,4 +91,12 @@ public interface IFileDao extends IInitBaseTable {
*/
List<FileInfoDTO> listByMd5(String fileMd5) throws SearchException;
/**
* 文件列表
*
* @param params
* @return
* @throws SearchException
*/
List<FilePO> listPO(Map<String, Object> params) throws SearchException;
}

View File

@ -0,0 +1,84 @@
package ink.wgink.module.file.service;
import ink.wgink.exceptions.FileException;
import ink.wgink.exceptions.base.SystemException;
import ink.wgink.module.file.enums.UploadTypeEnum;
import ink.wgink.pojo.pos.FilePO;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
* @ClassName: IDefaultFileService
* @Description: 默认文件业务
* @Author: wanggeng
* @Date: 2021/10/17 10:18 上午
* @Version: 1.0
*/
public interface IDefaultFileService {
/**
* 上传文件
*
* @param token app的Token
* @param uploadFile 上传文件
* @param uploadTypeEnum 上传文件类型
* @param params 参数
* @throws SystemException
*/
void uploadFile(String token, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) throws SystemException;
/**
* 物理删除文件
*
* @param filePOs 要删除的文件列表
*/
void deleteFile(List<FilePO> filePOs);
/**
* 保存文件信息
*
* @param token
* @param fileName
* @param uploadPath
* @param uploadFileName
* @param fileType
* @param fileSize
*/
void saveUploadFileInfo(String token, String fileName, String uploadPath, String uploadFileName, String fileType, long fileSize, Map<String, Object> params);
/**
* 获取上传文件路径
*
* @param baseUploadPath
* @param uploadTypeEnum
* @param fileType
* @return
* @throws FileException
*/
String getUploadPath(String baseUploadPath, UploadTypeEnum uploadTypeEnum, String fileType) throws FileException;
/**
* 下载文件
*
* @param request
* @param response
* @param params
* @param canRange
*/
void downLoadFile(HttpServletRequest request, HttpServletResponse response, Map<String, Object> params, boolean canRange);
/**
* 下载文件
*
* @param request
* @param response
* @param filePO
* @param isOpen
* @param canRange
*/
void downLoadFile(HttpServletRequest request, HttpServletResponse response, FilePO filePO, boolean isOpen, boolean canRange);
}

View File

@ -74,6 +74,14 @@ public interface IFileService {
* uEditor 文件列表
*/
String UEDITOR_LIST_FILE = "listfile";
/**
* 文件MD5值开头
*/
public static final String FILE_MD5_PREFIX = "MD5:";
/**
* 文件引用值开头
*/
public static final String FILE_REF_PREFIX = "REF:";
/**
* 保存文件
@ -236,6 +244,14 @@ public interface IFileService {
*/
List<FileDTO> list(List<String> idList);
/**
* 通过ID列表获取文件列表
*
* @param idList
* @return
*/
List<FilePO> listPO(List<String> idList);
/**
* 校验视频长度是否符合
*

View File

@ -0,0 +1,49 @@
package ink.wgink.module.file.service;
import ink.wgink.module.file.enums.UploadTypeEnum;
import ink.wgink.pojo.pos.FilePO;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
* @ClassName: IMinIOService
* @Description: minIO
* @Author: wanggeng
* @Date: 2021/10/17 10:15 上午
* @Version: 1.0
*/
public interface IMinIoFileService {
/**
* 物理删除文件
*
* @param filePOs 要删除的文件列表
*/
void deleteFile(List<FilePO> filePOs);
/**
* 上传文件
*
* @param token
* @param uploadFile
* @param uploadTypeEnum
* @param params
*/
void uploadFile(String token, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params);
/**
* 下载文件
*
* @param request
* @param response
* @param filePO
* @param isOpen
* @param canRange
*/
void downLoadFile(HttpServletRequest request, HttpServletResponse response, FilePO filePO, boolean isOpen, boolean canRange);
}

View File

@ -0,0 +1,508 @@
package ink.wgink.module.file.service.impl;
import ink.wgink.common.base.DefaultBaseService;
import ink.wgink.exceptions.FileException;
import ink.wgink.exceptions.SaveException;
import ink.wgink.exceptions.base.SystemException;
import ink.wgink.module.file.dao.IFileDao;
import ink.wgink.module.file.enums.UploadTypeEnum;
import ink.wgink.module.file.pojo.dtos.FileInfoDTO;
import ink.wgink.module.file.service.IDefaultFileService;
import ink.wgink.module.file.service.IFileService;
import ink.wgink.pojo.pos.FilePO;
import ink.wgink.properties.FileProperties;
import ink.wgink.util.UUIDUtil;
import ink.wgink.util.date.DateUtil;
import ink.wgink.util.request.StaticResourceRequestUtil;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @ClassName: DefaultFileServiceImpl
* @Description: 默认文件处理
* @Author: wanggeng
* @Date: 2021/10/17 10:22 上午
* @Version: 1.0
*/
@Service
public class DefaultFileServiceImpl extends DefaultBaseService implements IDefaultFileService {
private static final char[] HEX_CODE = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
@Autowired
private IFileDao fileDao;
@Autowired
private FileProperties fileProperties;
private String[] imageTypes;
private String[] videoTypes;
private String[] audioTypes;
private String[] fileTypes;
/**
* 上传文件
*
* @param token
* @param uploadFile
* @param uploadTypeEnum
* @param params
* @throws SystemException
*/
@Override
public void uploadFile(String token, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) throws SystemException {
String baseUploadPath = fileProperties.getUploadPath();
if (StringUtils.isBlank(baseUploadPath)) {
throw new SystemException("上传路径未配置");
}
String fileName = uploadFile.getOriginalFilename();
// 文件大小
long fileSize = uploadFile.getSize();
// 文件类型
String fileType = getFileType(fileName);
// 文件保存路径
String uploadPath = getUploadPath(baseUploadPath, uploadTypeEnum, fileType);
// 文件保存名称
String uploadFileName = getUploadFileName(fileType);
String fileMd5 = uploadFile(uploadFile, uploadPath, uploadFileName);
if (fileMd5 == null) {
throw new SaveException("文件上传失败");
}
// 获取MD5相同的文件
List<FileInfoDTO> fileInfoDTOs = fileDao.listByMd5(IFileService.FILE_MD5_PREFIX + fileMd5);
if (fileInfoDTOs.size() > 0) {
// 删除新增的文件
File uploadedFile = new File(uploadPath + File.separator + uploadFileName);
if (uploadedFile.exists()) {
uploadedFile.delete();
}
// 保存记录但文件信息都是原有的文件
params.clear();
FileInfoDTO fileInfoDTO = fileInfoDTOs.get(0);
params.put("fileSummary", "REF:" + fileInfoDTO.getFileId());
saveFile(token, params, fileInfoDTO.getFileName(), fileInfoDTO.getFilePath(), fileInfoDTO.getFileUrl(), fileInfoDTO.getFileType(), fileInfoDTO.getFileSize());
return;
}
params.put("fileSummary", "MD5:" + fileMd5);
saveUploadFileInfo(token, fileName, uploadPath, uploadFileName, fileType, fileSize, params);
}
@Override
public void deleteFile(List<FilePO> filePOs) {
Map<String, Object> fileParams = getHashMap(4);
// 删除文件
for (FilePO filePO : filePOs) {
// 如果文件描述为空可以直接删除源文件
if (StringUtils.isBlank(filePO.getFileSummary())) {
deleteSourceFile(filePO.getFilePath());
continue;
}
// 文件描述不为空时需要判断是否删除的是源文件源文件在一个系统中只保留一份
// 如果是引用文件的数据不删除源文件
if (filePO.getFileSummary().startsWith(IFileService.FILE_REF_PREFIX)) {
continue;
}
// 如果不是MD5源文件略过
if (!filePO.getFileSummary().startsWith(IFileService.FILE_MD5_PREFIX)) {
continue;
}
// 如果删除的是源文件需要查询系统中是否还存在引用的数据
List<FileInfoDTO> fileInfoDTOs = fileDao.listByMd5(IFileService.FILE_REF_PREFIX + filePO.getFileId());
// 如果不存在对源文件引用的数据则直接删除源文件
if (fileInfoDTOs.size() == 0) {
deleteSourceFile(filePO.getFilePath());
continue;
}
fileParams.clear();
// 如果存在引用数据取出第一个修改为源文件并将其他的引用更新为新的源文件ID
FileInfoDTO firstFileInfoDTO = fileInfoDTOs.get(0);
fileParams.put("fileSummary", firstFileInfoDTO.getFileSummary());
fileParams.put("fileId", firstFileInfoDTO.getFileId());
fileDao.updateSummary(fileParams);
// 获取其他的ID列表更新文件引用关系
List<String> otherFileIds = new ArrayList<>();
for (int i = 1; i < fileInfoDTOs.size(); i++) {
otherFileIds.add(fileInfoDTOs.get(i).getFileId());
}
// 如果不存在其它的引用略过
if (otherFileIds.isEmpty()) {
continue;
}
fileParams.remove("fileId");
fileParams.put("fileSummary", IFileService.FILE_REF_PREFIX + firstFileInfoDTO.getFileId());
fileParams.put("fileIds", otherFileIds);
fileDao.updateSummary(fileParams);
}
}
/**
* 删除源文件
*
* @param sourceFilePath 源文件路径
*/
private void deleteSourceFile(String sourceFilePath) {
File file = new File(sourceFilePath);
if (file.exists()) {
boolean isDelete = file.delete();
if (isDelete) {
LOG.debug("文件删除成功");
} else {
LOG.debug("文件删除失败");
}
}
}
/**
* 保存文件
*
* @param token token
* @param params 参数
* @param fileName 文件名
* @param fileFullPath 文件全路径
* @param fileUrl 文件相对地址
* @param fileType 文件类型
* @param fileSize 文件大小
*/
private void saveFile(String token, Map<String, Object> params, String fileName, String fileFullPath, String fileUrl, String fileType, long fileSize) {
params.put("fileId", UUIDUtil.getUUID());
params.put("fileName", fileName);
params.put("filePath", fileFullPath);
params.put("fileUrl", fileUrl);
params.put("fileType", fileType);
params.put("fileSize", fileSize);
params.put("isBack", 0);
if (StringUtils.isBlank(token)) {
setSaveInfo(params);
} else {
setAppSaveInfo(token, params);
}
fileDao.save(params);
}
@Override
public void saveUploadFileInfo(String token, String fileName, String uploadPath, String uploadFileName, String fileType, long fileSize, Map<String, Object> params) {
String fixPath = uploadPath.replace(fileProperties.getUploadPath(), "");
if ("\\".equals(File.separator)) {
fixPath = fixPath.replace("\\", "/");
}
if (fixPath.startsWith("/")) {
fixPath = fixPath.substring(1, fixPath.length() - 1);
}
String fileFullPath = String.format("%s%s%s", uploadPath, File.separator, uploadFileName);
// 压缩图片
if (isImageFile(fileType)) {
compressImage(fileFullPath);
File photo = new File(fileFullPath);
fileSize = photo.length();
}
saveFile(token, params, fileName, fileFullPath, String.format("files/%s/%s", fixPath, uploadFileName), fileType, fileSize);
}
@Override
public void downLoadFile(HttpServletRequest request, HttpServletResponse response, Map<String, Object> params, boolean canRange) {
FilePO filePO = fileDao.getPO(params);
boolean isOpen = Boolean.valueOf(params.get("isOpen").toString());
downLoadFile(request, response, filePO, isOpen, canRange);
}
@Override
public void downLoadFile(HttpServletRequest request, HttpServletResponse response, FilePO filePO, boolean isOpen, boolean canRange) {
try (
RandomAccessFile randomAccessFile = new RandomAccessFile(filePO.getFilePath(), "r");
FileChannel fileChannel = randomAccessFile.getChannel();
OutputStream outputStream = response.getOutputStream();
WritableByteChannel writableByteChannel = Channels.newChannel(outputStream);
) {
response.setHeader("Content-Length", filePO.getFileSize());
response.setContentType(StaticResourceRequestUtil.getContentType(filePO.getFileType()));
if (!isOpen) {
// 下载
response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
} else {
// 直接打开
response.setHeader("Content-Disposition", "inline;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
// 如果是图片资源开启缓存
if (isImageFile(filePO.getFileType())) {
// 如果存在校验修改时间且未做修改返回304使用本地资源
String ifModifiedSince = request.getHeader("If-Modified-Since");
if (StringUtils.isNotBlank(ifModifiedSince) && StringUtils.equalsIgnoreCase(ifModifiedSince, filePO.getGmtModified())) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
// 缓存有效时间为7天
response.setHeader("Expires", DateTime.now().plusDays(7).toDateTime(DateTimeZone.forID("GMT")).toString());
// 一小时之内不发送新请求
response.setHeader("max-age", "3600");
response.setHeader("Last-Modified", filePO.getGmtModified());
}
}
String rangeString = null;
if (canRange && request != null) {
rangeString = request.getHeader("Range");
LOG.debug("range: {}", rangeString);
}
long contentLength = Long.valueOf(filePO.getFileSize());
long startRange = 0;
long endRange = contentLength - 1;
if (!StringUtils.isBlank(rangeString)) {
if (!isOpen) {
response.setContentType("multipart/byteranges");
}
String[] rangeArray = rangeString.substring(rangeString.indexOf("=") + 1).split("-");
startRange = Long.valueOf(rangeArray[0]);
if (rangeArray.length > 1) {
endRange = Long.valueOf(rangeArray[1]);
}
setRangeHeader(startRange, endRange, response, filePO.getFileId(), contentLength);
randomAccessFile.seek(startRange);
}
LOG.debug("startRange: {}, endRange: {}", startRange, endRange);
long totalOutputLength = endRange - startRange + 1;
fileChannel.transferTo(startRange, totalOutputLength, writableByteChannel);
outputStream.flush();
} catch (Exception e) {
if (e instanceof ClientAbortException) {
LOG.debug("客户端断开连接");
} else {
throw new FileException("文件输出异常", e);
}
}
}
/**
* 处理视频流问题
*
* @param startRange
* @param endRange
* @param response
* @param fileId
* @param contentLength
*/
private void setRangeHeader(long startRange, long endRange, HttpServletResponse response, String fileId, long contentLength) {
// 这里不设置会出现第一次加载很慢的情况
response.setHeader("Content-Length", String.valueOf(endRange - startRange + 1));
response.setHeader("Content-Range", String.format("bytes %d-%d/%d", startRange, endRange, contentLength));
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Etag", fileId);
response.setStatus(206);
}
/**
* 是否是图片文件, GIF图片不压缩
*
* @param fileType
* @return
*/
private boolean isImageFile(String fileType) {
if (StringUtils.equalsIgnoreCase("gif", fileType)) {
return false;
}
String imageTypes = fileProperties.getImageTypes();
for (String imageType : imageTypes.split(",")) {
if (StringUtils.equalsIgnoreCase(fileType, imageType)) {
return true;
}
}
return false;
}
/**
* 压缩图片
*
* @param fileFullPath
*/
private void compressImage(String fileFullPath) {
if (fileFullPath.endsWith(".blob")) {
return;
}
try {
Thumbnails.of(fileFullPath).scale(1.0f).outputQuality(fileProperties.getImageOutputQuality()).toFile(fileFullPath);
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
/**
* 获取文件类型
*
* @param fileName
* @return
*/
private String getFileType(String fileName) {
String[] names = fileName.split("\\.");
if (names != null) {
return names[names.length - 1].toLowerCase();
}
return "";
}
public String[] getImageTypes() {
return imageTypes == null ? fileProperties.getImageTypes().split(",") : imageTypes;
}
public void setImageTypes(String[] imageTypes) {
this.imageTypes = imageTypes;
}
public String[] getVideoTypes() {
return videoTypes == null ? fileProperties.getVideoTypes().split(",") : videoTypes;
}
public void setVideoTypes(String[] videoTypes) {
this.videoTypes = videoTypes;
}
public String[] getAudioTypes() {
return audioTypes == null ? fileProperties.getAudioTypes().split(",") : audioTypes;
}
public void setAudioTypes(String[] audioTypes) {
this.audioTypes = audioTypes;
}
public String[] getFileTypes() {
return fileTypes == null ? fileProperties.getFileTypes().split(",") : fileTypes;
}
public void setFileTypes(String[] fileTypes) {
this.fileTypes = fileTypes;
}
/**
* 校验类型
*
* @param types
* @param fileType
* @return
*/
private boolean isTypeCorrect(String[] types, String fileType) {
for (String type : types) {
if (StringUtils.equalsIgnoreCase(fileType, type)) {
return true;
}
}
return false;
}
@Override
public String getUploadPath(String baseUploadPath, UploadTypeEnum uploadTypeEnum, String fileType) throws FileException {
StringBuilder filePath = new StringBuilder();
if (!baseUploadPath.endsWith(File.separator)) {
filePath.append(baseUploadPath).append(File.separator);
} else {
filePath.append(baseUploadPath);
}
boolean hasFileType = !StringUtils.isBlank(fileType);
if (uploadTypeEnum.getValue() == UploadTypeEnum.IMAGE.getValue()) {
if (hasFileType && !isTypeCorrect(getImageTypes(), fileType)) {
throw new FileException("图片格式不支持上传");
}
filePath.append("images");
} else if (uploadTypeEnum.getValue() == UploadTypeEnum.VIDEO.getValue()) {
if (hasFileType && !isTypeCorrect(getVideoTypes(), fileType)) {
throw new FileException("视频格式不支持上传");
}
filePath.append("videos");
} else if (uploadTypeEnum.getValue() == UploadTypeEnum.AUDIO.getValue()) {
if (hasFileType && !isTypeCorrect(getAudioTypes(), fileType)) {
throw new FileException("音频格式不支持上传");
}
filePath.append("audios");
} else if (uploadTypeEnum.getValue() == UploadTypeEnum.ERROR_EXCEL.getValue()) {
filePath.append("errorexcels");
} else {
if (hasFileType && !isTypeCorrect(getFileTypes(), fileType)) {
throw new FileException("文件格式不支持上传");
}
filePath.append("files");
}
filePath.append(File.separator).append(DateUtil.getDays());
return filePath.toString();
}
/**
* 获取上传文件名称
*
* @param fileType
* @return
*/
private String getUploadFileName(String fileType) {
String uploadFileName = UUIDUtil.get32UUID();
if (!StringUtils.isEmpty(fileType)) {
uploadFileName += "." + fileType;
}
return uploadFileName;
}
/**
* 保存文件到本地
*
* @param uploadFile
* @param filePath
* @param uploadFileName
* @return
*/
private String uploadFile(MultipartFile uploadFile, String filePath, String uploadFileName) throws FileException {
String fileMd5;
File uploadFolder = new File(filePath);
if (!uploadFolder.exists()) {
uploadFolder.mkdirs();
}
InputStream uploadFileInputStream = null;
FileOutputStream uploadFileOutputStream = null;
try {
uploadFileInputStream = uploadFile.getInputStream();
uploadFileOutputStream = new FileOutputStream(new File(uploadFolder + "/" + uploadFileName));
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
for (byte[] buf = new byte[IFileService.INPUT_STREAM_SIZE]; uploadFileInputStream.read(buf) > -1; ) {
uploadFileOutputStream.write(buf, 0, buf.length);
messageDigest.update(buf, 0, buf.length);
}
uploadFileOutputStream.flush();
// 计算文件的MD5
byte[] data = messageDigest.digest();
StringBuilder fileMd5SB = new StringBuilder(data.length * 2);
for (byte b : data) {
fileMd5SB.append(HEX_CODE[(b >> 4) & 0xF]);
fileMd5SB.append(HEX_CODE[(b & 0xF)]);
}
fileMd5 = fileMd5SB.toString();
} catch (Exception e) {
LOG.error(e.getMessage(), e);
throw new FileException("文件上传失败");
} finally {
try {
if (null != uploadFileOutputStream) {
uploadFileOutputStream.close();
}
if (null != uploadFileInputStream) {
uploadFileInputStream.close();
}
} catch (Exception e1) {
LOG.error(e1.getMessage());
throw new FileException("文件上传失败");
}
}
return fileMd5;
}
}

View File

@ -7,7 +7,6 @@ import com.github.pagehelper.PageInfo;
import ink.wgink.common.base.DefaultBaseService;
import ink.wgink.exceptions.FileException;
import ink.wgink.exceptions.ParamsException;
import ink.wgink.exceptions.SaveException;
import ink.wgink.exceptions.SearchException;
import ink.wgink.exceptions.base.SystemException;
import ink.wgink.module.file.dao.IFileDao;
@ -15,7 +14,9 @@ import ink.wgink.module.file.enums.UploadTypeEnum;
import ink.wgink.module.file.pojo.dtos.FileDTO;
import ink.wgink.module.file.pojo.dtos.FileInfoDTO;
import ink.wgink.module.file.pojo.vos.FileVO;
import ink.wgink.module.file.service.IDefaultFileService;
import ink.wgink.module.file.service.IFileService;
import ink.wgink.module.file.service.IMinIoFileService;
import ink.wgink.pojo.ListPage;
import ink.wgink.pojo.pos.FilePO;
import ink.wgink.pojo.result.ErrorResult;
@ -23,18 +24,12 @@ import ink.wgink.pojo.result.SuccessResultList;
import ink.wgink.properties.FileProperties;
import ink.wgink.util.ResourceUtil;
import ink.wgink.util.UUIDUtil;
import ink.wgink.util.date.DateUtil;
import ink.wgink.util.map.HashMapUtil;
import ink.wgink.util.request.StaticResourceRequestUtil;
import it.sauronsoftware.jave.Encoder;
import it.sauronsoftware.jave.EncoderException;
import it.sauronsoftware.jave.MultimediaInfo;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -44,12 +39,8 @@ import org.springframework.web.multipart.commons.CommonsMultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.security.MessageDigest;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
@ -63,25 +54,16 @@ import java.util.*;
public class FileServiceImpl extends DefaultBaseService implements IFileService {
private static final Logger LOG = LoggerFactory.getLogger(FileServiceImpl.class);
private static final char[] HEX_CODE = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
/**
* 文件MD5值开头
*/
public static final String FILE_MD5_PREFIX = "MD5:";
/**
* 文件引用值开头
*/
public static final String FILE_REF_PREFIX = "REF:";
@Autowired
private FileProperties fileProperties;
@Autowired
private IFileDao fileDao;
private String[] imageTypes;
private String[] videoTypes;
private String[] audioTypes;
private String[] fileTypes;
@Autowired
private IDefaultFileService defaultFileService;
@Autowired
private IMinIoFileService minIoFileService;
public List<FileInfoDTO> listFileInfo(Map<String, Object> params) {
return fileDao.listInfo(params);
@ -117,73 +99,17 @@ public class FileServiceImpl extends DefaultBaseService implements IFileService
public void delete(List<String> ids) {
Map<String, Object> params = getHashMap(2);
params.put("fileIds", ids);
Map<String, Object> fileParams = getHashMap(4);
List<FileInfoDTO> fileInfoWithPathDTOs = fileDao.listInfo(params);
// 删除文件
for (FileInfoDTO fileInfoDTO : fileInfoWithPathDTOs) {
// 如果文件描述为空可以直接删除源文件
if (StringUtils.isBlank(fileInfoDTO.getFileSummary())) {
deleteSourceFile(fileInfoDTO.getFilePath());
continue;
}
// 文件描述不为空时需要判断是否删除的是源文件源文件在一个系统中只保留一份
// 如果是引用文件的数据不删除源文件
if (fileInfoDTO.getFileSummary().startsWith(FILE_REF_PREFIX)) {
continue;
}
// 如果不是MD5源文件略过
if (!fileInfoDTO.getFileSummary().startsWith(FILE_MD5_PREFIX)) {
continue;
}
// 如果删除的是源文件需要查询系统中是否还存在引用的数据
List<FileInfoDTO> fileInfoDTOs = fileDao.listByMd5(FILE_REF_PREFIX + fileInfoDTO.getFileId());
// 如果不存在对源文件引用的数据则直接删除源文件
if (fileInfoDTOs.size() == 0) {
deleteSourceFile(fileInfoDTO.getFilePath());
continue;
}
fileParams.clear();
// 如果存在引用数据取出第一个修改为源文件并将其他的引用更新为新的源文件ID
FileInfoDTO firstFileInfoDTO = fileInfoDTOs.get(0);
fileParams.put("fileSummary", firstFileInfoDTO.getFileSummary());
fileParams.put("fileId", firstFileInfoDTO.getFileId());
fileDao.updateSummary(fileParams);
// 获取其他的ID列表更新文件引用关系
List<String> otherFileIds = new ArrayList<>();
for (int i = 1; i < fileInfoDTOs.size(); i++) {
otherFileIds.add(fileInfoDTOs.get(i).getFileId());
}
// 如果不存在其它的引用略过
if (otherFileIds.isEmpty()) {
continue;
}
fileParams.remove("fileId");
fileParams.put("fileSummary", FILE_REF_PREFIX + firstFileInfoDTO.getFileId());
fileParams.put("fileIds", otherFileIds);
fileDao.updateSummary(fileParams);
List<FilePO> filePOs = listPO(ids);
if (fileProperties.getUseMinIo()) {
minIoFileService.deleteFile(filePOs);
} else {
defaultFileService.deleteFile(filePOs);
}
// 删除记录
fileDao.delete(params);
}
/**
* 删除源文件
*
* @param sourceFilePath 源文件路径
*/
private void deleteSourceFile(String sourceFilePath) {
File file = new File(sourceFilePath);
if (file.exists()) {
boolean isDelete = file.delete();
if (isDelete) {
LOG.debug("文件删除成功");
} else {
LOG.debug("文件删除失败");
}
}
}
@Override
public String uploadSingle(MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) throws SystemException {
uploadFile(null, uploadFile, uploadTypeEnum, params);
@ -217,45 +143,16 @@ public class FileServiceImpl extends DefaultBaseService implements IFileService
* @throws SystemException
*/
private void uploadFile(String token, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) throws SystemException {
String baseUploadPath = fileProperties.getUploadPath();
if (StringUtils.isBlank(baseUploadPath)) {
throw new SystemException("上传路径未配置");
if (fileProperties.getUseMinIo()) {
minIoFileService.uploadFile(token, uploadFile, uploadTypeEnum, params);
} else {
defaultFileService.uploadFile(token, uploadFile, uploadTypeEnum, params);
}
String fileName = uploadFile.getOriginalFilename();
// 文件大小
long fileSize = uploadFile.getSize();
// 文件类型
String fileType = getFileType(fileName);
// 文件保存路径
String uploadPath = getUploadPath(baseUploadPath, uploadTypeEnum, fileType);
// 文件保存名称
String uploadFileName = getUploadFileName(fileType);
String fileMd5 = uploadFile(uploadFile, uploadPath, uploadFileName);
if (fileMd5 == null) {
throw new SaveException("文件上传失败");
}
// 获取MD5相同的文件
List<FileInfoDTO> fileInfoDTOs = fileDao.listByMd5(FILE_MD5_PREFIX + fileMd5);
if (fileInfoDTOs.size() > 0) {
// 删除新增的文件
File uploadedFile = new File(uploadPath + File.separator + uploadFileName);
if (uploadedFile.exists()) {
uploadedFile.delete();
}
// 保存记录但文件信息都是原有的文件
params.clear();
FileInfoDTO fileInfoDTO = fileInfoDTOs.get(0);
params.put("fileSummary", "REF:" + fileInfoDTO.getFileId());
saveFile(token, params, fileInfoDTO.getFileName(), fileInfoDTO.getFilePath(), fileInfoDTO.getFileUrl(), fileInfoDTO.getFileType(), fileInfoDTO.getFileSize());
return;
}
params.put("fileSummary", "MD5:" + fileMd5);
saveUploadFileInfo(token, fileName, uploadPath, uploadFileName, fileType, fileSize, params);
}
@Override
public void uploadErrorExcelFileInfo(String fileName, String uploadPath, long fileSize, Map<String, Object> params) {
saveUploadFileInfo(null, fileName, uploadPath, fileName, "xls", fileSize, params);
defaultFileService.saveUploadFileInfo(null, fileName, uploadPath, fileName, "xls", fileSize, params);
}
@Override
@ -264,7 +161,7 @@ public class FileServiceImpl extends DefaultBaseService implements IFileService
if (StringUtils.isBlank(baseUploadPath)) {
throw new SystemException("上传路径未配置");
}
return getUploadPath(baseUploadPath, UploadTypeEnum.ERROR_EXCEL, null);
return defaultFileService.getUploadPath(baseUploadPath, UploadTypeEnum.ERROR_EXCEL, null);
}
@Override
@ -274,32 +171,11 @@ public class FileServiceImpl extends DefaultBaseService implements IFileService
return fileDao.list(params);
}
/**
* 保存文件信息
*
* @param token
* @param fileName
* @param uploadPath
* @param uploadFileName
* @param fileType
* @param fileSize
*/
private void saveUploadFileInfo(String token, String fileName, String uploadPath, String uploadFileName, String fileType, long fileSize, Map<String, Object> params) {
String fixPath = uploadPath.replace(fileProperties.getUploadPath(), "");
if ("\\".equals(File.separator)) {
fixPath = fixPath.replace("\\", "/");
}
if (fixPath.startsWith("/")) {
fixPath = fixPath.substring(1, fixPath.length() - 1);
}
String fileFullPath = String.format("%s%s%s", uploadPath, File.separator, uploadFileName);
// 压缩图片
if (isImageFile(fileType)) {
compressImage(fileFullPath);
File photo = new File(fileFullPath);
fileSize = photo.length();
}
saveFile(token, params, fileName, fileFullPath, String.format("files/%s/%s", fixPath, uploadFileName), fileType, fileSize);
@Override
public List<FilePO> listPO(List<String> idList) {
Map<String, Object> params = getHashMap(2);
params.put("fileIds", idList);
return fileDao.listPO(params);
}
@Override
@ -313,32 +189,6 @@ public class FileServiceImpl extends DefaultBaseService implements IFileService
return fileId;
}
/**
* 保存文件
*
* @param token token
* @param params 参数
* @param fileName 文件名
* @param fileFullPath 文件全路径
* @param fileUrl 文件相对地址
* @param fileType 文件类型
* @param fileSize 文件大小
*/
private void saveFile(String token, Map<String, Object> params, String fileName, String fileFullPath, String fileUrl, String fileType, long fileSize) {
params.put("fileId", UUIDUtil.getUUID());
params.put("fileName", fileName);
params.put("filePath", fileFullPath);
params.put("fileUrl", fileUrl);
params.put("fileType", fileType);
params.put("fileSize", fileSize);
params.put("isBack", 0);
if (StringUtils.isBlank(token)) {
setSaveInfo(params);
} else {
setAppSaveInfo(token, params);
}
fileDao.save(params);
}
@Override
public void downLoadFile(HttpServletResponse response, Map<String, Object> params) {
@ -354,85 +204,18 @@ public class FileServiceImpl extends DefaultBaseService implements IFileService
public void downLoadFile(HttpServletRequest request, HttpServletResponse response, Map<String, Object> params, boolean canRange) {
FilePO filePO = fileDao.getPO(params);
if (null == filePO) {
throw new SearchException("文件获取失败");
throw new FileException("文件不存在");
}
try (
RandomAccessFile randomAccessFile = new RandomAccessFile(filePO.getFilePath(), "r");
FileChannel fileChannel = randomAccessFile.getChannel();
OutputStream outputStream = response.getOutputStream();
WritableByteChannel writableByteChannel = Channels.newChannel(outputStream);
) {
boolean isOpen = Boolean.valueOf(params.get("isOpen").toString());
response.setHeader("Content-Length", filePO.getFileSize());
response.setContentType(StaticResourceRequestUtil.getContentType(filePO.getFileType()));
if (!isOpen) {
response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
} else {
response.setHeader("Content-Disposition", "inline;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
// 如果是图片资源开启缓存
if (isImageFile(filePO.getFileType())) {
// 如果存在校验修改时间且未做修改返回304使用本地资源
String ifModifiedSince = request.getHeader("If-Modified-Since");
if (StringUtils.isNotBlank(ifModifiedSince) && StringUtils.equalsIgnoreCase(ifModifiedSince, filePO.getGmtModified())) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
// 没有备份的数据从本地下载
if (filePO.getIsBack() == 0) {
defaultFileService.downLoadFile(request, response, filePO, isOpen, canRange);
return;
}
// 缓存有效时间为7天
response.setHeader("Expires", DateTime.now().plusDays(7).toDateTime(DateTimeZone.forID("GMT")).toString());
// 一小时之内不发送新请求
response.setHeader("max-age", "3600");
response.setHeader("Last-Modified", filePO.getGmtModified());
if (fileProperties.getUseMinIo()) {
minIoFileService.downLoadFile(request, response, filePO, isOpen, canRange);
}
}
String rangeString = null;
if (canRange && request != null) {
rangeString = request.getHeader("Range");
LOG.debug("range: {}", rangeString);
}
long contentLength = Long.valueOf(filePO.getFileSize());
long startRange = 0;
long endRange = contentLength - 1;
if (!StringUtils.isBlank(rangeString)) {
if (!isOpen) {
response.setContentType("multipart/byteranges");
}
String[] rangeArray = rangeString.substring(rangeString.indexOf("=") + 1).split("-");
startRange = Long.valueOf(rangeArray[0]);
if (rangeArray.length > 1) {
endRange = Long.valueOf(rangeArray[1]);
}
setRangeHeader(startRange, endRange, response, filePO.getFileId(), contentLength);
randomAccessFile.seek(startRange);
}
LOG.debug("startRange: {}, endRange: {}", startRange, endRange);
long totalOutputLength = endRange - startRange + 1;
fileChannel.transferTo(startRange, totalOutputLength, writableByteChannel);
outputStream.flush();
} catch (Exception e) {
if (e instanceof ClientAbortException) {
LOG.debug("客户端断开连接");
} else {
throw new FileException("文件输出异常", e);
}
}
}
/**
* 处理视频流问题
*
* @param startRange
* @param endRange
* @param response
* @param fileId
* @param contentLength
*/
private void setRangeHeader(long startRange, long endRange, HttpServletResponse response, String fileId, long contentLength) {
// 这里不设置会出现第一次加载很慢的情况
response.setHeader("Content-Length", String.valueOf(endRange - startRange + 1));
response.setHeader("Content-Range", String.format("bytes %d-%d/%d", startRange, endRange, contentLength));
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Etag", fileId);
response.setStatus(206);
throw new FileException("文件下载异常");
}
@Override
@ -598,212 +381,4 @@ public class FileServiceImpl extends DefaultBaseService implements IFileService
return JSONObject.parseObject(uEditorConfig);
}
public String[] getImageTypes() {
return imageTypes == null ? fileProperties.getImageTypes().split(",") : imageTypes;
}
public void setImageTypes(String[] imageTypes) {
this.imageTypes = imageTypes;
}
public String[] getVideoTypes() {
return videoTypes == null ? fileProperties.getVideoTypes().split(",") : videoTypes;
}
public void setVideoTypes(String[] videoTypes) {
this.videoTypes = videoTypes;
}
public String[] getAudioTypes() {
return audioTypes == null ? fileProperties.getAudioTypes().split(",") : audioTypes;
}
public void setAudioTypes(String[] audioTypes) {
this.audioTypes = audioTypes;
}
public String[] getFileTypes() {
return fileTypes == null ? fileProperties.getFileTypes().split(",") : fileTypes;
}
public void setFileTypes(String[] fileTypes) {
this.fileTypes = fileTypes;
}
/**
* 校验类型
*
* @param types
* @param fileType
* @return
*/
private boolean isTypeCorrect(String[] types, String fileType) {
for (String type : types) {
if (StringUtils.equalsIgnoreCase(fileType, type)) {
return true;
}
}
return false;
}
/**
* 获取文件类型
*
* @param fileName
* @return
*/
private String getFileType(String fileName) {
String[] names = fileName.split("\\.");
if (names != null) {
return names[names.length - 1].toLowerCase();
}
return "";
}
/**
* 获取上传文件路径
*
* @param baseUploadPath
* @param uploadTypeEnum
* @param fileType
* @return
* @throws FileException
*/
private String getUploadPath(String baseUploadPath, UploadTypeEnum uploadTypeEnum, String fileType) throws FileException {
StringBuilder filePath = new StringBuilder();
if (!baseUploadPath.endsWith(File.separator)) {
filePath.append(baseUploadPath).append(File.separator);
} else {
filePath.append(baseUploadPath);
}
boolean hasFileType = !StringUtils.isBlank(fileType);
if (uploadTypeEnum.getValue() == UploadTypeEnum.IMAGE.getValue()) {
if (hasFileType && !isTypeCorrect(getImageTypes(), fileType)) {
throw new FileException("图片格式不支持上传");
}
filePath.append("images");
} else if (uploadTypeEnum.getValue() == UploadTypeEnum.VIDEO.getValue()) {
if (hasFileType && !isTypeCorrect(getVideoTypes(), fileType)) {
throw new FileException("视频格式不支持上传");
}
filePath.append("videos");
} else if (uploadTypeEnum.getValue() == UploadTypeEnum.AUDIO.getValue()) {
if (hasFileType && !isTypeCorrect(getAudioTypes(), fileType)) {
throw new FileException("音频格式不支持上传");
}
filePath.append("audios");
} else if (uploadTypeEnum.getValue() == UploadTypeEnum.ERROR_EXCEL.getValue()) {
filePath.append("errorexcels");
} else {
if (hasFileType && !isTypeCorrect(getFileTypes(), fileType)) {
throw new FileException("文件格式不支持上传");
}
filePath.append("files");
}
filePath.append(File.separator).append(DateUtil.getDays());
return filePath.toString();
}
/**
* 获取上传文件名称
*
* @param fileType
* @return
*/
private String getUploadFileName(String fileType) {
String uploadFileName = UUIDUtil.get32UUID();
if (!StringUtils.isEmpty(fileType)) {
uploadFileName += "." + fileType;
}
return uploadFileName;
}
/**
* 保存文件到本地
*
* @param uploadFile
* @param filePath
* @param uploadFileName
* @return
*/
private String uploadFile(MultipartFile uploadFile, String filePath, String uploadFileName) throws FileException {
String fileMd5;
File uploadFolder = new File(filePath);
if (!uploadFolder.exists()) {
uploadFolder.mkdirs();
}
InputStream uploadFileInputStream = null;
FileOutputStream uploadFileOutputStream = null;
try {
uploadFileInputStream = uploadFile.getInputStream();
uploadFileOutputStream = new FileOutputStream(new File(uploadFolder + "/" + uploadFileName));
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
for (byte[] buf = new byte[INPUT_STREAM_SIZE]; uploadFileInputStream.read(buf) > -1; ) {
uploadFileOutputStream.write(buf, 0, buf.length);
messageDigest.update(buf, 0, buf.length);
}
uploadFileOutputStream.flush();
// 计算文件的MD5
byte[] data = messageDigest.digest();
StringBuilder fileMd5SB = new StringBuilder(data.length * 2);
for (byte b : data) {
fileMd5SB.append(HEX_CODE[(b >> 4) & 0xF]);
fileMd5SB.append(HEX_CODE[(b & 0xF)]);
}
fileMd5 = fileMd5SB.toString();
} catch (Exception e) {
LOG.error(e.getMessage(), e);
throw new FileException("文件上传失败");
} finally {
try {
if (null != uploadFileOutputStream) {
uploadFileOutputStream.close();
}
if (null != uploadFileInputStream) {
uploadFileInputStream.close();
}
} catch (Exception e1) {
LOG.error(e1.getMessage());
throw new FileException("文件上传失败");
}
}
return fileMd5;
}
/**
* 是否是图片文件, GIF图片不压缩
*
* @param fileType
* @return
*/
private boolean isImageFile(String fileType) {
if (StringUtils.equalsIgnoreCase("gif", fileType)) {
return false;
}
String imageTypes = fileProperties.getImageTypes();
for (String imageType : imageTypes.split(",")) {
if (StringUtils.equalsIgnoreCase(fileType, imageType)) {
return true;
}
}
return false;
}
/**
* 压缩图片
*
* @param fileFullPath
*/
private void compressImage(String fileFullPath) {
if (fileFullPath.endsWith(".blob")) {
return;
}
try {
Thumbnails.of(fileFullPath).scale(1.0f).outputQuality(fileProperties.getImageOutputQuality()).toFile(fileFullPath);
} catch (IOException e) {
LOG.error(e.getMessage(), e);
}
}
}

View File

@ -0,0 +1,271 @@
package ink.wgink.module.file.service.impl;
import ink.wgink.common.base.DefaultBaseService;
import ink.wgink.exceptions.FileException;
import ink.wgink.exceptions.base.SystemException;
import ink.wgink.module.file.dao.IFileDao;
import ink.wgink.module.file.enums.UploadTypeEnum;
import ink.wgink.module.file.service.IFileService;
import ink.wgink.module.file.service.IMinIoFileService;
import ink.wgink.pojo.pos.FilePO;
import ink.wgink.properties.FileMinIoProperties;
import ink.wgink.properties.FileProperties;
import ink.wgink.util.UUIDUtil;
import ink.wgink.util.request.StaticResourceRequestUtil;
import io.minio.MinioClient;
import io.minio.PutObjectOptions;
import io.minio.errors.*;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
/**
* @ClassName: MinIOServiceImpl
* @Description: minIO
* @Author: wanggeng
* @Date: 2021/10/17 10:16 上午
* @Version: 1.0
*/
@Service
public class MinIoFileServiceImpl extends DefaultBaseService implements IMinIoFileService {
@Autowired
private FileProperties fileProperties;
@Autowired
private IFileDao fileDao;
private FileMinIoProperties minIoProperties;
private MinioClient minioClient;
@PostConstruct
public void init() throws InvalidPortException, InvalidEndpointException {
minIoProperties = fileProperties.getMinIo();
minioClient = new MinioClient(minIoProperties.getEndpoint(), minIoProperties.getAccessKey(), minIoProperties.getSecretKey(), minIoProperties.getSecure());
}
@Override
public void deleteFile(List<FilePO> filePOs) {
}
@Override
public void uploadFile(String token, MultipartFile uploadFile, UploadTypeEnum uploadTypeEnum, Map<String, Object> params) {
String fileId = UUIDUtil.getUUID();
params.put("fileId", fileId);
String fileName = uploadFile.getOriginalFilename();
// 文件大小
long fileSize = uploadFile.getSize();
// 文件类型
String fileType = getFileType(fileName);
String uploadFileName = fileId + "." + fileType;
String bucketName = getBucketName(uploadTypeEnum);
// 上传文件
upload(uploadFileName, fileSize, bucketName, uploadFile);
saveFile(token, fileId, fileName, fileType, fileSize, bucketName);
}
@Override
public void downLoadFile(HttpServletRequest request, HttpServletResponse response, FilePO filePO, boolean isOpen, boolean canRange) {
// minIo中文件名
String objectName = filePO.getFileId() + "." + filePO.getFileType();
InputStream inputStream = null;
try (OutputStream outputStream = response.getOutputStream();) {
response.setHeader("Content-Length", filePO.getFileSize());
response.setContentType(StaticResourceRequestUtil.getContentType(filePO.getFileType()));
if (!isOpen) {
// 下载
response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
inputStream = minioClient.getObject(filePO.getFileUrl(), objectName);
} else {
// 直接打开
response.setHeader("Content-Disposition", "inline;fileName=" + URLEncoder.encode(filePO.getFileName(), "UTF-8"));
// 如果是图片资源开启缓存
if (isImageFile(filePO.getFileType())) {
// 如果存在校验修改时间且未做修改返回304使用本地资源
String ifModifiedSince = request.getHeader("If-Modified-Since");
if (StringUtils.isNotBlank(ifModifiedSince) && StringUtils.equalsIgnoreCase(ifModifiedSince, filePO.getGmtModified())) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
// 缓存有效时间为7天
response.setHeader("Expires", DateTime.now().plusDays(7).toDateTime(DateTimeZone.forID("GMT")).toString());
// 一小时之内不发送新请求
response.setHeader("max-age", "3600");
response.setHeader("Last-Modified", filePO.getGmtModified());
}
String rangeString = null;
if (canRange && request != null) {
rangeString = request.getHeader("Range");
LOG.debug("range: {}", rangeString);
}
long contentLength = Long.valueOf(filePO.getFileSize());
long startRange = 0;
long endRange = contentLength - 1;
if (!StringUtils.isBlank(rangeString)) {
if (!isOpen) {
response.setContentType("multipart/byteranges");
}
String[] rangeArray = rangeString.substring(rangeString.indexOf("=") + 1).split("-");
startRange = Long.valueOf(rangeArray[0]);
if (rangeArray.length > 1) {
endRange = Long.valueOf(rangeArray[1]);
}
setRangeHeader(startRange, endRange, response, filePO.getFileId(), contentLength);
inputStream = minioClient.getObject(filePO.getFileUrl(), objectName, startRange, endRange);
} else {
inputStream = minioClient.getObject(filePO.getFileUrl(), objectName);
}
}
byte[] readBuf = new byte[IFileService.INPUT_STREAM_SIZE];
for (int length = 0; (length = inputStream.read(readBuf)) > 0; ) {
outputStream.write(readBuf, 0, length);
}
outputStream.flush();
inputStream.close();
} catch (IOException | ErrorResponseException | InsufficientDataException | InternalException | InvalidBucketNameException | InvalidKeyException | InvalidResponseException | NoSuchAlgorithmException | XmlParserException e) {
throw new FileException("文件输出异常", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 上传文件到minIo
*
* @param uploadFileName
* @param fileSize
* @param bucketName
* @param uploadFile
*/
private void upload(String uploadFileName, long fileSize, String bucketName, MultipartFile uploadFile) {
try (InputStream inputStream = uploadFile.getInputStream()) {
// 创建桶
if (!minioClient.bucketExists(bucketName)) {
minioClient.makeBucket(bucketName);
}
PutObjectOptions putObjectOptions = new PutObjectOptions(fileSize, PutObjectOptions.MAX_PART_SIZE);
minioClient.putObject(bucketName, uploadFileName, inputStream, putObjectOptions);
} catch (MinioException | InvalidKeyException | IOException | NoSuchAlgorithmException e) {
throw new SystemException(e);
}
}
/**
* 保存文件
*
* @param token
* @param fileId
* @param fileName
* @param fileType
* @param fileSize
* @param bucketName 桶名称
*/
private void saveFile(String token, String fileId, String fileName, String fileType, long fileSize, String bucketName) {
Map<String, Object> params = getHashMap(2);
params.put("fileId", fileId);
params.put("fileName", fileName);
params.put("filePath", null);
params.put("fileUrl", bucketName);
params.put("fileType", fileType);
params.put("fileSize", fileSize);
params.put("fileSummary", "minio");
params.put("isBack", 1);
if (StringUtils.isBlank(token)) {
setSaveInfo(params);
} else {
setAppSaveInfo(token, params);
}
fileDao.save(params);
}
/**
* 获取文件类型
*
* @param fileName
* @return
*/
private String getFileType(String fileName) {
String[] names = fileName.split("\\.");
if (names != null) {
return names[names.length - 1].toLowerCase();
}
return "";
}
/**
* 获取桶文件名
*
* @param uploadTypeEnum 上传类型
* @return
*/
private String getBucketName(UploadTypeEnum uploadTypeEnum) {
if (uploadTypeEnum.getValue() == UploadTypeEnum.IMAGE.getValue()) {
return "images";
} else if (uploadTypeEnum.getValue() == UploadTypeEnum.VIDEO.getValue()) {
return "videos";
} else if (uploadTypeEnum.getValue() == UploadTypeEnum.AUDIO.getValue()) {
return "audios";
}
return "files";
}
/**
* 是否是图片
*
* @param fileType
* @return
*/
private boolean isImageFile(String fileType) {
if (StringUtils.equalsIgnoreCase("gif", fileType)) {
return false;
}
String imageTypes = fileProperties.getImageTypes();
for (String imageType : imageTypes.split(",")) {
if (StringUtils.equalsIgnoreCase(fileType, imageType)) {
return true;
}
}
return false;
}
/**
* 处理视频流问题
*
* @param startRange
* @param endRange
* @param response
* @param fileId
* @param contentLength
*/
private void setRangeHeader(long startRange, long endRange, HttpServletResponse response, String fileId, long contentLength) {
// 这里不设置会出现第一次加载很慢的情况
response.setHeader("Content-Length", String.valueOf(endRange - startRange + 1));
response.setHeader("Content-Range", String.format("bytes %d-%d/%d", startRange, endRange, contentLength));
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Etag", fileId);
response.setStatus(206);
}
}

View File

@ -277,4 +277,33 @@
file_summary = #{_parameter}
</select>
<!-- 文件列表 -->
<select id="listPO" parameterType="map" resultMap="filePO" useCache="true">
SELECT
file_id,
file_name,
file_path,
file_url,
file_type,
file_size,
file_summary,
is_back,
creator,
gmt_create,
modifier,
gmt_modified,
is_delete
FROM
sys_file
<where>
<if test="fileIds != null and fileIds.size > 0">
AND
file_id IN
<foreach collection="fileIds" index="index" open="(" separator="," close=")">
#{fileIds[${index}]}
</foreach>
</if>
</where>
</select>
</mapper>

View File

@ -0,0 +1,47 @@
import ink.wgink.util.request.StaticResourceRequestUtil;
import io.minio.MinioClient;
import io.minio.PutObjectOptions;
import io.minio.errors.MinioException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* @ClassName: MinIOTest
* @Description:
* @Author: wanggeng
* @Date: 2021/10/17 12:05 上午
* @Version: 1.0
*/
public class MinIOTest {
public static void main(String[] args) throws Exception {
try {
// 使用MinIO服务的URL端口Access key和Secret key创建一个MinioClient对象
MinioClient minioClient = new MinioClient("http://192.168.0.188:19001", "demotest", "demotest", false);
// 检查存储桶是否已经存在
boolean isExist = minioClient.bucketExists("images");
if (isExist) {
System.out.println("Bucket already exists.");
} else {
// 创建一个名为asiatrip的存储桶用于存储照片的zip文件
minioClient.makeBucket("images");
}
// 使用putObject上传一个文件到存储桶中
File file = new File("/Users/wanggeng/Desktop/UploadFiles/images/20210820/1.jpeg");
InputStream inputStream = new FileInputStream(file);
PutObjectOptions putObjectOptions = new PutObjectOptions(file.length(), PutObjectOptions.MAX_PART_SIZE);
// 下载
// putObjectOptions.setContentType("application/octet-stream");
// 直接打开
String contentType = StaticResourceRequestUtil.getContentType("jpeg");
putObjectOptions.setContentType(contentType);
minioClient.putObject("images", "demo.jpeg", inputStream, putObjectOptions);
System.out.println("successfully uploaded");
} catch (MinioException e) {
System.out.println("Error occurred: " + e);
}
}
}

View File

@ -89,6 +89,7 @@
<slf4j.version>1.7.30</slf4j.version>
<activiti.version>6.0.0</activiti.version>
<xmlgraphics.version>1.10</xmlgraphics.version>
<minio.version>7.0.2</minio.version>
</properties>
<dependencyManagement>
@ -505,6 +506,14 @@
<version>${xmlgraphics.version}</version>
</dependency>
<!-- xml graphics end -->
<!-- minio start -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<!-- minio end -->
</dependencies>
</dependencyManagement>