spring boot集成文件上传插件(x-file-storage),支持本地、FTP、SFTP、 OSS、COS、MinIO、 Amazon S3等文件存储

111 阅读24分钟

以前针对 spring boot集成文件上传插件(x-file-storage),同时支持本地、FTP、SFTP、阿里云 OSS、腾讯云 COS、MinIO、 Amazon S3等各种文件存储方式 包括 大文件上传 写过两篇文章 :

参考地址: spring boot实现大文件上传【分片上传】插件(x-file-storage),同时支持本地、FTP、SFTP、阿里云 OSS、腾讯云 COS、MinIO、 Amazon S3等。 juejin.cn/post/744770…

大文件上传 参考地址:

juejin.cn/post/744957…

上面的这两篇文章 主要是 第一次使用 整理的有可能不太清楚 所以 这篇文章 把 普通文件上传和 大文件上传 整合到一个文章里。

下面有些话 是从上面两篇文章摘录过来的 省的看起来懵逼~

在开发管理系统 ,因为系统要求 要同时支持 本地、FTP、SFTP、阿里云 OSS、腾讯云 COS、MinIO、 Amazon S3 这几种文件存储的上传方式 ,如果一一开发 肯定开发到花都谢了。

经过搜索 发现了一个好用的插件

x-file-storage

官方地址:x-file-storage.xuyanwu.cn/#/

spring 版本 啥的 就不说了

1、因为我们的文件上传的 基础参数配置是在 nacos 和数据库里 所以 采用的是动态 切换 存储方式 没有把配置参数 定义在 bootstrap.yml 配置文件中

2、官方模式的使用方式 是读取 配置文件的信息 来知道你用的哪种文件存储

我把官方的配置文件 复制过来 大家参考 一下

这些配置 我们是读取的数据库 所以代码里 没有直接从这里取

如果 配置了 这个 就不用使用动态切换了 他会默认找

default-platform: local-plus-1 #默认使用的存储平台

dromara:
  x-file-storage: #文件存储配置,不使用的情况下可以不写
    default-platform: local-plus-1 #默认使用的存储平台
    thumbnail-suffix: ".min.jpg" #缩略图后缀,例如【.min.jpg】【.png】
    local: # 本地存储(不推荐使用)
      - platform: local-1 # 存储平台标识
        enable-storage: true  #启用存储
        enable-access: true #启用访问(线上请使用 Nginx 配置,效率更高)
        domain: "" # 访问域名,例如:“http://127.0.0.1:8030/test/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名
        base-path: D:/Temp/test/ # 存储地址
        path-patterns: /test/file/** # 访问路径,开启 enable-access 后,通过此路径可以访问到上传的文件
    local-plus: # 本地存储升级版
      - platform: local-plus-1 # 存储平台标识
        enable-storage: true  #启用存储
        enable-access: true #启用访问(线上请使用 Nginx 配置,效率更高)
        domain: http://127.0.0.1:8030/file/ # 访问域名,访问域名,例如:“http://127.0.0.1:8030/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名
        base-path: local-plus/ # 基础路径
        path-patterns: /file/** # 访问路径
        storage-path: D:/Temp/ # 存储路径
    huawei-obs: # 华为云 OBS ,不使用的情况下可以不写
      - platform: huawei-obs-1 # 存储平台标识
        enable-storage: false  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.obs.com/
        base-path: hy/ # 基础路径
    aliyun-oss: # 阿里云 OSS ,不使用的情况下可以不写
      - platform: aliyun-oss-1 # 存储平台标识
        enable-storage: false  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/
        base-path: hy/ # 基础路径
    qiniu-kodo: # 七牛云 kodo ,不使用的情况下可以不写
      - platform: qiniu-kodo-1 # 存储平台标识
        enable-storage: false  # 启用存储
        access-key: ??
        secret-key: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.hn-bkt.clouddn.com/
        base-path: base/ # 基础路径
    tencent-cos: # 腾讯云 COS
      - platform: tencent-cos-1 # 存储平台标识
        enable-storage: true  # 启用存储
        secret-id: ??
        secret-key: ??
        region: ?? #存仓库所在地域
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.cos.ap-nanjing.myqcloud.com/
        base-path: hy/ # 基础路径
    baidu-bos: # 百度云 BOS
      - platform: baidu-bos-1 # 存储平台标识
        enable-storage: true  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ?? # 例如 abc.fsh.bcebos.com
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.fsh.bcebos.com/abc/
        base-path: hy/ # 基础路径
    upyun-uss: # 又拍云 USS
      - platform: upyun-uss-1 # 存储平台标识
        enable-storage: true  # 启用存储
        username: ??
        password: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:http://abc.test.upcdn.net/
        base-path: hy/ # 基础路径
    minio: # MinIO,由于 MinIO SDK 支持 Amazon S3,其它兼容 Amazon S3 协议的存储平台也都可配置在这里
      - platform: minio-1 # 存储平台标识
        enable-storage: true  # 启用存储
        access-key: ??
        secret-key: ??
        end-point: ??
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:http://minio.abc.com/abc/
        base-path: hy/ # 基础路径
    amazon-s3: # Amazon S3,其它兼容 Amazon S3 协议的存储平台也都可配置在这里
      - platform: amazon-s3-1 # 存储平台标识
        enable-storage: true  # 启用存储
        access-key: ??
        secret-key: ??
        region: ?? # 与 end-point 参数至少填一个
        end-point: ?? # 与 region 参数至少填一个
        bucket-name: ??
        domain: ?? # 访问域名,注意“/”结尾,例如:https://abc.hn-bkt.clouddn.com/
        base-path: s3/ # 基础路径
    ftp: # FTP
      - platform: ftp-1 # 存储平台标识
        enable-storage: true  # 启用存储
        host: ?? # 主机,例如:192.168.1.105
        port: 21 # 端口,默认21
        user: anonymous # 用户名,默认 anonymous(匿名)
        password: "" # 密码,默认空
        domain: ?? # 访问域名,注意“/”结尾,例如:ftp://192.168.1.105/
        base-path: ftp/ # 基础路径
        storage-path: / # 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
    sftp: # SFTP
      - platform: sftp-1 # 存储平台标识
        enable-storage: true  # 启用存储
        host: ?? # 主机,例如:192.168.1.105
        port: 22 # 端口,默认22
        user: root # 用户名
        password: ?? # 密码或私钥密码
        private-key-path: ?? # 私钥路径,兼容Spring的ClassPath路径、文件路径、HTTP路径等,例如:classpath:id_rsa_2048
        domain: ?? # 访问域名,注意“/”结尾,例如:https://file.abc.com/
        base-path: sftp/ # 基础路径
        storage-path: /www/wwwroot/file.abc.com/ # 存储路径,注意“/”结尾
    webdav: # WebDAV
      - platform: webdav-1 # 存储平台标识
        enable-storage: true  # 启用存储
        server: ?? # 服务器地址,例如:http://192.168.1.105:8405/
        user: ?? # 用户名
        password: ?? # 密码
        domain: ?? # 访问域名,注意“/”结尾,例如:https://file.abc.com/
        base-path: webdav/ # 基础路径
        storage-path: / # 存储路径,上传的文件都会存储在这个路径下面,默认“/”,注意“/”结尾
    google-cloud-storage: # 谷歌云存储
      - platform: google-1 # 存储平台标识
        enable-storage: true  # 启用存储
        project-id: ?? # 项目 id
        bucket-name: ??
        credentials-path: file:/deploy/example-key.json # 授权 key json 路径,兼容Spring的ClassPath路径、文件路径、HTTP路径等
        domain: ?? # 访问域名,注意“/”结尾,例如:https://storage.googleapis.com/test-bucket/
        base-path: hy/ # 基础路径
    fastdfs:
      - platform: fastdfs-1 # 存储平台标识
        enable-storage: true  # 启用存储
        run-mod: COVER #运行模式
        tracker-server: # Tracker Server 配置
          server-addr: ?? # Tracker Server 地址(IP:PORT),多个用英文逗号隔开
          http-port: 80 # 默认:80
        extra: # 额外扩展配置
          group-name: group2 # 组名,可以为空
          http-secret-key: FastDFS1234567890 # 安全密钥,默认:FastDFS1234567890
        domain: ?? # 访问域名,注意“/”结尾,例如:https://file.abc.com/
        base-path: hy/ # 基础路径
    azure-blob:
      - platform: azure-blob-1 # 存储平台标识
        enable-storage: true  # 启用存储
        connection-string: ?? # 连接字符串,AzureBlob控制台-安全性和网络-访问秘钥-连接字符串
        end-point: ?? # 终结点 AzureBlob控制台-设置-终结点-主终结点-Blob服务
        container-name: ?? # 容器名称,类似于 s3 的 bucketName,AzureBlob控制台-数据存储-容器
        domain: ?? # 访问域名,注意“/”结尾,与 end-point 保持一致
        base-path: hy/ # 基础路径

这个 大家按需 配置即可

我直接分享我的动态切换方式 大家按需

第一步:

引入依赖

    <!--        X Spring File Storage 开始-->
        <dependency>
            <groupId>org.dromara.x-file-storage</groupId>
            <artifactId>x-file-storage-spring</artifactId>
            <version>2.1.0</version>
        </dependency>
        <!--        阿里云-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.16.1</version>
        </dependency>
        <!--        腾讯云-->
        <dependency>
            <groupId>com.qcloud</groupId>
            <artifactId>cos_api</artifactId>
            <version>5.6.137</version>
        </dependency>
         <!--        minio  发现用这个依赖请求minio存储 会报错 有可能是版本依赖的问题 -->
<!--        <dependency>-->
<!--            <groupId>io.minio</groupId>-->
<!--            <artifactId>minio</artifactId>-->
<!--            <version>8.5.2</version>-->
<!--        </dependency>-->
        <!--        Amazon S3 其它兼容 Amazon S3 协议  这个 可以兼容minio 可以用这个依赖请求 -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk-s3</artifactId>
            <version>1.12.429</version>
        </dependency>

        <!--        FTP 开始-->
        <dependency>
            <groupId>commons-net</groupId>
            <artifactId>commons-net</artifactId>
            <version>3.9.0</version>
        </dependency>
        <!--糊涂工具类扩展-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-extra</artifactId>
            <version>5.8.22</version>
        </dependency>
           <!--  Apache 的对象池   redis 也需要依赖 这个  所以这里不需要了
        如果以前引入的 两个依赖使用的版本不一致 需要调整  -->
<!--        <dependency>-->
<!--            <groupId>org.apache.commons</groupId>-->
<!--            <artifactId>commons-pool2</artifactId>-->
<!--            <version>2.11.1</version>-->
<!--        </dependency>-->
        <!--        FTP 结束-->
        <!--        X Spring File Storage 结束-->

跟上面的两篇文章不一样的方法 就从这里开始

第二步: 初始化 配置参数 @Bean

package com.testweb.testweb.files.web.config;


import org.dromara.x.file.storage.spring.SpringFileStorageProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


import java.util.Collections;


/**
 * User:Json
 * Date: 2024/8/23
 **/
