小工具知识网

当前位置: 首页 > 行业知识 > spring boot集成文件上传插件(x-file-storage),同时支持本地、FTP、SFTP、阿里云 OSS、腾讯云 COS、MinIO、 Amazon S3等各种文件存储方式解决方案

行业知识

spring boot集成文件上传插件(x-file-storage),同时支持本地、FTP、SFTP、阿里云 OSS、腾讯云 COS、MinIO、 Amazon S3等各种文件存储方式解决方案

2026-11-10 20:01:26 来源:互联网转载

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

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

x-file-storage

官方地址:https://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/ # 基础路径

这个 大家按需 配置即可

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

第一步:

引入依赖

                        org.dromara.x-file-storage            x-file-storage-spring            2.1.0                                    com.aliyun.oss            aliyun-sdk-oss            3.16.1                                    com.qcloud            cos_api            5.6.137                                             com.amazonaws            aws-java-sdk-s3            1.12.429                                    commons-net            commons-net            3.9.0                                    cn.hutool            hutool-extra            5.8.22                                   

第二步: 初始化 配置参数

package com.xx.init.fileStorage;import com.xx.init.utils.BaseDataUtil;import lombok.Data;import org.dromara.x.file.storage.core.FileStorageProperties;import org.dromara.x.file.storage.core.FileStorageService;import org.dromara.x.file.storage.core.FileStorageServiceBuilder;import org.dromara.x.file.storage.core.platform.FileStorage;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import java.util.Collections;import java.util.concurrent.CopyOnWriteArrayList;/** * User:Json * Date: 2024/4/12 **/@Component@Datapublic class FileStorageInit {    @Autowired    private FileStorageService fileStorageService;//注入实列    private CopyOnWriteArrayList list;    private String filesystemType;    public FileStorageService init(String filesystemType) {        this.list = this.fileStorageService.getFileStorageList();        this.filesystemType = filesystemType;        if (StringUtils.isEmpty(this.filesystemType)) {            this.filesystemType = "cos";        }        if ("cos".equals(this.filesystemType)) {            FileStorageProperties.TencentCosConfig tencentCosConfig = new FileStorageProperties.TencentCosConfig();            tencentCosConfig.setPlatform(this.filesystemType);            tencentCosConfig.setRegion(BaseDataUtil.getSystemConfigNacos().getFilesystemQCloudRegion());            tencentCosConfig.setSecretId(BaseDataUtil.getSystemConfigNacos().getQcloudSecretId());            tencentCosConfig.setSecretKey(BaseDataUtil.getSystemConfigNacos().getQcloudSecretKey());            tencentCosConfig.setBucketName(BaseDataUtil.getSystemConfigNacos().getFilesystemQCloudBucket() + "-" + BaseDataUtil.getSystemConfigNacos().getQcloudAppId());            list.addAll(FileStorageServiceBuilder.buildTencentCosFileStorage(Collections.singletonList(tencentCosConfig), null));        } else if ("minio".equals(this.filesystemType) || "s3".equals(this.filesystemType)) {            FileStorageProperties.AmazonS3Config amazonS3Config = new FileStorageProperties.AmazonS3Config();            amazonS3Config.setPlatform(this.filesystemType);//            amazonS3Config.setAccessKey("");//            amazonS3Config.setSecretKey("");//            amazonS3Config.setRegion("");//            amazonS3Config.setEndPoint("");//            amazonS3Config.setBucketName("");            amazonS3Config.setAccessKey(BaseDataUtil.getSystemConfigNacos().getFilesystemS3Key());            amazonS3Config.setSecretKey(BaseDataUtil.getSystemConfigNacos().getFilesystemS3Secret());            amazonS3Config.setRegion(BaseDataUtil.getSystemConfigNacos().getFilesystemS3Region());            amazonS3Config.setEndPoint(BaseDataUtil.getSystemConfigNacos().getFilesystemS3Endpoint());            amazonS3Config.setBucketName(BaseDataUtil.getSystemConfigNacos().getFilesystemS3Bucket());            list.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections.singletonList(amazonS3Config), null));//        } else if ("oss".equals(this.filesystemType)) {            FileStorageProperties.AliyunOssConfig ossConfig = new FileStorageProperties.AliyunOssConfig();            ossConfig.setPlatform(this.filesystemType);//            ossConfig.setAccessKey("");//            ossConfig.setSecretKey("");//            ossConfig.setBucketName("");//            ossConfig.setEndPoint("");            ossConfig.setAccessKey(BaseDataUtil.getSystemConfigNacos().getFilesystemOssAccessId());            ossConfig.setSecretKey(BaseDataUtil.getSystemConfigNacos().getFilesystemOssAccessSecret());            ossConfig.setBucketName(BaseDataUtil.getSystemConfigNacos().getFilesystemOssBucket());            ossConfig.setEndPoint(BaseDataUtil.getSystemConfigNacos().getFilesystemOssEndpoint());            list.addAll(FileStorageServiceBuilder.buildAliyunOssFileStorage(Collections.singletonList(ossConfig), null));        } else if ("ftp".equals(this.filesystemType)) {            FileStorageProperties.FtpConfig ftpConfig = new FileStorageProperties.FtpConfig();            ftpConfig.setPlatform(this.filesystemType);//            ftpConfig.setHost("192.168.237.221");//            ftpConfig.setPort(21);//            ftpConfig.setUser("");//            ftpConfig.setPassword("123456");            ftpConfig.setHost(BaseDataUtil.getSystemConfigNacos().getFilesystemFtpHost());            ftpConfig.setPort(Integer.parseInt(BaseDataUtil.getSystemConfigNacos().getFilesystemFtpPort()));            ftpConfig.setUser(BaseDataUtil.getSystemConfigNacos().getFilesystemFtpUsername());            ftpConfig.setPassword(BaseDataUtil.getSystemConfigNacos().getFilesystemFtpPassword());            //ftpConfig.setRoot(BaseDataUtil.getSystemConfigNacos().getFilesystemFtpRoot()); //这个也没有 用上了调试            //ftpConfig.setSSl(BaseDataUtil.getSystemConfigNacos().getFilesystemFtpSsl());  //这个也没有 用上了调试            list.addAll(FileStorageServiceBuilder.buildFtpFileStorage(Collections.singletonList(ftpConfig), null));        }        return this.fileStorageService;        //下面这个是 不放list 直接修改默认的  参考 脱离 SpringBoot 单独使用 文档//        FileStorageProperties properties = new FileStorageProperties();//        FileStorageProperties.TencentCosConfig tencentCosConfig1 = new FileStorageProperties.TencentCosConfig();//        tencentCosConfig1.setPlatform(this.filesystemType);//        tencentCosConfig1.setRegion(BaseDataUtil.getSystemConfigNacos().getFilesystemQCloudRegion());//        tencentCosConfig1.setSecretId(BaseDataUtil.getSystemConfigNacos().getQcloudSecretId());//        tencentCosConfig1.setSecretKey(BaseDataUtil.getSystemConfigNacos().getQcloudSecretKey());//        tencentCosConfig1.setBucketName(BaseDataUtil.getSystemConfigNacos().getFilesystemQCloudBucket()+"-"+BaseDataUtil.getSystemConfigNacos().getQcloudAppId());//        properties.setTencentCos(Collections.singletonList(tencentCosConfig1));//        properties.setDefaultPlatform(this.filesystemType);//        return  FileStorageServiceBuilder.create(properties).useDefault().build();    }}

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

package com.xx.init.utils;import com.xx.init.utils.BaseDataUtil;import com.xx.init.utils.HttpUtil;import com.xx.api.exception.xxRuntimeException;import com.xx.init.fileStorage.FileStorageInit;import org.apache.tika.Tika;import org.dromara.x.file.storage.core.FileInfo;import org.dromara.x.file.storage.core.FileStorageService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.http.HttpHeaders;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 java.io.File;import java.io.IOException;import java.nio.file.Path;import java.nio.file.Paths;import java.util.*;/** * User:Json * Date: 2024/4/12 **/@Componentpublic class UploadHelper {    @Autowired    FileStorageInit fileStorageInit;    @Value("${fileStorage.${spring.profiles.active}}")    private String fileStorageHome;    public FileStorageService init() {        return fileStorageInit.init(BaseDataUtil.getSystemConfigNacos().getFilesystemType());    }    public String getFilesystemType() {        return fileStorageInit.getFilesystemType();    }    //上传文件    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 this.init()                .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 this.init()                .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 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 this.init()                    .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 = this.init()                .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 = this.init()                .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;    }    /**     * 下载云文件至本地(服务应用内部调用).     */    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;        FileStorageService init = this.init();        FileInfo fileInfoByUrl = init.getFileInfoByUrl(file);        init.download(fileInfoByUrl).file(fileStorageHome+"/"+localFile);        return fileInfoByUrl;    }    //获取文件地址    private String relativePath(String filePath) {        List domainSec = getDomainSec();        String result="";        if (filePath instanceof String) {             result = filePath.replaceAll(String.join("|", domainSec), "").replaceFirst("^/", "");        }        return result;    }    private List  relativePath(List filePath) {        List domainSec = getDomainSec();        if (filePath instanceof List && domainSec.stream().allMatch(s -> s instanceof String)) {            for (int i = 0; i  0 && dotIndex  

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

还包含 分片上传的表

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

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

对应的官方这里

两个数据表

-- 这里使用的是 mysqlCREATE 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;/** * 

* 文件记录表 *

* * @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;/** * 

* 文件分片信息表,仅在手动分片上传时使用 *

* * @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 {}public interface IFilePartDetailService extends IService {}

Mapper 层 也是两个接口

@Mapperpublic interface FileDetailMapper extends BaseMapper {}@Mapperpublic interface FilePartDetailMapper extends BaseMapper {}

重点是 实现层的代码 当下载 和 上传后 会自动执行这里的代码

因为 实现了 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 工具类 */@Servicepublic class FileDetailServiceImpl extends ServiceImpl 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 qw = new QueryWrapper()                .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().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().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 jsonToMetadata(String json) throws JsonProcessingException {        if (StrUtil.isBlank(json)) return null;        return objectMapper.readValue(json, new TypeReference() {});    }    /**     * 将 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 工具类 * 目前仅手动分片分片上传时使用 */@Servicepublic class FilePartDetailServiceImpl extends ServiceImpl 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().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);    }}

