diff --git a/src/main/java/com/youlai/system/controller/FileController.java b/src/main/java/com/youlai/system/controller/FileController.java new file mode 100644 index 00000000..b805a1ed --- /dev/null +++ b/src/main/java/com/youlai/system/controller/FileController.java @@ -0,0 +1,40 @@ +package com.youlai.system.controller; + +import com.youlai.system.common.result.Result; +import com.youlai.system.pojo.vo.file.FileInfo; +import com.youlai.system.service.FileService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@Api(tags = "文件接口") +@RestController +@RequestMapping("/api/v1/files") +@RequiredArgsConstructor +public class FileController { + + private final FileService fileService; + + @PostMapping + @ApiOperation(value = "文件上传") + public Result uploadFile( + @ApiParam("表单文件对象") @RequestParam(value = "file") MultipartFile file + ) { + FileInfo fileInfo = fileService.uploadFile(file); + return Result.success(fileInfo); + } + + @DeleteMapping + @ApiOperation(value = "文件删除") + @SneakyThrows + public Result deleteFile( + @ApiParam("文件路径") @RequestParam String fileName + ) { + boolean result = fileService.deleteFile(fileName); + return Result.judge(result); + } +} diff --git a/src/main/java/com/youlai/system/pojo/vo/file/FileInfo.java b/src/main/java/com/youlai/system/pojo/vo/file/FileInfo.java new file mode 100644 index 00000000..11afcf74 --- /dev/null +++ b/src/main/java/com/youlai/system/pojo/vo/file/FileInfo.java @@ -0,0 +1,12 @@ +package com.youlai.system.pojo.vo.file; + +import lombok.Data; + +@Data +public class FileInfo { + + private String name; + + private String url; + +} diff --git a/src/main/java/com/youlai/system/service/FileService.java b/src/main/java/com/youlai/system/service/FileService.java new file mode 100644 index 00000000..83551c0d --- /dev/null +++ b/src/main/java/com/youlai/system/service/FileService.java @@ -0,0 +1,32 @@ +package com.youlai.system.service; + +import com.youlai.system.pojo.vo.file.FileInfo; +import org.springframework.web.multipart.MultipartFile; + +/** + * 文件接口 + *

+ * 已实现 MinIO + * + * @author haoxr + * @date 2022/11/19 + */ +public interface FileService { + + /** + * 上传文件 + * @param file 表单文件对象 + * @return + */ + FileInfo uploadFile(MultipartFile file); + + /** + * 删除文件 + * + * @param filePath + * @return + */ + boolean deleteFile(String filePath); + + +} diff --git a/src/main/java/com/youlai/system/service/impl/MinioServiceImpl.java b/src/main/java/com/youlai/system/service/impl/MinioServiceImpl.java new file mode 100644 index 00000000..aaaba9d9 --- /dev/null +++ b/src/main/java/com/youlai/system/service/impl/MinioServiceImpl.java @@ -0,0 +1,182 @@ +package com.youlai.system.service.impl; + +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 cn.hutool.core.util.StrUtil; +import com.youlai.system.pojo.vo.file.FileInfo; +import com.youlai.system.service.FileService; +import io.minio.*; +import io.minio.http.Method; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.InitializingBean; +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; + + +@Component +@ConfigurationProperties(prefix = "minio") +@Slf4j +public class MinioServiceImpl implements FileService, InitializingBean { + + /** + * MinIO的API地址 + */ + @Setter + private String endpoint; + + /** + * 用户名 + */ + @Setter + private String accessKey; + + /** + * 密钥 + */ + @Setter + private String secretKey; + + /** + * 存储桶名称 + */ + @Setter + private String bucketName; + + /** + * 自定义域名(非必须) + */ + @Setter + private String customDomain; + + + private MinioClient minioClient; + + @Override + public void afterPropertiesSet() { + log.info("MinIO Client init..."); + Assert.notBlank(endpoint, "MinIO endpoint can not be null"); + 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) + .credentials(accessKey, secretKey) + .build(); + } + + + @Override + @SneakyThrows + public FileInfo uploadFile(MultipartFile file) { + // 存储桶不存在则创建 + createBucketIfAbsent(bucketName); + + // 生成文件名(日期文件夹) + String suffix = FileUtil.getSuffix(file.getOriginalFilename()); + String uuid = IdUtil.simpleUUID(); + String fileName = DateUtil.format(LocalDateTime.now(), "yyyy/MM/dd") + "/" + uuid + "." + suffix; + + InputStream inputStream = file.getInputStream(); + + // 文件上传 + PutObjectArgs putObjectArgs = PutObjectArgs.builder() + .bucket(bucketName) + .object(fileName) + .contentType(file.getContentType()) + .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(); + + 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; + } + + + + @Override + @SneakyThrows + public boolean deleteFile(String fileName) { + RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder() + .bucket(bucketName) + .object(fileName) + .build(); + minioClient.removeObject(removeObjectArgs); + return true; + } + + + /** + * PUBLIC桶策略 + * 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied + * + * @param bucketName + * @return + */ + private static String publicBucketPolicy(String bucketName) { + /** + * AWS的S3存储桶策略 + * Principal: 生效用户对象 + * Resource: 指定存储桶 + * Action: 操作行为 + */ + StringBuilder builder = new StringBuilder(); + builder.append("{\"Version\":\"2012-10-17\"," + + "\"Statement\":[{\"Effect\":\"Allow\"," + + "\"Principal\":{\"AWS\":[\"*\"]}," + + "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"]," + + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]}," + + "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]}," + + "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"]," + + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"); + + return builder.toString(); + } + + /** + * 创建存储桶(存储桶不存在) + * + * @param bucketName + */ + @SneakyThrows + private void createBucketIfAbsent(String bucketName) { + BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build(); + if (!minioClient.bucketExists(bucketExistsArgs)) { + MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build(); + + minioClient.makeBucket(makeBucketArgs); + + // 设置存储桶访问权限为PUBLIC, 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied + SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs + .builder() + .bucket(bucketName) + .config(publicBucketPolicy(bucketName)) + .build(); + minioClient.setBucketPolicy(setBucketPolicyArgs); + } + } + +}