@Configuration
public class FilesConfig {


    //如果 配置信息 在 数据库里 可查询数据库 放在这里

    @Bean
    public SpringFileStorageProperties fileStorageProperties(){

        //举例
//        if("本地".equals('从数据库查出来的类型')){
//            SpringFileStorageProperties.SpringLocalPlusConfig springLocalPlusConfig = new SpringFileStorageProperties.SpringLocalPlusConfig();
//            springLocalPlusConfig.setEnableStorage(true)
//                    .setEnableAccess(true).setPathPatterns(new String[]{"/file/**"})
//                    .setStoragePath("D:/Temp/")
//                    .setDomain("http://127.0.0.1:18080/file/")
//                    .setPlatform("local-plus-1");
//            return   new SpringFileStorageProperties().setDefaultPlatform("local-plus-1")
//                    .setLocalPlus(Collections.singletonList(springLocalPlusConfig));
//        }else if("oss".equals('阿里云')){
//            SpringFileStorageProperties.SpringAliyunOssConfig springAliyunOssConfig = new SpringFileStorageProperties.SpringAliyunOssConfig();
//        }
        //  SpringFileStorageProperties. 这个对象下 有很多文件上传的配置类 如果数据库支持多个 可if判断 交给springboot 管理
        // 一般一个系统默认只支持一个存储方式 所以 使用@bean
        SpringFileStorageProperties.SpringLocalPlusConfig springLocalPlusConfig = new SpringFileStorageProperties.SpringLocalPlusConfig();
        springLocalPlusConfig.setEnableStorage(true)
                .setEnableAccess(true).setPathPatterns(new String[]{"/file/**"})
                .setStoragePath("D:/Temp/")
                .setDomain("http://127.0.0.1:18080/file/")
                .setPlatform("local-plus-1");
       return   new SpringFileStorageProperties().setDefaultPlatform("local-plus-1")
                .setLocalPlus(Collections.singletonList(springLocalPlusConfig));
    }
}


这样启动的时候会加载文件存储类型:

1.png 第三步: 编写文件上传下载工具类

package com.testweb.testweb.files.web.utils;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika;
import org.dromara.x.file.storage.core.*;
import org.dromara.x.file.storage.core.platform.FileStorage;
import org.dromara.x.file.storage.core.platform.MultipartUploadSupportInfo;
import org.dromara.x.file.storage.core.upload.FilePartInfo;
import org.dromara.x.file.storage.core.upload.FilePartInfoList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;


@Slf4j
@Component
public class FilesUtils {

    @Autowired
    private FileStorageService fileStorageService;



    public File getFile() throws IOException {
        String url = "https://app.xuyanwu.cn/BadApple/video/Bad%20Apple.mp4";

        File file = new File(System.getProperty("java.io.tmpdir"), "Bad Apple.mp4");
        if (!file.exists()) {
            log.info("测试手动分片上传文件不存在,正在下载中");
            FileUtil.writeFromStream(new URL(url).openStream(), file);
            log.info("测试手动分片上传文件下载完成");
        }
        return file;
    }

