Minio安装目录:/usr/local/minio
Minio数据存储目录:/usr/local/minio/data/minio
Minio服务启动脚本:
./minio server --address :9966 --console-address :9967 /usr/local/minio/data/minio
Minio管理平台地址:http://172.40.240.162:9967/buckets,用户名和密码为:minio/minio123
每个文件都要生成一个Md5值,用于大文件分片后合并,文件对比。
/**
* 分块计算文件的md5值
* @param file 文件
* @param chunkSize 分片大小
* @returns Promise
*/
function calculateFileMd5(file, chunkSize) {
return new Promise((resolve, reject) => {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
let spark = new SparkMD5.ArrayBuffer();
let fileReader = new FileReader();
fileReader.onload = function (e) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
let md5 = spark.end();
console.log("...md5...",md5)
resolve(md5);
}
};
fileReader.onerror = function (e) {
reject(e);
};
function loadNext() {
let start = currentChunk * chunkSize;
let end = start + chunkSize;
if (end > file.size) {
end = file.size;
}
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
}
SparkMD5.js文件私下给出
test为bucket名
单文件测试页面:http://localhost:19081/portal-osp/osp-file/client/file/home/upload
大文件测试页面:http://localhost:19081/portal-osp/osp-file/client/file/home/chunk/upload
对于图片资源,自动生成一张0.25倍的缩略图,名称为:fileId_thumb.后缀名。eg:
分片文件最小为5M,Minio规定如果需要进行合并文件操作,每个分片文件最小为5M。
本着谁分片谁合并原则,前端和后台都可以进行文件合并操作,如果在服务端进行大文件合并,影响服务资源,但提供文件合并接口。
前端文件分片代码:
/**
* 执行分片上传
* @param file 上传的文件
* @param i 第几分片,从0开始
* @param md5 文件的md5值
*/
function PostFile(file, i, md5) {
resultDiv.innerHTML += '上传文件,当前分片为:' + i + '
'
let name = file.name, // 文件名
size = file.size, // 总大小
segSize = 4 * 1024 * 1024, // 以5MB为一个分片,每个分片的大小
segTotal = Math.ceil(size / segSize); //总片数
if (i >= segTotal) {
return;
}
let start = i * segSize;
let end = start + segSize;
let packet = file.slice(start, end); //将文件进行切片
/* 构建form表单进行提交 */
let form = new FormData();
form.append("md5", md5);// 前端生成uuid作为标识符传个后台每个文件都是一个uuid防止文件串了
form.append("file", packet); //slice方法用于切出文件的一部分
form.append("name", name);
form.append("fileSize", size);
form.append("segTotal", segTotal); //总片数
form.append("segCurrent", i + 1); //当前是第几片
$.ajax({
url: baseUrl + "/client/file/chunk/upload",
type: "POST",
data: form,
//timeout:"10000", //超时10秒
async: true, //异步
dataType: "json",
processData: false, //很重要,告诉jquery不要对form进行处理
contentType: false, //很重要,指定为false才能形成正确的Content-Type
success: function (msg) {
console.log(msg);
/* 表示上一块文件上传成功,继续下一次 */
if (msg.status === 20001) {
form = '';
i++;
PostFile(file, i, md5);
} else if (msg.status === 50000) {
form = '';
resultDiv.innerHTML += '请求状态码:' + msg.status + ',' + msg.message + '
'
// setInterval(function () {
// PostFile(file, i, md5)
// }, 2000);
} else if (msg.status === 20002) {
// merge(segTotal, name, md5, getFileType(file.name), file.size)
console.log("上传成功");
resultDiv.innerHTML += '请求状态码:' + msg.status + ',' + msg.message + '
'
resultDiv.innerHTML += '
---------------------------上传文件结束--------------------------------------------------------
'
} else {
console.log('未知错误');
}
}
})
}
前端文件合并代码:
document.getElementById("mergedFile").addEventListener("change", function () {
let file1 = this.files[0];
let file2 = this.files[1];
console.log('file1',file1);
console.log('file2',file2);
let arrayBlobs = [];
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
arrayBlobs.push(blobSlice.call(file1, 0, file1.fileSize));
arrayBlobs.push(blobSlice.call(file2, 0, file2.fileSize));
let fileData = new Blob(arrayBlobs);
downloadFileByBlob(fileData,"test.docx");
});
minio依赖
<dependency>
<groupId>io.miniogroupId>
<artifactId>minioartifactId>
<version>8.4.0version>
dependency>
/client/file/check/{md5}
/**
* 每个文件都有一个md5值
* 根据文件的md5校验文件是否存在
* 实现秒传接口
*
* @param md5 文件的md5
* @return 操作是否成功
*/
@GetMapping(value = "/check/{md5}")
public CommonResponse checkFileExists(@PathVariable("md5") String md5) {
if (ObjectUtils.isEmpty(md5)) {
return CommonResponse.ok(StatusCode.PARAM_ERROR_MD5.getCode())
.message(StatusCode.PARAM_ERROR_MD5.getMessage());
}
// 从数据库中查询该MD5是否存在
FileInfo fileInfo = fileInfoService.selectByMd5(md5);
// 文件不存在
if (fileInfo == null) {
return CommonResponse.ok(StatusCode.NOT_FOUND.getCode())
.message(StatusCode.NOT_FOUND.getMessage());
}
return CommonResponse.ok(StatusCode.EXIST_FILE_SUCCESS.getCode())
.message(StatusCode.EXIST_FILE_SUCCESS.getMessage())
.data("fileInfo",fileInfo);
}
/client/file/upload
/**
* 单个文件上传
* @param requestParams
* @param file
* @return
*/
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CommonResponse upload(
@RequestParam Map<String, Object> requestParams,
@RequestParam("file") MultipartFile file) {
/**
* md5: 5ec5cec2b522c7a647e1fa4e7c6a08c7
*/
FileRequest request = new JSONObject(requestParams).toJavaObject(FileRequest.class);
log.info("上传文件信息:{}", request);
if (ObjectUtils.isEmpty(request.getMd5())) {
return CommonResponse.ok(StatusCode.PARAM_ERROR_MD5.getCode())
.message(StatusCode.PARAM_ERROR_MD5.getMessage());
}
// 检查文件是否上传过
FileInfo fileInfo = fileInfoService.selectByMd5(request.getMd5());
if (fileInfo != null) {
return CommonResponse.ok(StatusCode.EXIST_FILE_SUCCESS.getCode())
.message(StatusCode.EXIST_FILE_SUCCESS.getMessage());
}
// 上传过程中出现异常,状态码设置为50000
if (file == null) {
return CommonResponse.error(StatusCode.RECEIVE_FILE_FAILURE.getCode())
.message(StatusCode.RECEIVE_FILE_FAILURE.getMessage());
}
request.setFile(file);
// 不需要分片的文件
try {
// 上传文件
FileInfo result = minoFileService.putObject(bucketName,request);
// 设置上传分片的状态
return CommonResponse.ok(StatusCode.SUCCESS.getCode())
.message(StatusCode.SUCCESS.getMessage())
.data("fileInfo",result);
} catch (Exception e) {
e.printStackTrace();
return CommonResponse.ok(StatusCode.FAILURE.getCode())
.message(StatusCode.FAILURE.getMessage());
}
}
/client/file/chunk/upload
/**
* 文件上传,适用大文件,分片上传
* 上传文件:
* 如果不需要分片,则用uuid生成文件名
* 如果需要分片且不合并,则用md5值作为文件夹名,文件夹下为分片数据
* 如果合并分片文件,则移除分片信息,并删除md5作为的文件夹名,合并成一个文件
* @param requestParams
* @param file
* @return
*/
@PostMapping(value = "/chunk/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CommonResponse chunkUpload(
@RequestParam Map<String, Object> requestParams,
@RequestParam("file") MultipartFile file) {
/**
* 分片文件上传,传的是blob,获取不到文件名
* md5: 5ec5cec2b522c7a647e1fa4e7c6a08c7
* name: 服务维护-2022-0712.docx
* fileSize: 7227540
* segTotal: 2
* segCurrent: 1
*/
FileRequest request = new JSONObject(requestParams).toJavaObject(FileRequest.class);
log.info("上传文件信息:{}", request);
if (ObjectUtils.isEmpty(request.getMd5())) {
return CommonResponse.error(StatusCode.PARAM_ERROR_MD5.getCode())
.message(StatusCode.PARAM_ERROR_MD5.getMessage());
}
// 检查文件是否上传过
FileInfo fileInfo = fileInfoService.selectByMd5(request.getMd5());
if (fileInfo != null && fileInfo.getSegCurrent().intValue() == fileInfo.getSegTotal().intValue()) {
return CommonResponse.ok(StatusCode.EXIST_FILE_SUCCESS.getCode())
.message(StatusCode.EXIST_FILE_SUCCESS.getMessage());
}
// 上传过程中出现异常,状态码设置为50000
if (file == null) {
return CommonResponse.error(StatusCode.RECEIVE_FILE_FAILURE.getCode())
.message(StatusCode.RECEIVE_FILE_FAILURE.getMessage());
}
request.setFile(file);
String fileId = fileInfo == null ? UUIDUtil.timeUuid():fileInfo.getFileId();
request.setFileId(fileId);
FileInfo result = null;
// 当不是最后一片时,上传返回的状态码为20001
if (request.getSegCurrent() < request.getSegTotal()) {
try {
// 上传文件
result = minoFileService.putChunkObject(request);
log.info("segment file upload success {}", fileInfo);
// 设置上传分片的状态
return CommonResponse.ok(StatusCode.ALONE_CHUNK_UPLOAD_SUCCESS.getCode())
.message(StatusCode.ALONE_CHUNK_UPLOAD_SUCCESS.getMessage())
.data("fileInfo",result);
} catch (Exception e) {
e.printStackTrace();
return CommonResponse.error(StatusCode.FAILURE.getCode())
.message(StatusCode.FAILURE.getMessage());
}
} else {
// 为分片文件的最后一片时状态码为20002
try {
// 上传文件
result = minoFileService.putChunkObject(request);
// 设置上传分片的状态
return CommonResponse.ok(StatusCode.ALL_CHUNK_UPLOAD_SUCCESS.getCode())
.message(StatusCode.ALL_CHUNK_UPLOAD_SUCCESS.getMessage())
.data("fileInfo",result);
} catch (Exception e) {
e.printStackTrace();
return CommonResponse.error(StatusCode.FAILURE.getCode())
.message(StatusCode.FAILURE.getMessage());
}
}
}
/client/file/merge
/**
* 文件合并
* @return 分片合并的状态
*/
@PostMapping(value = "/merge")
public CommonResponse merge(
@RequestParam Map<String, Object> requestParams) {
/**
* 参数:
* fileId:
* md5: 5ec5cec2b522c7a647e1fa4e7c6a08c7
*/
FileRequest request = new JSONObject(requestParams).toJavaObject(FileRequest.class);
FileInfo fileInfo = minoFileService.getFileInfo(request);
// 检查文件是否存在
if(fileInfo == null){
return CommonResponse.ok(StatusCode.NOT_FOUND.getCode()).message(StatusCode.NOT_FOUND.getMessage());
}
if(fileInfo.getIsMerged() > 0 || fileInfo.getSegTotal() <= 1){
// 文件不需要合并
return CommonResponse.ok(StatusCode.NOT_SEGMENT_FILE_FAILURE.getCode()).message(StatusCode.NOT_SEGMENT_FILE_FAILURE.getMessage());
}
// path : /test/5ec5cec2b522c7a647e1fa4e7c6a08c7/1
List<FileSegment> fileSegments = fileSegmentService.selectByMD5(fileInfo.getMd5());
List<String> objectNameList = new ArrayList<>();
for (FileSegment item : fileSegments) {
String fileName = item.getPath().replace(bucketName,"").substring(1);
objectNameList.add(fileName);
}
try {
// 查询片数据
if (fileInfo.getSegTotal() == fileSegments.size()) {
// 开始合并请求
String targetBucketName = bucketName;
String filenameExtension = StringUtils.getFilenameExtension(fileInfo.getName());
// 要合并成的文件名
String objectName = fileSegments.get(0).getFileId() + "." + filenameExtension;
// 返回合并后的新文件的路径
String newPath = minoFileService.composeObject(objectNameList, bucketName, fileInfo.getMd5(), targetBucketName, objectName);
log.info("桶:{} 中的分片文件,已经在桶:{},文件 {} 合并成功", fileInfo.getMd5(), targetBucketName, newPath);
// 计算文件的md5
String fileMd5 = null;
try (InputStream inputStream = minoFileService.getObject(targetBucketName, objectName)) {
fileMd5 = Md5Util.calculateMd5(inputStream);
} catch (IOException e) {
log.error("", e);
}
// 计算文件真实的类型
List<String> typeList = new ArrayList<>();
try (InputStream inputStreamCopy = minoFileService.getObject(targetBucketName, objectName)) {
typeList.addAll(FileTypeUtil.getFileRealTypeList(inputStreamCopy, fileInfo.getName(), fileInfo.getFileSize()));
} catch (IOException e) {
log.error("", e);
}
// 并和前台的md5进行对比
if (!ObjectUtils.isEmpty(fileMd5)
&& !ObjectUtils.isEmpty(typeList)
&& fileMd5.equalsIgnoreCase(fileInfo.getMd5())
&& typeList.contains(fileInfo.getFileType().toLowerCase(Locale.ENGLISH))) {
// 表示是同一个文件, 且文件后缀名没有被修改过, 合并成功之后删除对应的分块文件
minoFileService.removeObjects(bucketName,objectNameList);
log.info("删除文件 {} 成功", fileInfo.getMd5());
// 可以优化 todo
FileInfo oldFileInfo = fileInfoService.selectByMd5(fileMd5);
oldFileInfo.setPath(newPath);
oldFileInfo.setIsMerged(StatusCode.FILE_MEGERD.getCode());
fileInfo.setMtime(new Date());
// 更新数据库的文件路径及合并状态
fileInfoService.updateFileInfo(oldFileInfo);
// 删除文件分块信息
fileSegmentService.deleteFileByMD5(fileInfo.getMd5());
// 成功,返回合并后的文件
FileInfo result = fileInfoService.selectByMd5(oldFileInfo.getMd5());
return CommonResponse.ok(StatusCode.SUCCESS.getCode())
.message(StatusCode.SUCCESS.getMessage())
.data("fileInfo",result);
} else {
log.info("非法的文件信息: 分片数量:{}, 文件名称:{}, 文件fileMd5:{}, 文件真实类型:{}, 文件大小:{}",fileInfo.getSegTotal(), fileInfo.getName(), fileMd5, typeList, fileInfo.getFileSize());
// 文件md5对比失败,则要删除已经合并的文件
minoFileService.removeObject(targetBucketName, objectName);
return CommonResponse.ok(StatusCode.MERGED_FILE_FAILURE.getCode())
.message(StatusCode.MERGED_FILE_FAILURE.getMessage());
}
} else {
// 失败,文件分片数不一致
return CommonResponse.error(StatusCode.SEGMENT_COUNT_FILE_FAILURE.getCode())
.message(StatusCode.SEGMENT_COUNT_FILE_FAILURE.getMessage());
}
} catch (Exception e) {
log.error("", e);
// 失败
return CommonResponse.error(StatusCode.FAILURE.getCode())
.message(StatusCode.FAILURE.getMessage());
}
}
/client/file/delete
@PostMapping(value = "/delete")
public CommonResponse delete(
@RequestParam Map<String, Object> requestParams) {
/**
* 参数:
* md5: 5ec5cec2b522c7a647e1fa4e7c6a08c7
* fileId :
*/
FileRequest request = new JSONObject(requestParams).toJavaObject(FileRequest.class);
FileInfo fileInfo = minoFileService.getFileInfo(request);
minoFileService.removeObjectAll(bucketName,fileInfo);
return CommonResponse.ok(StatusCode.SUCCESS.getCode()).message(StatusCode.SUCCESS.getMessage());
}
/client/file/download/{fileId}
/client/file/download/{fileId}/{index}
@GetMapping(value = {"/download/{fileId}","/download/{fileId}/{index}"})
public void download(
HttpServletResponse response,
@PathVariable("fileId") String fileId,
@PathVariable(value= "index", required = false) String index,
@RequestParam(value = "offset", required = false) Long offset,
@RequestParam(value = "length", required = false) Long offLength) {
/**
* 参数:
* fileId: 文件id
* index: 分片索引
* offset: 断点下载,指定位置
* offLength: 读取指定长度
* 备注:
* path:
* 非分块文件路径:/test/46951928120b11edadf4005056b2b395.txt
* 分块文件路径: /bucketName/md5值/分片索引
*/
InputStream inputStream = null;
OutputStream outputStream = null;
try{
log.info("请求文件id为:{}",fileId);
FileInfo fileInfo = fileInfoService.selectByFileId(fileId);
String path = null;
if(fileInfo.getIsMerged() == 0){
// 分片文件下载路径
path = "/" + fileInfo.getMd5() + "/" + index;
response.setHeader("Content-Disposition", "attachment;filename="+ URLEncoder.encode(index,"UTF-8"));
}else{
path = fileInfo.getPath().replace(bucketName,"").substring(1);
response.setHeader("Content-Disposition", "attachment;filename="+ URLEncoder.encode(fileInfo.getName(),"UTF-8"));
}
log.info("请求文件id为:{},下载路径为:{}",fileId,path);
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/octet-stream");
response.setCharacterEncoding("UTF-8");
if(!ObjectUtils.isEmpty(offset) && !ObjectUtils.isEmpty(offLength)){
inputStream = minoFileService.getObject(bucketName, path, offset, offLength);
} else {
inputStream = minoFileService.getObject(bucketName, path);
}
outputStream = response.getOutputStream();
int length = 0;
byte[] buffer = new byte[1024];
while((length = inputStream.read(buffer)) != -1){
outputStream.write(buffer,0,length);
}
inputStream.close();
outputStream.flush();
outputStream.close();
}catch (Exception e){
e.printStackTrace();
}finally {
try {
if(inputStream != null){
inputStream.close();
}
if (outputStream != null){
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/client/file//info
@PostMapping(value = "/info")
public CommonResponse info(
@RequestParam Map<String, Object> requestParams) {
/** 参数:
* md5: 5ec5cec2b522c7a647e1fa4e7c6a08c7
* fileId :
*/
FileRequest request = new JSONObject(requestParams).toJavaObject(FileRequest.class);
FileInfo fileInfo = minoFileService.getFileInfo(request);
List<FileSegment> chunkInfo = null;
if(fileInfo.getIsMerged() == 0){
chunkInfo = fileSegmentService.selectByFileId(fileInfo.getFileId());
return CommonResponse.ok(StatusCode.SUCCESS.getCode())
.message(StatusCode.SUCCESS.getMessage())
.data("fileInfo",fileInfo)
.data("chunkInfo",chunkInfo);
}
return CommonResponse.ok(StatusCode.SUCCESS.getCode())
.message(StatusCode.SUCCESS.getMessage())
.data("fileInfo",fileInfo);
}
@Slf4j
@Service
public class MinioFileService {
@Value("${oss.defaultBucket:default}")
String bucketName;
@Autowired
MinioTemplate minioTemplate;
@Autowired
FileInfoService fileInfoService;
@Autowired
FileSegmentService fileSegmentService;
/**
* 查询所有Bucket
* @return
*/
@SneakyThrows
public List<BucketDTO> listBuckets(){
List<BucketDTO> result = new ArrayList<>();
BucketDTO bucketDTO = null;
List<Bucket> buckets = minioTemplate.listBuckets();
for (Bucket bucket: buckets){
bucketDTO = new BucketDTO();
bucketDTO.setName(bucket.name());
bucketDTO.setCreationDate(bucket.creationDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
result.add(bucketDTO);
}
return result;
}
/**
* 获取bucket下的所有object
* @param bucketName
* @return
*/
@SneakyThrows
public List<ObjectDTO> listObjectsFromOSS(String bucketName){
boolean found = minioTemplate.bucketExists(bucketName);
List<ObjectDTO> list = new ArrayList<>();
ObjectDTO objectDTO = null;
if(found){
// 获取bucket下的所有object
Iterable<Result<Item>> result = minioTemplate.listObjects(bucketName,true);
for(Result<Item> object: result){
objectDTO = new ObjectDTO();
Item item = object.get();
BeanUtils.copyProperties(item,objectDTO);
objectDTO.setLastModified(item.lastModified().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
objectDTO.setDir(item.isDir());
objectDTO.setOwnerId(item.owner().id());
objectDTO.setOwnerName(item.owner().displayName());
list.add(objectDTO);
}
return list;
}
log.info(bucketName + "does not exist.");
return null;
}
/**
* 查询桶中所有的对象名
* @param bucketName 桶名
* @return objectNames
*/
@SneakyThrows
public List<String> listObjectNames(String bucketName) {
List<String> objectNameList = new ArrayList<>();
if (minioTemplate.bucketExists(bucketName)) {
Iterable<Result<Item>> results = minioTemplate.listObjects(bucketName, true);
for (Result<Item> result : results) {
String objectName = result.get().objectName();
objectNameList.add(objectName);
}
}
return objectNameList;
}
/**
* 获取DB里bucket中的所有记录
* @param bucketName
* @return
*/
public List<FileInfo> listObjectsFromDB(String bucketName) {
return fileInfoService.selectByGroup(bucketName);
}
@SneakyThrows
public FileInfo putObject(String bucketName, FileRequest request) {
MultipartFile file = request.getFile();
String fileId = UUIDUtil.timeUuid();
String fileOriginalName = file.getOriginalFilename(); // cat.jpg
String fileType = fileOriginalName.substring(fileOriginalName.lastIndexOf(".")+1);
String fileName = fileId + "." +fileType;
request.setFileId(fileId);
request.setFileSize(file.getSize());
request.setName(fileOriginalName);
request.setFileExt(request.getFileExtName());
request.setFileType(fileType);
request.setGroup(bucketName);
request.setIsMerged(StatusCode.FILE_MEGERD.getCode());
request.setSegTotal(1);
request.setSegCurrent(1);
// 检查是否为为图片,为图片生成缩略图 todo
InputStream stream = null;
try {
stream = file.getInputStream();
minioTemplate.putObject(bucketName,fileName,stream,file.getContentType());
// 此处stream用完会被关闭,所有要再次获取
String fileRealMimeType = FileTypeUtil.getFileMimeType(file.getInputStream()); // image/jpeg
if(fileRealMimeType != null && fileRealMimeType.contains("image")){
// 图片
generateThumb(bucketName,fileId,fileType,file);
}
} catch (Exception e) {
e.printStackTrace();
log.info("putObject exception:{}",e.getMessage());
}finally {
stream = null;
}
// 文件路径
String path = "/" + bucketName + "/" + fileName;
request.setPath(path);
// 将文件信息存入数据库中
fileInfoService.insertOrUpdateFileInfo(request);
return request.toFileInfo();
}
/**
* 上传文件,分片
* @param request 请求信息
* @return FileInfo
*/
@SneakyThrows
public FileInfo putChunkObject(FileRequest request) {
/**
* md5: 5ec5cec2b522c7a647e1fa4e7c6a08c7
* name: 移动采录-配网服务维护-2022-0712.docx
* fileSize: 7227540
* segTotal: 2
* segCurrent: 1
*/
MultipartFile file = request.getFile();
InputStream inputStream = file.getInputStream();
try {
String fileOriginalName = request.getName(); // cat.jpg
String fileType = fileOriginalName.substring(fileOriginalName.lastIndexOf(".")+1);
// 文件服务存储的目录为:/md5值/分片段名。eg:/721fc54dbdd35fa648c4f19e00a4c0ff/1
String objectName = "/" + request.getMd5() + "/" + request.getSegCurrent(); // 存入文件服务器中的文件名称
String savePath = objectName;
request.setFileType(fileType);
request.setFileExt(request.getFileExtName());
request.setGroup(bucketName);
// 存入文件服务
minioTemplate.putChunkObject(inputStream,bucketName,objectName);
// 文件存储的完整路径要加上bucketName
String path = "/" + bucketName + savePath;
request.setPath(path);
// 将文件信息存入数据库中
fileInfoService.insertOrUpdateFileInfo(request);
// 分片信息入库
FileSegment fileSegment = new FileSegment();
fileSegment.setFileId(request.getFileId());
fileSegment.setMd5(request.getMd5());
fileSegment.setSegSize(file.getSize());
fileSegment.setPath(path);
fileSegment.setSegIndex(request.getSegCurrent());
log.info("文件分片信息为:{}",fileSegment);
fileSegmentService.insertFileSegment(fileSegment);
return fileInfoService.selectByFileId(request.getFileId());
} catch (Exception e){
e.printStackTrace();
} finally {
if (inputStream != null) {
inputStream.close();
}
}
return null;
}
/**
* 上传文件,分片
* @param request 请求信息
* @return FileInfo
*/
@SneakyThrows
public FileInfo putUnionObject(FileRequest request,FileInfo fileInfo) {
/**
* md5: 5ec5cec2b522c7a647e1fa4e7c6a08c7
* name: 移动采录-配网服务维护-2022-0712.docx
* fileSize: 7227540
* segTotal: 2
* segCurrent: 1
*/
MultipartFile file = request.getFile();
InputStream inputStream = file.getInputStream();
try {
String fileOriginalName = request.getName(); // cat.jpg
String fileType = fileOriginalName.substring(fileOriginalName.lastIndexOf("."));
// 根据md5查询数据库信息
String fileId = fileInfo == null ? UUIDUtil.timeUuid():fileInfo.getFileId();
String objectName = null; // 存入文件服务器中的文件名称
String savePath = null;
// 如果文件分片,文件存储的目录结构要改变
if(request.getSegCurrent() <= request.getSegTotal() && request.getSegTotal() > 1){
// 文件服务存储的目录为:/md5值/分片段名。eg:/721fc54dbdd35fa648c4f19e00a4c0ff/1
// 业务服务存储的是文件目录:/md5值
savePath = "/" + request.getMd5();
objectName = savePath + "/" + request.getSegCurrent();
}else {
// 如果文件不分段,则用uuid生成文件的文件名,但不包含文件后缀,需补充。eg:e287a46c0d4811eda62f005056b2b395.jpg
objectName = fileId + fileType;
savePath = objectName;
// 不分片文件需要把分片标识设为已合并状态。分片文件的合并状态需要手动调用合并接口
request.setIsMerged(StatusCode.FILE_MEGERD.getCode());
}
request.setFileId(fileId);
request.setName(fileOriginalName);
request.setFileType(fileType);
request.setFileExt(request.getFileExtName());
request.setGroup(bucketName);
// 存入文件服务
minioTemplate.putObject(bucketName,objectName,inputStream,fileType);
// 文件存储的完整路径
String path = "/" + bucketName + savePath;
request.setPath(path);
// 将文件信息存入数据库中
fileInfoService.insertOrUpdateFileInfo(request);
// 如果是分片文件,将分片文件信息存到对应分片表中
if(request.getSegTotal() > 1){
FileSegment fileSegment = new FileSegment();
BeanUtils.copyProperties(request,fileSegment);
fileSegment.setSegSize(file.getSize());
fileSegment.setPath(path + "/" +request.getSegCurrent());
fileSegment.setSegIndex(request.getSegCurrent());
log.info("文件分片信息为:{}",fileSegment);
fileSegmentService.insertFileSegment(fileSegment);
}
return fileInfoService.selectByFileId(fileId);
} finally {
if (inputStream != null) {
inputStream.close();
}
}
}
/**
* GetObject接口用于获取某个文件(Object),此操作需要对此Object具有读权限
* @param bucketName 桶名
* @param objectName 文件路径
*/
@SneakyThrows
public InputStream getObject(String bucketName, String objectName) {
return minioTemplate.getObject(bucketName,objectName);
}
@SneakyThrows
public InputStream getObject(String bucketName, String objectName, long offset, long length) {
return minioTemplate.getObject(bucketName,objectName,offset,length);
}
/**
* 文件删除
* @param bucketName
* @param fileId
*/
@SneakyThrows
public void removeObject(String bucketName, String fileId) {
FileInfo fileInfo = fileInfoService.selectByFileId(fileId);
String fileName = fileId + "." + fileInfo.getFileType();
minioTemplate.removeObject(bucketName,fileName);
fileInfoService.deleteFile(fileId);
}
/**
* 文件删除,根据Md5或者fileId都可以
* @param bucketName
* @param fileInfo
*/
@SneakyThrows
@Transactional
public void removeObjectAll(String bucketName, FileInfo fileInfo) {
// 是否删除的分片文件
if(fileInfo.getIsMerged() == 1){
String objectName = fileInfo.getPath().replace(bucketName,"").substring(1);
minioTemplate.removeObject(bucketName,objectName);
} else {
// 删除分片文件
List<FileSegment> fileSegments = fileSegmentService.selectByMD5(fileInfo.getMd5());
List<String> objectNameList = new ArrayList<>();
for (FileSegment item : fileSegments) {
String fileName = item.getPath().replace(bucketName,"").substring(1);
objectNameList.add(fileName);
}
// 表示是同一个文件, 且文件后缀名没有被修改过, 合并成功之后删除对应的分块文件
minioTemplate.removeObjects(bucketName,objectNameList);
fileSegmentService.deleteFile(fileInfo.getFileId());
}
fileInfoService.deleteFile(fileInfo.getFileId());
}
/**
* 批量文件删除
* @param bucketName
* @param objectNameList 文件名称集合
*/
@SneakyThrows
public void removeObjects(String bucketName, List<String> objectNameList) {
minioTemplate.removeObjects(bucketName,objectNameList);
}
/**
* 文件合并,将分块文件组成一个新的文件
* @param objectNameList 分片名称
* @param bucketName 分块文件所在的桶
* @param targetBucketName 合并文件生成文件所在的桶
* @param objectName 存储于桶中的对象名
* @return OssFile
*
* 注意:minio规定,每个分片文件最小是5M,要不然合并文件会报错
*/
@SneakyThrows
public String composeObject(List<String> objectNameList, String bucketName, String md5, String targetBucketName, String objectName) {
if (ObjectUtils.isEmpty(objectNameList)) {
throw new IllegalArgumentException(bucketName + "桶中没有文件,请检查");
}
List<ComposeSource> composeSourceList = new ArrayList<>(objectNameList.size());
// 在合并文件时,需要对分片的文件进行升序排序
for (String object : objectNameList) {
composeSourceList.add(ComposeSource.builder()
.bucket(bucketName)
.object(object)
.build());
}
return minioTemplate.composeObject(composeSourceList, targetBucketName, objectName);
}
public FileInfo getFileInfo(FileRequest request) {
FileInfo fileInfo = null;
if(!ObjectUtils.isEmpty(request.getMd5())){
fileInfo = fileInfoService.selectByMd5(request.getMd5());
} else if(!ObjectUtils.isEmpty(request.getFileId())){
fileInfo = fileInfoService.selectByFileId(request.getFileId());
}
return fileInfo;
}
/**
* 为图片生成缩略图
* @param bucketName
* @param fileId
* @param fileType
* @param file
*/
private void generateThumb(String bucketName, String fileId, String fileType, MultipartFile file) {
// 存放缩略图的临时目录
String tempDir = "." + File.separator + bucketName + File.separator;
// String tempDir = "e:" + File.separator + bucketName + File.separator;
// 生成文件名
String fileName = fileId + "_thumb" + "." +fileType;
// 文件完整路径
String path = tempDir + fileName;
// 创建目录
File dir = new File(tempDir);
if(!dir.exists()){
dir.setWritable(true,false);
dir.setReadable(true,false);
dir.setExecutable(true,false);
dir.mkdir();
}
try {
File toFile = new File(path);
// 生成缩略图
Thumbnails.of(file.getInputStream()).scale(0.25f).toFile(path);
minioTemplate.uploadObject(bucketName,fileName,path);
// 缩略图是否要入库 todo
} catch (IOException e) {
e.printStackTrace();
} finally {
if(dir != null){
File[] files = dir.listFiles();
for(File item : files){
item.delete();
}
}
}
}
}
配置文件
oss:
enabled: true
type: minio
endPoint: http://172.40.240.162:9966
accessKey: minio
secretKey: minio123
defaultBucket: test
数据库脚本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_osp_file_segment
-- ----------------------------
DROP TABLE IF EXISTS `t_osp_file_segment`;
CREATE TABLE `t_osp_file_segment` (
`fileId` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件ID',
`md5` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文件md5值,唯一',
`path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件存储路径',
`segSize` bigint(255) NULL DEFAULT NULL COMMENT '分片文件大小',
`segIndex` int(255) NULL DEFAULT NULL COMMENT '分片的顺序'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_osp_fileinfo
-- ----------------------------
DROP TABLE IF EXISTS `t_osp_fileinfo`;
CREATE TABLE `t_osp_fileinfo` (
`fileId` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'default' COMMENT '文件ID',
`md5` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件md5值',
`parentId` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '虚拟文件标识',
`name` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件名',
`path` varchar(1024) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'FASTDFS路径',
`fileType` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文件类型',
`fileSize` bigint(255) NULL DEFAULT NULL COMMENT '文件总大小',
`segCurrent` int(255) NULL DEFAULT NULL COMMENT '已上传的分片',
`segTotal` int(255) NULL DEFAULT NULL COMMENT '总分片数',
`isMerged` bit(1) NULL DEFAULT b'0' COMMENT '是否合并,1表示合并,0表示未合并',
`fileExt` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '扩展参数',
`mtime` timestamp(6) NOT NULL ON UPDATE CURRENT_TIMESTAMP(6) COMMENT '文件修改时间',
`groupName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT 'default' COMMENT 'FASTDFS分组',
PRIMARY KEY (`fileId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;