测试:

  @PostMapping("index12")    @ApiOperation(value = "文件上传 测试")    @PassToken    public R index12(@RequestBody MultipartFile file){        FileInfo fileInfo = uploadHelper.uploadFile(file, "jsonTest");        System.out.println(fileInfo);        return R.success();    }    @PostMapping("index13")    @ApiOperation(value = "文件下载 测试")    @PassToken    public R index13(){        FileInfo fileInfo = uploadHelper.downLoadFile("/upload/jsonTest/20240415/661d14915a772807e8dd1f89.xls", "jsonTest","测试.xls");        System.out.println(fileInfo);        return R.success();    }

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

@EnableFileStorage

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

补充:如果附件不想下载到本地 也可以读取 数据流 显示 比如 img 标签图片显示 这段代码是后补充的

没有穿插上上面

具体操作可以看官方文档

https://x-file-storage.xuyanwu.cn/#/%E5%9F%BA%E7%A1%80%E5%8A%9F%E8%83%BD?id=%e4%b8%8b%e8%bd%bd

  public Downloader downLoadFile(String file) {        FileStorageService init = this.init();        file = relativePath(file);        FileInfo fileInfoByUrl = init.getFileInfoByUrl(file);         return init.download(fileInfoByUrl);    }     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=downLoadFile(fileUrl);            Downloader  downloader=filesUtil.downLoadFile(fileUrl);            downloader.outputStream(response.getOutputStream());            response.flushBuffer();        } catch (Exception e) {            log.error("文件下载失败: " + e.getMessage());        }    }

完美收工!!!

兄弟们

配置文件 :

fileStorage:

dev: D:\home

test: /home

prd: /home

大文件上传 分片上传 参考下面博客

https://blog.csdn.net/Drug_/article/details/138329505

上一篇:mqtt服务器怎么订阅消息推送?

下一篇:java代码怎么上传到服务器?