    /**
     * 测试手动分片上传
     */
    public void upload() throws IOException {
        File file = getFile();

        String defaultPlatform = fileStorageService.getDefaultPlatform();
        MultipartUploadSupportInfo supportInfo = fileStorageService.isSupportMultipartUpload(defaultPlatform);

        if (!supportInfo.getIsSupport()) {
            log.info("手动分片上传文件结束,当前存储平台【{}】不支持此功能", defaultPlatform);
            return;
        }
        FileStorage fileStorage = fileStorageService.getFileStorage();

        int partSize = 5 * 1024 * 1024; // 每个分片大小 5MB
        FileInfo fileInfo = fileStorageService
                .initiateMultipartUpload()
                .setPath("test/")
                .setOriginalFilename(file.getName())
                .setSaveFilename("BadApple.mp4")
                .setSize(file.length())
//                .setObjectId("0")
//                .setObjectType("user")
//                .putAttr("user", "admin")
//                .putMetadata(Constant.Metadata.CONTENT_DISPOSITION, "attachment;filename=DownloadFileName.mp4")
//                // 又拍云 USS 比较特殊,需要传入分片大小,虽然已有默认值,但为了方便测试还是单独设置一下
//                .putMetadata(
//                        fileStorage instanceof UpyunUssFileStorage, "X-Upyun-Multi-Part-Size", String.valueOf(partSize))
//                .putMetadata("Test-Not-Support", "123456") // 测试不支持的元数据
//                .putUserMetadata("role", "666")
//                .setFileAcl(Constant.ACL.PRIVATE)
                .init();

        log.info("手动分片上传文件初始化成功:{}", fileInfo);

        try (BufferedInputStream in = FileUtil.getInputStream(file)) {
            for (int partNumber = 1; ; partNumber++) {
                byte[] bytes = IoUtil.readBytes(in, partSize); // 每个分片大小
                if (bytes == null || bytes.length == 0) break;

                int finalPartNumber = partNumber;
                FilePartInfo filePartInfo = fileStorageService
                        .uploadPart(fileInfo, partNumber, bytes, (long) bytes.length)
                        .setProgressListener(new ProgressListener() {
                            @Override
                            public void start() {
                                System.out.println("分片 " + finalPartNumber + " 上传开始");
                            }

                            @Override
                            public void progress(long progressSize, Long allSize) {
                                if (allSize == null) {
                                    System.out.println("分片 " + finalPartNumber + " 已上传 " + progressSize + " 总大小未知");
                                } else {
                                    System.out.println("分片 " + finalPartNumber + " 已上传 " + progressSize + " 总大小"
                                            + allSize + " " + (progressSize * 10000 / allSize * 0.01) + "%");
                                }
                            }

                            @Override
                            public void finish() {
                                System.out.println("分片 " + finalPartNumber + " 上传结束");
                            }
                        })
                        .setHashCalculatorMd5()
                        .setHashCalculatorSha256()
                        .upload();
                log.info("手动分片上传-分片上传成功:{}", filePartInfo);
            }
        }

        if (supportInfo.getIsSupportListParts()) {
            FilePartInfoList partList = fileStorageService.listParts(fileInfo).listParts();
            for (FilePartInfo info : partList.getList()) {
                log.info("手动分片上传-列举已上传的分片:{}", info);
            }
        } else {
            log.info("手动分片上传-列举已上传的分片:当前存储平台暂不支持此功能");
        }

        fileStorageService
                .completeMultipartUpload(fileInfo)
                //                .setPartInfoList(partList)
                .setProgressListener(new ProgressListener() {
                    @Override
                    public void start() {
                        System.out.println("文件合并开始");
                    }

                    @Override
                    public void progress(long progressSize, Long allSize) {
                        if (allSize == null) {
                            System.out.println("文件已合并 " + progressSize + " 总大小未知");
                        } else {
                            System.out.println("文件已合并 " + progressSize + " 总大小" + allSize + " "
                                    + (progressSize * 10000 / allSize * 0.01) + "%");
                        }
                    }

                    @Override
                    public void finish() {
                        System.out.println("文件合并结束");
                    }
                })
                .complete();
        log.info("手动分片上传文件完成成功:{}", fileInfo);

        //        fileStorageService.delete(fileInfo);
    }

    /**
     * 测试手动分片上传后取消
     */

    public void abort() throws IOException {
        String defaultPlatform = fileStorageService.getDefaultPlatform();
        MultipartUploadSupportInfo supportInfo = fileStorageService.isSupportMultipartUpload(defaultPlatform);
        if (!supportInfo.getIsSupportAbort()) {
            log.info("手动分片上传文件结束,当前存储平台【{}】不支持此功能", defaultPlatform);
            return;
        }

        File file = getFile();

        FileInfo fileInfo = fileStorageService
                .initiateMultipartUpload()
                .setPath("test/")
                .setSaveFilename("BadApple.mp4")
                .init();

        log.info("手动分片上传文件初始化成功:{}", fileInfo);

        try (BufferedInputStream in = FileUtil.getInputStream(file)) {
            for (int partNumber = 1; ; partNumber++) {
                byte[] bytes = IoUtil.readBytes(in, 5 * 1024 * 1024); // 每个分片大小 5MB
                if (bytes == null || bytes.length == 0) break;
                System.out.println("分片 " + partNumber + " 上传开始");
                fileStorageService
                        .uploadPart(fileInfo, partNumber, bytes, (long) bytes.length)
                        .upload();
                System.out.println("分片 " + partNumber + " 上传完成");
            }
        }

        FilePartInfoList partList = fileStorageService.listParts(fileInfo).listParts();
        for (FilePartInfo info : partList.getList()) {
            log.info("手动分片上传-列举已上传的分片:{}", info);
        }

        fileStorageService.abortMultipartUpload(fileInfo).abort();
        log.info("手动分片上传文件已取消,正在验证:{}", fileInfo);
        try {
            partList = null;
            partList = fileStorageService.listParts(fileInfo).listParts();
        } catch (Exception e) {
        }
        Assert.isNull(partList, "手动分片上传文件取消失败!");
        log.info("手动分片上传文件取消成功:{}", fileInfo);
    }

//====================================上面是在文档上复制的例子===================================================//

