文章目录
1. Minio服务部署
- 1. Minio服务部署
- 2. 集成Minio服务说明
- 2.1 文件Md5值计算
- 2.2 上传文件命名
- 2.3 缩略图
- 2.4 大文件分片
- 3. 接口说明
- 3.1 检查文件Md5接口
- 3.2 单文件上传接口
- 3.3 大文件分片上传接口
- 3.4 分片文件合并接口
- 3.5 文件删除接口
- 3.6 单文件下载接口
- 3.7 分片文件下载接口
- 3.8 查询文件信息接口
- 4. 服务说明
Minio安装目录:/usr/local/minio
Minio数据存储目录:/usr/local/minio/data/minio
Minio服务启动脚本:
./minio server --address :9966 --console-address :9967 /usr/local/minio/data/minio
- 9966为Minio服务api端口
- 9967为Minio服务平台端口
Minio管理平台地址:http://172.40.240.162:9967/buckets,用户名和密码为:minio/minio123
2. 集成Minio服务说明 2.1 文件Md5值计算每个文件都要生成一个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 file.size) {
end = file.size;
}
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
}
SparkMD5.js文件私下给出
2.2 上传文件命名- 单文件上传,文件命名为:fileId + 文件后缀名。eg:/test/44fd7a29130511ed9fe5005056b2b395.jpg
- 大文件上传,分片文件命名:文件md5值/分片索引。eg:/test/19d66e11606ef41bd6e447156f572969/1
- 服务端大文件合并后,文件命名同单文件。
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
2.3 缩略图对于图片资源,自动生成一张0.25倍的缩略图,名称为:fileId_thumb.后缀名。eg:
- 原文件名称为:44fd7a29130511ed9fe5005056b2b395.jpg
- 缩略图名称为:44fd7a29130511ed9fe5005056b2b395_thumb.jpg
分片文件最小为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");
});
3. 接口说明
minio依赖
io.minio
minio
8.4.0
3.1 检查文件Md5接口
/client/file/check/{md5}
- GET请求
/**
* 每个文件都有一个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);
}
3.2 单文件上传接口
/client/file/upload
- POST请求
- 参数:
- md5:文件Md5值
- file:上传文件
/**
* 单个文件上传
* @param requestParams
* @param file
* @return
*/
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CommonResponse upload(
@RequestParam Map 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());
}
}
3.3 大文件分片上传接口
/client/file/chunk/upload
- POST请求
- 参数:
- md5:文件MD5值
- file:文件一部分
- name:文件名称。分片文件获取不到文件名
- fileSize:文件总大小
- segTotal:文件总分片数
- segCurrent:文件当前分片
/**
* 文件上传,适用大文件,分片上传
* 上传文件:
* 如果不需要分片,则用uuid生成文件名
* 如果需要分片且不合并,则用md5值作为文件夹名,文件夹下为分片数据
* 如果合并分片文件,则移除分片信息,并删除md5作为的文件夹名,合并成一个文件
* @param requestParams
* @param file
* @return
*/
@PostMapping(value = "/chunk/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public CommonResponse chunkUpload(
@RequestParam Map 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() 0 || fileInfo.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 fileSegments = fileSegmentService.selectByMD5(fileInfo.getMd5());
List 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 objectNameList) {
minioTemplate.removeObjects(bucketName,objectNameList);
}
/**
* 文件合并,将分块文件组成一个新的文件
* @param objectNameList 分片名称
* @param bucketName 分块文件所在的桶
* @param targetBucketName 合并文件生成文件所在的桶
* @param objectName 存储于桶中的对象名
* @return OssFile
*
* 注意:minio规定,每个分片文件最小是5M,要不然合并文件会报错
*/
@SneakyThrows
public String composeObject(List objectNameList, String bucketName, String md5, String targetBucketName, String objectName) {
if (ObjectUtils.isEmpty(objectNameList)) {
throw new IllegalArgumentException(bucketName + "桶中没有文件,请检查");
}
List 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;