行业知识
spring boot集成文件上传插件(x-file-storage),同时支持本地、FTP、SFTP、阿里云 OSS、腾讯云 COS、MinIO、 Amazon S3等各种文件存储方式解决方案
在开发管理系统 ,因为系统要求 要同时支持 本地、FTP、SFTP、阿里云 OSS、腾讯云 COS、MinIO、 Amazon S3 这几种文件存储的上传方式 ,如果一一开发 肯定开发到花都谢了。
经过搜索 发现了一个好用的插件
x-file-storage
官方地址:https://x-file-storage.xuyanwu.cn/#/
废话不多说 还是直接看代码
spring 版本 啥的 就不说了
-
因为我们的文件上传的 基础参数配置是在 nacos 和数据库里 所以 采用的是动态 切换 存储方式
没有把配置参数 定义在 bootstrap.yml 配置文件中
-
官方模式的使用方式 是读取 配置文件的信息 来知道你用的哪种文件存储
我把官方的配置文件 复制过来 大家参考 一下
这些配置 我们是读取的数据库 所以代码里 没有直接从这里取
如果 配置了 这个 就不用使用动态切换了 他会默认找
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代码怎么上传到服务器?