    public Map<String, Objects> uploadFileBig(MultipartFile file2,  int partNumber2, int countNumber,FileInfo fileInfo) {


        FilePartInfo filePartInfo = fileStorageService
                .uploadPart(fileInfo, partNumber2, file2, (long) file2.getSize())
                .setProgressListener(new ProgressListener() {
                    @Override
                    public void start() {
                        System.out.println("分片 " + partNumber2 + " 上传开始");
                    }

                    @Override
                    public void progress(long progressSize, Long allSize) {
                        if (allSize == null) {
                            System.out.println("分片 " + partNumber2 + " 已上传 " + progressSize + " 总大小未知");
                        } else {
                            System.out.println("分片 " + partNumber2 + " 已上传 " + progressSize + " 总大小"
                                    + allSize + " " + (progressSize * 10000 / allSize * 0.01) + "%");
                        }
                    }

                    @Override
                    public void finish() {
                        System.out.println("分片 " + partNumber2 + " 上传结束");

                    }
                })
                .setHashCalculatorMd5()
                .setHashCalculatorSha256()
                .upload();
        log.info("手动分片上传-分片上传成功:{}", filePartInfo);


        if (partNumber2 == countNumber) {
            fileStorageService.completeMultipartUpload(fileInfo)
                    .setProgressListener(new ProgressListener() {
                        @Override
                        public void start() {
                            System.out.println("文件合并开始");
                        }

                        @Override
                        public void progress(long progressSize, Long allSize) {
                            if (allSize == null) {
                                System.out.println("文件已合并 " + progressSize + " 总大小未知");
                            } else {
                                System.out.println("文件已合并 " + progressSize + " 总大小" + allSize + " "
                                        + (progressSize * 10000 / allSize * 0.01) + "%");
                            }
                        }

                        @Override
                        public void finish() {
                            System.out.println("文件合并结束");
                        }
                    })
                    .complete();
            log.info("手动分片上传文件完成成功:{}", fileInfo);

            Map map = JSON.parseObject(JSON.toJSONString(fileInfo), Map.class);
            return map;
        } else {
            Map map = JSON.parseObject(JSON.toJSONString(filePartInfo), Map.class);
            return map;
        }


    }

    public Map initUpload(String fileDir,String filename, long size) {
        if (StringUtils.isEmpty(fileDir)) {
            fileDir = "files";
        }
        MultipartUploadSupportInfo supportInfo = fileStorageService.isSupportMultipartUpload();
        if (!supportInfo.getIsSupport()) {
            throw new RuntimeException("此存储类型不支持分片上传!");
        }

        FileInfo fileInfo = fileStorageService
                .initiateMultipartUpload()
                .setPath(generateFilePath(fileDir))
                .setOriginalFilename(filename)
                .setSaveFilename(getFileName() + "." + getFileExtensionWithDot(Objects.requireNonNull(filename)))
                .setSize(size)
                .init();

        log.info("手动分片上传文件初始化成功:{}", fileInfo);
        Map<String,FileInfo> map=new HashMap<>();
        map.put("fileInfo",fileInfo);
        return map;
    }

    public String getFilesystemType(){
        return "";
    }

    //上传文件
    public FileInfo uploadFile(MultipartFile file, String fileDir) {
        if (StringUtils.isEmpty(fileDir)) {
            fileDir = "files";
        }
        if (file.isEmpty()) {
            throw new xxRuntimeException("请上传文件!");
        }
        // 获取文件的MIME类型
        String mimeType = getMimeType(file);

        // 检查是否允许MIME类型
        if (!isValidMimeType(mimeType,true)) {
            throw new xxRuntimeException("文件类型不合法!");
        }
        return fileStorageService
                .of(file)
                .setPlatform(getFilesystemType())
                .setPath(generateFilePath(fileDir))
                .setSaveFilename(getFileName() + "." + getFileExtensionWithDot(Objects.requireNonNull(file.getOriginalFilename())))
                .upload();
    }


    //上传图片
    public FileInfo uploadImage(MultipartFile file, String fileDir) {
        if (StringUtils.isEmpty(fileDir)) {
            fileDir = "images";
        }
        if (file.isEmpty()) {
            throw new xxRuntimeException("请上传图片!");
        }
        // 获取文件的MIME类型
        String mimeType = getMimeType(file);

        // 检查是否允许MIME类型
        if (!isValidMimeType(mimeType,false)) {
            throw new xxRuntimeException("文件类型不合法!");
        }

        // String fileName = generateFileName(fileDir) + "." + getFileExtensionWithDot(file.getName());

        return fileStorageService
                .of(file)
                .setPlatform(getFilesystemType())
                .setPath(generateFilePath(fileDir))
                .setSaveFilename(getFileName() + "." + getFileExtensionWithDot(Objects.requireNonNull(file.getOriginalFilename())))
                .upload();
    }

    //上传远程文件(服务应用内部调用,先下载再上传). 没测
    // fileUrl 远程文件网址,folder 文件目录 ,extension 没有指定上传保存扩展名,通过链接获取
    public FileInfo uploadRemoteFile(String fileUrl, String folder, String extension) {
        if (StringUtils.isEmpty(folder)) {
            folder = "remote";
        }
        try {
            ResponseEntity<String> responseEntity = HttpUtil.doGet(fileUrl, new HttpHeaders());
            if (responseEntity.getStatusCodeValue() != 200) {
                throw new xxRuntimeException(String.format("文件下载失败(错误码%s)", responseEntity.getStatusCodeValue()));
            }
            String fileStr = responseEntity.getBody();
            //没有指定上传保存扩展名,通过链接获取
            if (StringUtils.isEmpty(extension)) {
                Path path = Paths.get(fileUrl);
                String fileName = path.getFileName().toString();
                extension = getFileExtensionWithDot(fileName);
            }
            return fileStorageService
                    .of(fileStr)
                    .setPlatform(getFilesystemType())
                    .setPath(generateFilePath(folder))
                    .setSaveFilename(getFileName() + "." +extension)
                    .upload();
        } catch (Exception e) {
            throw new xxRuntimeException("文件上传出错:" + e.getMessage());
        }

    }

    /**
     * 上传本地文件(服务应用内部调用).  没测
     * file /opt/www/runtime/doc/1640071827.docx
     * folder 文件目录
     */
    public FileInfo uploadLocalFile(String filePath, String folder, boolean unlink) {  //线上开启
        if (StringUtils.isEmpty(folder)) {
            folder = "contract";
        }

        File file = new File(filePath);
        if (!file.exists()) {
            throw new xxRuntimeException("文件不存在");
        }
        Path path = Paths.get(filePath);
        String fileName = path.getFileName().toString();
        String extension = getFileExtensionWithDot(fileName);
        FileInfo upload = fileStorageService
                .of(file)
                .setPlatform(getFilesystemType())
                .setPath(generateFilePath(folder))
                .setSaveFilename(getFileName() + "." +extension)
                .upload();
        if (!ObjectUtils.isEmpty(upload) && unlink) {
            if (file.exists()) {
                if (file.delete()) {
                    // System.out.println("文件删除成功");
                } else {
                    throw new xxRuntimeException("文件删除失败!");
                }
            } else {
                //System.out.println("文件不存在,无需删除");
            }
        }
        return upload;
    }

    /**
     * 上传本地文件(服务应用内部调用).  没测
     * mixed $file doc/1640071827.docx
     * string $folder 文件目录
     */
    public FileInfo uploadLocalFilesystem(String filePath, String folder, boolean unlink) {  //线上开启
        if (StringUtils.isEmpty(folder)) {
            folder = "contract";
        }

        File file = new File(filePath);
        if (!file.exists()) {
            throw new xxRuntimeException("文件不存在");
        }
        Path path = Paths.get(filePath);
        String fileName = path.getFileName().toString();
        String extension = getFileExtensionWithDot(fileName);
        FileInfo upload = fileStorageService
                .of(file)
                .setPlatform(getFilesystemType())
                .setPath(generateFilePath(folder))
                .setSaveFilename(getFileName() + "." +extension)
                .upload();
        if (!ObjectUtils.isEmpty(upload) && unlink) {
            if (file.exists()) {
                if (file.delete()) {
                    // System.out.println("文件删除成功");
                } else {
                    throw new xxRuntimeException("文件删除失败!");
                }
            } else {
                //System.out.println("文件不存在,无需删除");
            }
        }
        return upload;
    }

