feat: 新增 aliyun 文件对象存储方式和代码重构优化

This commit is contained in:
haoxr
2023-06-03 11:01:50 +08:00
parent 9b8de0b35e
commit 260e7950c5
6 changed files with 219 additions and 102 deletions

View File

@@ -1,8 +1,8 @@
package com.youlai.system.controller; package com.youlai.system.controller;
import com.youlai.system.common.result.Result; import com.youlai.system.common.result.Result;
import com.youlai.system.pojo.vo.FileInfoVO; import com.youlai.system.model.dto.FileInfo;
import com.youlai.system.service.FileService; import com.youlai.system.service.OssService;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -18,15 +18,15 @@ import org.springframework.web.multipart.MultipartFile;
@RequiredArgsConstructor @RequiredArgsConstructor
public class FileController { public class FileController {
private final FileService fileService; private final OssService ossService;
@PostMapping @PostMapping
@Operation(summary = "文件上传", security = {@SecurityRequirement(name = "Authorization")}) @Operation(summary = "文件上传", security = {@SecurityRequirement(name = "Authorization")})
public Result<FileInfoVO> uploadFile( public Result<FileInfo> uploadFile(
@Parameter(description ="表单文件对象") @RequestParam(value = "file") MultipartFile file @Parameter(description ="表单文件对象") @RequestParam(value = "file") MultipartFile file
) { ) {
FileInfoVO fileInfoVO = fileService.uploadFile(file); FileInfo fileInfo = ossService.uploadFile(file);
return Result.success(fileInfoVO); return Result.success(fileInfo);
} }
@DeleteMapping @DeleteMapping
@@ -35,7 +35,7 @@ public class FileController {
public Result deleteFile( public Result deleteFile(
@Parameter(description ="文件路径") @RequestParam String filePath @Parameter(description ="文件路径") @RequestParam String filePath
) { ) {
boolean result = fileService.deleteFile(filePath); boolean result = ossService.deleteFile(filePath);
return Result.judge(result); return Result.judge(result);
} }
} }

View File

@@ -1,11 +1,11 @@
package com.youlai.system.pojo.vo; package com.youlai.system.model.dto;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@Schema(description = "文件对象") @Schema(description = "文件对象")
@Data @Data
public class FileInfoVO { public class FileInfo {
@Schema(description = "文件名称") @Schema(description = "文件名称")
private String name; private String name;

View File

@@ -1,6 +1,6 @@
package com.youlai.system.service; package com.youlai.system.service;
import com.youlai.system.pojo.vo.FileInfoVO; import com.youlai.system.model.dto.FileInfo;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
/** /**
@@ -9,16 +9,16 @@ import org.springframework.web.multipart.MultipartFile;
* 已实现 MinIO * 已实现 MinIO
* *
* @author haoxr * @author haoxr
* @date 2022/11/19 * @since 2022/11/19
*/ */
public interface FileService { public interface OssService {
/** /**
* 上传文件 * 上传文件
* @param file 表单文件对象 * @param file 表单文件对象
* @return * @return
*/ */
FileInfoVO uploadFile(MultipartFile file); FileInfo uploadFile(MultipartFile file);
/** /**
* 删除文件 * 删除文件

View File

@@ -0,0 +1,102 @@
package com.youlai.system.service.impl.oss;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.ObjectMetadata;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import com.youlai.system.model.dto.FileInfo;
import com.youlai.system.service.OssService;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
/**
* Aliyun 对象存储服务类
*
* @author haoxr
* @since 2.3.0
*/
@Component
@ConditionalOnProperty(value = "oss.type", havingValue = "aliyun")
@ConfigurationProperties(prefix = "oss.aliyun")
@RequiredArgsConstructor
@Data
public class AliyunOssService implements OssService {
/**
* 服务Endpoint
*/
private String endpoint;
/**
* 访问凭据
*/
private String accessKeyId;
/**
* 凭据密钥
*/
private String accessKeySecret;
/**
* 存储桶名称
*/
private String bucketName;
private OSS aliyunOssClient;
@PostConstruct
public void init() {
aliyunOssClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
@Override
@SneakyThrows
public FileInfo uploadFile(MultipartFile file) {
// 生成文件名(日期文件夹)
String suffix = FileUtil.getSuffix(file.getOriginalFilename());
String uuid = IdUtil.simpleUUID();
String fileName = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd") + "/" + uuid + "." + suffix;
// try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) {
// 创建PutObjectRequest对象指定Bucket名称、对象名称和输入流
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream);
// 设置上传文件的元信息例如Content-Type
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
putObjectRequest.setMetadata(metadata);
// 上传文件
PutObjectResult putObjectResult = aliyunOssClient.putObject(putObjectRequest);
// 获取文件访问路径
String fileUrl = "https://" + bucketName + ".oss-cn-hangzhou.aliyuncs.com/" + fileName;
FileInfo fileInfo = new FileInfo();
fileInfo.setName(fileName);
fileInfo.setUrl(fileUrl);
return fileInfo;
} catch (Exception e) {
throw new RuntimeException("文件上传失败");
}
}
@Override
public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空");
String tempStr = "/" + bucketName + "/";
String fileName = filePath.substring(filePath.indexOf(tempStr) + tempStr.length()); // 2022/11/20/test.jpg
aliyunOssClient.deleteObject(bucketName, fileName);
return true;
}
}

View File

@@ -1,82 +1,74 @@
package com.youlai.system.service.impl; package com.youlai.system.service.impl.oss;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.system.pojo.vo.FileInfoVO; import com.youlai.system.model.dto.FileInfo;
import com.youlai.system.service.FileService; import com.youlai.system.service.OssService;
import io.minio.*; import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method; import io.minio.http.Method;
import lombok.Setter; import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* MinIO 文件实现类
*
* @author haoxr * @author haoxr
* @date 2022/12/17 * @since 2023/6/2
*/ */
@Component @Component
@ConfigurationProperties(prefix = "minio") @ConditionalOnProperty(value = "oss.type", havingValue = "minio")
@Slf4j @ConfigurationProperties(prefix = "oss.minio")
public class MinioServiceImpl implements FileService, InitializingBean { @RequiredArgsConstructor
@Data
public class MinioOssService implements OssService {
/** /**
* MinIO的API地址 * 服务Endpoint
*/ */
@Setter
private String endpoint; private String endpoint;
/** /**
* 用户名 * 访问凭据
*/ */
@Setter
private String accessKey; private String accessKey;
/** /**
* 密钥 * 凭据密钥
*/ */
@Setter
private String secretKey; private String secretKey;
/** /**
* 存储桶名称 * 存储桶名称
*/ */
@Setter
private String bucketName; private String bucketName;
/** /**
* 自定义域名(非必须) * 自定义域名
*/ */
@Setter
private String customDomain; private String customDomain;
private MinioClient minioClient; private MinioClient minioClient;
@Override // 依赖注入完成之后执行初始化
public void afterPropertiesSet() { @PostConstruct
log.info("MinIO Client init..."); public void init() {
Assert.notBlank(endpoint, "MinIO endpoint can not be null"); minioClient = MinioClient.builder()
Assert.notBlank(accessKey, "MinIO accessKey can not be null");
Assert.notBlank(secretKey, "MinIO secretKey can not be null");
Assert.notBlank(bucketName, "MinIO bucketName can not be null");
this.minioClient = MinioClient.builder()
.endpoint(endpoint) .endpoint(endpoint)
.credentials(accessKey, secretKey) .credentials(accessKey, secretKey)
.build(); .build();
} }
/** /**
* 上传文件 * 上传文件
* *
@@ -84,45 +76,44 @@ public class MinioServiceImpl implements FileService, InitializingBean {
* @return * @return
*/ */
@Override @Override
@SneakyThrows public FileInfo uploadFile(MultipartFile file) {
public FileInfoVO uploadFile(MultipartFile file) {
// 存储桶不存在则创建
createBucketIfAbsent(bucketName);
// 生成文件名(日期文件夹) // 生成文件名(日期文件夹)
String suffix = FileUtil.getSuffix(file.getOriginalFilename()); String suffix = FileUtil.getSuffix(file.getOriginalFilename());
String uuid = IdUtil.simpleUUID(); String uuid = IdUtil.simpleUUID();
String fileName = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd") + "/" + uuid + "." + suffix; String fileName = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd") + "/" + uuid + "." + suffix;
// try-with-resource 语法糖自动释放流
InputStream inputStream = file.getInputStream(); try (InputStream inputStream = file.getInputStream()) {
// 文件上传
// 文件上传 PutObjectArgs putObjectArgs = PutObjectArgs.builder()
PutObjectArgs putObjectArgs = PutObjectArgs.builder() .bucket(bucketName)
.bucket(bucketName) .object(fileName)
.object(fileName) .contentType(file.getContentType())
.contentType(file.getContentType()) .stream(inputStream, inputStream.available(), -1)
.stream(inputStream, inputStream.available(), -1)
.build();
minioClient.putObject(putObjectArgs);
// 返回文件路径
String fileUrl;
if (StrUtil.isBlank(customDomain)) { // 未配置自定义域名
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName).object(fileName)
.method(Method.GET)
.build(); .build();
minioClient.putObject(putObjectArgs);
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs); // 返回文件路径
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?")); String fileUrl;
} else { // 配置自定义文件路径域名 if (StrUtil.isBlank(customDomain)) { // 配置自定义域名
fileUrl = customDomain + '/' + bucketName + "/" + fileName; GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName).object(fileName)
.method(Method.GET)
.build();
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?"));
} else { // 配置自定义文件路径域名
fileUrl = customDomain + '/' + bucketName + "/" + fileName;
}
FileInfo fileInfo = new FileInfo();
fileInfo.setName(fileName);
fileInfo.setUrl(fileUrl);
return fileInfo;
} catch (Exception e) {
throw new RuntimeException("文件上传失败");
} }
FileInfoVO fileInfoVO = new FileInfoVO();
fileInfoVO.setName(fileName);
fileInfoVO.setUrl(fileUrl);
return fileInfoVO;
} }
@@ -134,7 +125,6 @@ public class MinioServiceImpl implements FileService, InitializingBean {
* @return * @return
*/ */
@Override @Override
@SneakyThrows
public boolean deleteFile(String filePath) { public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空"); Assert.notBlank(filePath, "删除文件路径不能为空");
String tempStr = "/" + bucketName + "/"; String tempStr = "/" + bucketName + "/";
@@ -144,7 +134,13 @@ public class MinioServiceImpl implements FileService, InitializingBean {
.bucket(bucketName) .bucket(bucketName)
.object(fileName) .object(fileName)
.build(); .build();
minioClient.removeObject(removeObjectArgs); try {
minioClient.removeObject(removeObjectArgs);
} catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException |
InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException |
XmlParserException e) {
throw new RuntimeException(e);
}
return true; return true;
} }
@@ -163,17 +159,15 @@ public class MinioServiceImpl implements FileService, InitializingBean {
* Resource: 指定存储桶 * Resource: 指定存储桶
* Action: 操作行为 * Action: 操作行为
*/ */
StringBuilder builder = new StringBuilder();
builder.append("{\"Version\":\"2012-10-17\"," return "{\"Version\":\"2012-10-17\","
+ "\"Statement\":[{\"Effect\":\"Allow\"," + "\"Statement\":[{\"Effect\":\"Allow\","
+ "\"Principal\":{\"AWS\":[\"*\"]}," + "\"Principal\":{\"AWS\":[\"*\"]},"
+ "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"]," + "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"],"
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]}," + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},"
+ "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]}," + "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]},"
+ "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"]," + "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"],"
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"); + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
return builder.toString();
} }
/** /**
@@ -198,5 +192,4 @@ public class MinioServiceImpl implements FileService, InitializingBean {
minioClient.setBucketPolicy(setBucketPolicyArgs); minioClient.setBucketPolicy(setBucketPolicyArgs);
} }
} }
} }

View File

@@ -30,8 +30,11 @@ mybatis-plus:
db-config: db-config:
# 主键ID类型 # 主键ID类型
id-type: none id-type: none
# 逻辑删除字段名称
logic-delete-field: deleted logic-delete-field: deleted
# 逻辑删除-删除值
logic-delete-value: 1 logic-delete-value: 1
# 逻辑删除-未删除值
logic-not-delete-value: 0 logic-not-delete-value: 0
configuration: configuration:
# 驼峰下划线转换 # 驼峰下划线转换
@@ -47,15 +50,34 @@ auth:
# token 有效期(单位:秒) # token 有效期(单位:秒)
ttl: 18000 ttl: 18000
# MinIO 分布式文件系统 oss:
minio: # OSS 类型 (目前支持aliyun、minio)
endpoint: http://localhost:9000 type: minio
access-key: minioadmin # MinIO 对象存储服务
secret-key: minioadmin minio:
# 存储桶名称 # 服务Endpoint
bucket-name: default endpoint: http://localhost:9000
# 自定义域名(非必须)Nginx配置反向代理转发文件路径 # 访问凭据
custom-domain: access-key: minioadmin
# 凭据密钥
secret-key: minioadmin
# 存储桶名称
bucket-name: default
# (可选)自定义域名如果配置了域名生成的文件URL是域名格式未配置则URL则是IP格式 (eg: https://oss.youlai.tech)
custom-domain:
# 阿里云OSS对象存储服务
aliyun:
# 服务Endpoint
endpoint: oss-cn-hangzhou.aliyuncs.com
# 访问凭据
access-key-id: your-access-key-id
# 凭据密钥
access-key-secret: your-access-key-secret
bucket-name: default
# (可选)自定义域名如果配置了域名生成的文件URL是域名格式未配置则URL则是IP格式 (eg: https://oss.youlai.tech)
custom-domain:
# springdoc配置 https://springdoc.org/properties.html # springdoc配置 https://springdoc.org/properties.html
springdoc: springdoc:
@@ -68,7 +90,6 @@ springdoc:
# 验证码配置 # 验证码配置
easy-captcha: easy-captcha:
enable: true
# 验证码类型: arithmetic-算术 # 验证码类型: arithmetic-算术
type: arithmetic type: arithmetic
# 验证码有效时间(单位:秒) # 验证码有效时间(单位:秒)
@@ -77,8 +98,6 @@ easy-captcha:
# xxl-job 定时任务配置 # xxl-job 定时任务配置
xxl: xxl:
job: job:
# xxl-job 开关
enabled: false
admin: admin:
# 多个地址使用,分割 # 多个地址使用,分割
addresses: http://127.0.0.1:8080/xxl-job-admin addresses: http://127.0.0.1:8080/xxl-job-admin
@@ -92,8 +111,11 @@ xxl:
logretentiondays: 30 logretentiondays: 30
# 系统配置 # 系统配置
system-config: system:
# 数据权限配置 config:
data-permission:
# 数据权限开关 # 数据权限开关
enabled: true data-permission-enabled: true
# 定时任务 xxl-job 开关
xxl-job-enabled: false
# WebSocket 开关
websocket-enabled: true