您当前的位置: 首页 >  nio

止步前行

暂无认证

  • 0浏览

    0关注

    247博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

使用Minio构建文件服务

止步前行 发布时间:2022-09-03 10:24:54 ,浏览量:0

文章目录
  • 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. 服务说明

1. Minio服务部署

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
2.4 大文件分片

分片文件最小为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;
关注
打赏
1657848381
查看更多评论
立即登录/注册

微信扫码登录

0.0389s