    private String fileStorageHome="home";
    /**
     * 下载云文件至本地(服务应用内部调用).
     */
    public FileInfo downLoadFile(String file, String folder, String fileName) {
        if (StringUtils.isEmpty(folder)) {
            folder = "contract";
        }
        file = relativePath(file);
        if (StringUtils.isEmpty(fileName)) {
            Path path = Paths.get(file);
            String fileName1 = path.getFileName().toString();
            String extension = getFileExtensionWithDot(fileName1);
            fileName=getFileName() + "." + extension;
        }
        String localFile = generateFilePath(folder) + fileName;

        FileInfo fileInfoByUrl = fileStorageService.getFileInfoByUrl(file);
        fileStorageService.download(fileInfoByUrl).file(fileStorageHome+"/"+localFile);

        return fileInfoByUrl;
    }

    //获取文件地址
    private String relativePath(String filePath) {
        List<String> domainSec = getDomainSec();
        String result="";
        if (filePath instanceof String) {
            result = filePath.replaceAll(String.join("|", domainSec), "").replaceFirst("^/", "");
        }
        return result;
    }
    private List<String>  relativePath(List<String> filePath) {
        List<String> domainSec = getDomainSec();
        if (filePath instanceof List<?> && domainSec.stream().allMatch(s -> s instanceof String)) {
            for (int i = 0; i < filePath.size(); i++) {
                filePath.set(i, filePath.get(i).replaceAll(String.join("|", domainSec), "").replaceFirst("^/", ""));
            }
            //     System.out.println(filePath);
        }
        return filePath;
    }


    //BaseDataUtil.getSystemConfigNacos() 从nacos获取基础信息 可按需修改
    private List<String> getDomainSec() {
        List<String> domainSec = new ArrayList<>();
        if ("cos".equals(getFilesystemType()) || StringUtils.isEmpty(getFilesystemType())) {
            domainSec.add("https://" + BaseDataUtil.getSystemConfigNacos().getFilesystemQCloudBucket() + "-" + BaseDataUtil.getSystemConfigNacos().getQcloudAppId() + ".cos." + BaseDataUtil.getSystemConfigNacos().getFilesystemQCloudRegion() + ".myqcloud.com");
            if (!StringUtils.isEmpty(BaseDataUtil.getSystemConfigNacos().getFilesystemQCloudDomain())) {
                domainSec.add("https://" + BaseDataUtil.getSystemConfigNacos().getFilesystemQCloudDomain());
            }
        } else if ("minio".equals(getFilesystemType()) || "s3".equals(getFilesystemType())) {
            if (!StringUtils.isEmpty(BaseDataUtil.getSystemConfigNacos().getFilesystemS3Endpoint())) {
                domainSec.add(BaseDataUtil.getSystemConfigNacos()
                        .getFilesystemS3Endpoint().trim().replace("/", "") + "/" +
                        BaseDataUtil.getSystemConfigNacos().getFilesystemS3Bucket());
            }
            if (!StringUtils.isEmpty(BaseDataUtil.getSystemConfigNacos().getFilesystemS3Domain())) {
                domainSec.add(BaseDataUtil.getSystemConfigNacos().getFilesystemS3Domain().trim().replace("/", ""));
            }
        } else if ("oss".equals(getFilesystemType())) {
            domainSec.add("https://" + BaseDataUtil.getSystemConfigNacos().getFilesystemOssDomain());
        } else if ("ftp".equals(getFilesystemType())) {
            domainSec.add(BaseDataUtil.getSystemConfigNacos().getFilesystemFtpDomain().trim().replace("/", ""));
        }
        return domainSec;
    }


    private String getMimeType(MultipartFile file) {
        try {
            Tika tika = new Tika();
            return tika.detect(file.getInputStream());
        } catch (IOException e) {
            return "";
        }
    }


    //文件验证 isAll true 全部验证  false 只验证图片
    private boolean isValidMimeType(String mimeType,boolean isAll) {
        if(isAll){
            // 允许的MIME类型列表
            String[] allowedMimeTypes = {"image/png", "image/jpeg", "image/gif", "application/zip", "text/plain", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/pdf", "application/x-rar-compressed"};

            for (String allowedMimeType : allowedMimeTypes) {
                if (allowedMimeType.equalsIgnoreCase(mimeType)) {
                    return true;
                }
            }
            return false;

        }else{
            // 允许的MIME类型列表
            String[] allowedMimeTypes = {"image/png", "image/jpeg", "image/gif"};
            for (String allowedMimeType : allowedMimeTypes) {
                if (allowedMimeType.equalsIgnoreCase(mimeType)) {
                    return true;
                }
            }
            return false;
        }

    }

    public Downloader downLoadFile(String file) {

        file = relativePath(file);


        FileInfo fileInfoByUrl = fileStorageService.getFileInfoByUrl(file);

        return fileStorageService.download(fileInfoByUrl);


    }




    //定义文件路径
    private String generateFilePath(String fileDir) {
        // 'yyyyMMdd'
        String currentDate = new java.text.SimpleDateFormat("yyyyMMdd").format(new Date());
        //  file name
        String fileName = "upload/" + fileDir + "/" + currentDate + "/";
        return fileName;
    }

    //随机文件名
    private String getFileName() {
        //  unique ID
        String uniqueID = UUID.randomUUID().toString();

        // 10000 and 99999
        int randomNum = (int) (Math.random() * (99999 - 10000 + 1)) + 10000;
        return uniqueID + randomNum;
    }

    //获取文件后缀
    private String getFileExtensionWithDot(String fileName) {
        int dotIndex = fileName.lastIndexOf('.');
        if (dotIndex > 0 && dotIndex < fileName.length() - 1) {
            return fileName.substring(dotIndex + 1);
        }
        return "";
    }



}


大文件上传的方法也包含在上面的工具类里
对于大文件上传使用方法
前端调用方式
一个大文件上传 就初始化一次 两个大文件上传 就初始化2次
意思就是 一个文件 对应一个初始化 initUpload 一次后 就一直分片调用uploadBig 这个接口 一直到文件上传完成

2.png

3.png

4.png

代码里 像分片验证 就没写了 如果有需要 可以去看源码 例子里有代码。

     Assert.isTrue(SecureUtil.md5().digestHex(bytes).equals(hashInfo.getMd5()), "分片 MD5 对比不一致!");
                log.info("分片 MD5 对比通过");
                Assert.isTrue(SecureUtil.sha256().digestHex(bytes).equals(hashInfo.getSha256()), "分片 SHA256 对比不一致!");

下面代码 是 这个插件 的 默认的增删改查 因为上传了 他需要保存 数据 下载的时候要取数据

还包含 分片上传的表

所以下面的代码也需要 复制到 项目里 这是这个插件带的

这两个表 按需可以修改 我这边就直接用官方提供的表

对数据库的操作 就是 这里使用了 MyBatis-Plus 和 Hutool 工具类

对应的官方这里

5.png 两个数据表

-- 这里使用的是 mysql
CREATE TABLE `file_detail`
(
    `id`                varchar(32)  NOT NULL COMMENT '文件id',
    `url`               varchar(512) NOT NULL COMMENT '文件访问地址',
    `size`              bigint(20)   DEFAULT NULL COMMENT '文件大小,单位字节',
    `filename`          varchar(256) DEFAULT NULL COMMENT '文件名称',
    `original_filename` varchar(256) DEFAULT NULL COMMENT '原始文件名',
    `base_path`         varchar(256) DEFAULT NULL COMMENT '基础存储路径',
    `path`              varchar(256) DEFAULT NULL COMMENT '存储路径',
    `ext`               varchar(32)  DEFAULT NULL COMMENT '文件扩展名',
    `content_type`      varchar(128) DEFAULT NULL COMMENT 'MIME类型',
    `platform`          varchar(32)  DEFAULT NULL COMMENT '存储平台',
    `th_url`            varchar(512) DEFAULT NULL COMMENT '缩略图访问路径',
    `th_filename`       varchar(256) DEFAULT NULL COMMENT '缩略图名称',
    `th_size`           bigint(20)   DEFAULT NULL COMMENT '缩略图大小,单位字节',
    `th_content_type`   varchar(128) DEFAULT NULL COMMENT '缩略图MIME类型',
    `object_id`         varchar(32)  DEFAULT NULL COMMENT '文件所属对象id',
    `object_type`       varchar(32)  DEFAULT NULL COMMENT '文件所属对象类型,例如用户头像,评价图片',
    `metadata`          text COMMENT '文件元数据',
    `user_metadata`     text COMMENT '文件用户元数据',
    `th_metadata`       text COMMENT '缩略图元数据',
    `th_user_metadata`  text COMMENT '缩略图用户元数据',
    `attr`              text COMMENT '附加属性',
    `file_acl`          varchar(32)  DEFAULT NULL COMMENT '文件ACL',
    `th_file_acl`       varchar(32)  DEFAULT NULL COMMENT '缩略图文件ACL',
    `hash_info`         text COMMENT '哈希信息',
    `upload_id`         varchar(128) DEFAULT NULL COMMENT '上传ID,仅在手动分片上传时使用',
    `upload_status`     int(11)      DEFAULT NULL COMMENT '上传状态,仅在手动分片上传时使用,1:初始化完成,2:上传完成',
    `create_time`       datetime     DEFAULT NULL COMMENT '创建时间',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARSET = utf8 ROW_FORMAT = DYNAMIC COMMENT ='文件记录表';

CREATE TABLE `file_part_detail`
(
    `id`          varchar(32) NOT NULL COMMENT '分片id',
    `platform`    varchar(32)  DEFAULT NULL COMMENT '存储平台',
    `upload_id`   varchar(128) DEFAULT NULL COMMENT '上传ID,仅在手动分片上传时使用',
    `e_tag`       varchar(255) DEFAULT NULL COMMENT '分片 ETag',
    `part_number` int(11)      DEFAULT NULL COMMENT '分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000',
    `part_size`   bigint(20)   DEFAULT NULL COMMENT '文件大小,单位字节',
    `hash_info`   text CHARACTER SET utf8 COMMENT '哈希信息',
    `create_time` datetime     DEFAULT NULL COMMENT '创建时间',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB DEFAULT CHARSET = utf8 COMMENT ='文件分片信息表,仅在手动分片上传时使用';

建两个实体类

package com.xx.api.entities.files;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xx.api.entities.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.time.LocalDateTime;

/**
 * <p>
 * 文件记录表
 * </p>
 *
 * @author json
 * @since 2024-04-15
 */
@Data
@Accessors(chain = true)
@TableName("file_detail")
@ApiModel(value="FileDetail对象", description="文件记录表")
public class FileDetail{


    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

    @ApiModelProperty(value = "文件访问地址")
    @TableField("url")
    private String url;

    @ApiModelProperty(value = "文件大小,单位字节")
    @TableField("size")
    private Long size;

    @ApiModelProperty(value = "文件名称")
    @TableField("filename")
    private String filename;

    @ApiModelProperty(value = "原始文件名")
    @TableField("original_filename")
    private String originalFilename;

    @ApiModelProperty(value = "基础存储路径")
    @TableField("base_path")
    private String basePath;

    @ApiModelProperty(value = "存储路径")
    @TableField("path")
    private String path;

    @ApiModelProperty(value = "文件扩展名")
    @TableField("ext")
    private String ext;

    @ApiModelProperty(value = "MIME类型")
    @TableField("content_type")
    private String contentType;

    @ApiModelProperty(value = "存储平台")
    @TableField("platform")
    private String platform;

    @ApiModelProperty(value = "缩略图访问路径")
    @TableField("th_url")
    private String thUrl;

    @ApiModelProperty(value = "缩略图名称")
    @TableField("th_filename")
    private String thFilename;

    @ApiModelProperty(value = "缩略图大小,单位字节")
    @TableField("th_size")
    private Long thSize;

    @ApiModelProperty(value = "缩略图MIME类型")
    @TableField("th_content_type")
    private String thContentType;

    @ApiModelProperty(value = "文件所属对象id")
    @TableField("object_id")
    private String objectId;

    @ApiModelProperty(value = "文件所属对象类型,例如用户头像,评价图片")
    @TableField("object_type")
    private String objectType;

    @ApiModelProperty(value = "文件元数据")
    @TableField("metadata")
    private String metadata;

    @ApiModelProperty(value = "文件用户元数据")
    @TableField("user_metadata")
    private String userMetadata;

    @ApiModelProperty(value = "缩略图元数据")
    @TableField("th_metadata")
    private String thMetadata;

    @ApiModelProperty(value = "缩略图用户元数据")
    @TableField("th_user_metadata")
    private String thUserMetadata;

    @ApiModelProperty(value = "附加属性")
    @TableField("attr")
    private String attr;

    @ApiModelProperty(value = "文件ACL")
    @TableField("file_acl")
    private String fileAcl;

    @ApiModelProperty(value = "缩略图文件ACL")
    @TableField("th_file_acl")
    private String thFileAcl;

    @ApiModelProperty(value = "哈希信息")
    @TableField("hash_info")
    private String hashInfo;

    @ApiModelProperty(value = "上传ID,仅在手动分片上传时使用")
    @TableField("upload_id")
    private String uploadId;

    @ApiModelProperty(value = "上传状态,仅在手动分片上传时使用,1:初始化完成,2:上传完成")
    @TableField("upload_status")
    private Integer uploadStatus;

    @ApiModelProperty(value = "创建时间")
    @TableField("create_time")
    private LocalDateTime createTime;


    public static final String COL_ID = "id";

    public static final String COL_URL = "url";

    public static final String COL_SIZE = "size";

    public static final String COL_FILENAME = "filename";

    public static final String COL_ORIGINAL_FILENAME = "original_filename";

    public static final String COL_BASE_PATH = "base_path";

    public static final String COL_PATH = "path";

    public static final String COL_EXT = "ext";

    public static final String COL_CONTENT_TYPE = "content_type";

    public static final String COL_PLATFORM = "platform";

    public static final String COL_TH_URL = "th_url";

    public static final String COL_TH_FILENAME = "th_filename";

    public static final String COL_TH_SIZE = "th_size";

    public static final String COL_TH_CONTENT_TYPE = "th_content_type";

    public static final String COL_OBJECT_ID = "object_id";

    public static final String COL_OBJECT_TYPE = "object_type";

    public static final String COL_METADATA = "metadata";

    public static final String COL_USER_METADATA = "user_metadata";

    public static final String COL_TH_METADATA = "th_metadata";

    public static final String COL_TH_USER_METADATA = "th_user_metadata";

    public static final String COL_ATTR = "attr";

    public static final String COL_HASH_INFO = "hash_info";

    public static final String COL_UPLOAD_ID = "upload_id";

    public static final String COL_UPLOAD_STATUS = "upload_status";

    public static final String COL_CREATE_TIME = "create_time";


}

package com.xx.api.entities.files;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.xx.api.entities.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.time.LocalDateTime;
import java.util.Date;

/**
 * <p>
 * 文件分片信息表,仅在手动分片上传时使用
 * </p>
 *
 * @author json
 * @since 2024-04-15
 */
@Data
@Accessors(chain = true)
@TableName("file_part_detail")
@ApiModel(value="FilePartDetail对象", description="文件分片信息表,仅在手动分片上传时使用")
public class FilePartDetail  {

    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

    @ApiModelProperty(value = "存储平台")
    @TableField("platform")
    private String platform;

    @ApiModelProperty(value = "上传ID,仅在手动分片上传时使用")
    @TableField("upload_id")
    private String uploadId;

    @ApiModelProperty(value = "分片 ETag")
    @TableField("e_tag")
    private String eTag;

    @ApiModelProperty(value = "分片号。每一个上传的分片都有一个分片号,一般情况下取值范围是1~10000")
    @TableField("part_number")
    private Integer partNumber;

    @ApiModelProperty(value = "文件大小,单位字节")
    @TableField("part_size")
    private Long partSize;

    @ApiModelProperty(value = "哈希信息")
    @TableField("hash_info")
    private String hashInfo;

    @ApiModelProperty(value = "创建时间")
    @TableField("create_time")
    private Date createTime;

    public static final String COL_ID = "id";

    public static final String COL_PLATFORM = "platform";

    public static final String COL_UPLOAD_ID = "upload_id";

    public static final String COL_E_TAG = "e_tag";

    public static final String COL_PART_NUMBER = "part_number";

    public static final String COL_PART_SIZE = "part_size";

    public static final String COL_HASH_INFO = "hash_info";

    public static final String COL_CREATE_TIME = "create_time";

}

接口层 两个接口

public interface IFileDetailService extends IService<FileDetail> {

}
public interface IFilePartDetailService extends IService<FilePartDetail> {

}

Mapper 层 也是两个接口

@Mapper
public interface FileDetailMapper extends BaseMapper<FileDetail> {

}
@Mapper
public interface FilePartDetailMapper extends BaseMapper<FilePartDetail> {

}

重点是 实现层的代码 当下载 和 上传后 会自动执行这里的代码
因为 实现了 FileRecorder 这个接口,把文件信息保存到数据库中。
这个接口FileRecorder

package com.xx.init.fileStorage.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;

import com.xx.init.fileStorage.impl.FilePartDetailServiceImpl;
import com.xx.api.entities.files.FileDetail;
import com.xx.api.exception.xxRuntimeException;
import com.xx.api.inteface.skeleton.IFileDetailService;
import com.xx.init.fileStorage.mapper.FileDetailMapper;
import lombok.SneakyThrows;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.hash.HashInfo;
import org.dromara.x.file.storage.core.recorder.FileRecorder;
import org.dromara.x.file.storage.core.upload.FilePartInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

/**
 * 用来将文件上传记录保存到数据库,这里使用了 MyBatis-Plus 和 Hutool 工具类
 */
@Service
public class FileDetailServiceImpl extends ServiceImpl<FileDetailMapper, FileDetail> implements FileRecorder, IFileDetailService {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private FilePartDetailServiceImpl filePartDetailService;

    /**
     * 保存文件信息到数据库
     */
    @SneakyThrows
    @Override
    public boolean save(FileInfo info) {
        FileDetail detail = toFileDetail(info);
        boolean b = save(detail);
        if (b) {
            info.setId(detail.getId());
        }
        return b;
    }

    /**
     * 更新文件记录,可以根据文件 ID 或 URL 来更新文件记录,
     * 主要用在手动分片上传文件-完成上传,作用是更新文件信息
     */
    @SneakyThrows
    @Override
    public void update(FileInfo info) {
        FileDetail detail = toFileDetail(info);
        QueryWrapper<FileDetail> qw = new QueryWrapper<FileDetail>()
                .eq(detail.getUrl() != null, FileDetail.COL_URL, detail.getUrl())
                .eq(detail.getId() != null, FileDetail.COL_ID, detail.getId());
        update(detail, qw);
    }

    /**
     * 根据 url 查询文件信息
     */
    @SneakyThrows
    @Override
    public FileInfo getByUrl(String url) {
        FileDetail one = getOne(new QueryWrapper<FileDetail>().eq(FileDetail.COL_URL, url));
        if(ObjectUtils.isEmpty(one)){
            throw new xxRuntimeException("未查询到文件记录!下载失败!");
        }
        return toFileInfo(one);
    }

    /**
     * 根据 url 删除文件信息
     */
    @Override
    public boolean delete(String url) {
        remove(new QueryWrapper<FileDetail>().eq(FileDetail.COL_URL, url));
        return true;
    }

    /**
     * 保存文件分片信息
     * @param filePartInfo 文件分片信息
     */
    @Override
    public void saveFilePart(FilePartInfo filePartInfo) {
        filePartDetailService.saveFilePart(filePartInfo);
    }

    /**
     * 删除文件分片信息
     */
    @Override
    public void deleteFilePartByUploadId(String uploadId) {
        filePartDetailService.deleteFilePartByUploadId(uploadId);
    }

    /**
     * 将 FileInfo 转为 FileDetail
     */
    public FileDetail toFileDetail(FileInfo info) throws JsonProcessingException {
        FileDetail detail = BeanUtil.copyProperties(
                info, FileDetail.class, "metadata", "userMetadata", "thMetadata", "thUserMetadata", "attr", "hashInfo");

        // 这里手动获 元数据 并转成 json 字符串,方便存储在数据库中
        detail.setMetadata(valueToJson(info.getMetadata()));
        detail.setUserMetadata(valueToJson(info.getUserMetadata()));
        detail.setThMetadata(valueToJson(info.getThMetadata()));
        detail.setThUserMetadata(valueToJson(info.getThUserMetadata()));
        // 这里手动获 取附加属性字典 并转成 json 字符串,方便存储在数据库中
        detail.setAttr(valueToJson(info.getAttr()));
        // 这里手动获 哈希信息 并转成 json 字符串,方便存储在数据库中
        detail.setHashInfo(valueToJson(info.getHashInfo()));
        return detail;
    }

    /**
     * 将 FileDetail 转为 FileInfo
     */
    public FileInfo toFileInfo(FileDetail detail) throws JsonProcessingException {
        FileInfo info = BeanUtil.copyProperties(
                detail, FileInfo.class, "metadata", "userMetadata", "thMetadata", "thUserMetadata", "attr", "hashInfo");

        // 这里手动获取数据库中的 json 字符串 并转成 元数据,方便使用
        info.setMetadata(jsonToMetadata(detail.getMetadata()));
        info.setUserMetadata(jsonToMetadata(detail.getUserMetadata()));
        info.setThMetadata(jsonToMetadata(detail.getThMetadata()));
        info.setThUserMetadata(jsonToMetadata(detail.getThUserMetadata()));
        // 这里手动获取数据库中的 json 字符串 并转成 附加属性字典,方便使用
        info.setAttr(jsonToDict(detail.getAttr()));
        // 这里手动获取数据库中的 json 字符串 并转成 哈希信息,方便使用
        info.setHashInfo(jsonToHashInfo(detail.getHashInfo()));
        return info;
    }

    /**
     * 将指定值转换成 json 字符串
     */
    public String valueToJson(Object value) throws JsonProcessingException {
        if (value == null) return null;
        return objectMapper.writeValueAsString(value);
    }

    /**
     * 将 json 字符串转换成元数据对象
     */
    public Map<String, String> jsonToMetadata(String json) throws JsonProcessingException {
        if (StrUtil.isBlank(json)) return null;
        return objectMapper.readValue(json, new TypeReference<Map<String, String>>() {});
    }

    /**
     * 将 json 字符串转换成字典对象
     */
    public Dict jsonToDict(String json) throws JsonProcessingException {
        if (StrUtil.isBlank(json)) return null;
        return objectMapper.readValue(json, Dict.class);
    }

    /**
     * 将 json 字符串转换成哈希信息对象
     */
    public HashInfo jsonToHashInfo(String json) throws JsonProcessingException {
        if (StrUtil.isBlank(json)) return null;
        return objectMapper.readValue(json, HashInfo.class);
    }
}

package com.xx.init.fileStorage.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xx.api.entities.files.FilePartDetail;
import com.xx.api.inteface.skeleton.IFilePartDetailService;
import com.xx.init.fileStorage.mapper.FilePartDetailMapper;
import lombok.SneakyThrows;
import org.dromara.x.file.storage.core.upload.FilePartInfo;

import org.springframework.stereotype.Service;

/**
 * 用来将文件分片上传记录保存到数据库,这里使用了 MyBatis-Plus 和 Hutool 工具类
 * 目前仅手动分片分片上传时使用
 */
@Service
public class FilePartDetailServiceImpl extends ServiceImpl<FilePartDetailMapper, FilePartDetail> implements IFilePartDetailService {

    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 保存文件分片信息
     * @param info 文件分片信息
     */
    @SneakyThrows
    public void saveFilePart(FilePartInfo info) {
        FilePartDetail detail = toFilePartDetail(info);
        if (save(detail)) {
            info.setId(detail.getId());
        }
    }

    /**
     * 删除文件分片信息
     */
    public void deleteFilePartByUploadId(String uploadId) {
        remove(new QueryWrapper<FilePartDetail>().eq(FilePartDetail.COL_UPLOAD_ID, uploadId));
    }

    /**
     * 将 FilePartInfo 转成 FilePartDetail
     * @param info 文件分片信息
     */
    public FilePartDetail toFilePartDetail(FilePartInfo info) throws JsonProcessingException {
        FilePartDetail detail = new FilePartDetail();
        detail.setPlatform(info.getPlatform());
        detail.setUploadId(info.getUploadId());
        detail.setETag(info.getETag());
        detail.setPartNumber(info.getPartNumber());
        detail.setPartSize(info.getPartSize());
        detail.setHashInfo(valueToJson(info.getHashInfo()));
        detail.setCreateTime(info.getCreateTime());
        return detail;
    }

    /**
     * 将指定值转换成 json 字符串
     */
    public String valueToJson(Object value) throws JsonProcessingException {
        if (value == null) return null;
        return objectMapper.writeValueAsString(value);
    }
}

测试:

package com.testweb.testweb.files.web.controller;

import com.alibaba.fastjson.JSON;
import com.testweb.testweb.files.web.utils.FilesUtils;

import org.dromara.x.file.storage.core.Downloader;
import org.dromara.x.file.storage.core.FileInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;


import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * User:Json
 * Date: 2024/8/2
 **/
@RestController
@RequestMapping("/test")
public class TestController {

    @Autowired
    FilesUtils filesUtils;

    @GetMapping("test")
    public void test() {
        System.out.println("test");
    }



    //大文件 初始化
    @GetMapping("initUpload")
    @CrossOrigin(origins = "*")
    public Map initUpload(@RequestParam("filename") String filename,@RequestParam("size") long size){
        return filesUtils.initUpload( "dmt", filename,  size);
    }

    //大文件 上传
    @PostMapping("uploadBig")
    @CrossOrigin(origins = "*")
    public Map uploadBig(@RequestParam("file") MultipartFile file,  //分片文件
                         @RequestParam("chunk") int chunkNumber,  //分片数
                         @RequestParam("chunks") int totalChunks,  //分片总数
                         @RequestParam("fileInfo") String fileInfo
    ){
        FileInfo fileInfo2 =  JSON.parseObject(fileInfo, FileInfo.class);
        Map<String, Objects> fileInfo1 = filesUtils.uploadFileBig(file, chunkNumber,totalChunks,fileInfo2);
        Map<String, Map<String, Objects>> a=new HashMap<>();
        a.put("fileInfo",fileInfo1);
        return a;
    }


    //文件上传
    @PostMapping("index12")
    public Map index12(@RequestBody MultipartFile file){
        FileInfo fileInfo = filesUtils.uploadFile(file, "jsonTest");
        System.out.println(fileInfo);
        return new HashMap<>();
    }

    //文件下载 测试
    @PostMapping("index13")
    public Map index13(){
        FileInfo fileInfo = filesUtils.downLoadFile("/upload/jsonTest/20240415/661d14915a772807e8dd1f89.xls", "jsonTest","测试.xls");
        System.out.println(fileInfo);
        return new HashMap<>();
    }

    public void download(String fileUrl, HttpServletResponse response) {
        try {
            response.setContentType("application/force-download");// 设置强制下载不打开
            response.addHeader("Content-Disposition", "attachment;fileName=" + new String(fileUrl.getBytes("UTF-8"), "iso-8859-1"));

            Downloader  downloader=filesUtils.downLoadFile(fileUrl);
            downloader.outputStream(response.getOutputStream());
            response.flushBuffer();
        } catch (Exception e) {
            log.error("文件下载失败: " + e.getMessage());
        }
    }



}

最后启动类上 不要忘记打注解

@EnableFileStorage

如果 附件的增删改查 扫不到包 需要使用 @MapperScan 注解 指定位置

数据流 显示 比如 img 标签图片显示|

具体操作可以看官方文档

x-file-storage.xuyanwu.cn/#/%E5%9F%BA…