|
4 | 4 | **/ |
5 | 5 | package com.ligg.common.service.impl; |
6 | 6 |
|
| 7 | +import com.ligg.common.config.MinioConfig; |
| 8 | +import com.ligg.common.enums.BusinessStates; |
7 | 9 | import com.ligg.common.enums.ImageType; |
8 | 10 | import com.ligg.common.constants.Constant; |
9 | 11 | import com.ligg.common.service.FileService; |
| 12 | +import io.minio.*; |
| 13 | +import io.minio.errors.*; |
| 14 | +import io.minio.http.Method; |
| 15 | +import lombok.RequiredArgsConstructor; |
10 | 16 | import lombok.extern.slf4j.Slf4j; |
11 | 17 | import net.coobird.thumbnailator.Thumbnails; |
12 | 18 | import org.springframework.beans.factory.annotation.Value; |
|
21 | 27 | import java.nio.file.Files; |
22 | 28 | import java.nio.file.Path; |
23 | 29 | import java.nio.file.Paths; |
| 30 | +import java.security.InvalidKeyException; |
| 31 | +import java.security.NoSuchAlgorithmException; |
24 | 32 | import java.util.UUID; |
25 | 33 |
|
26 | 34 | @Slf4j |
27 | 35 | @Service |
| 36 | +@RequiredArgsConstructor |
28 | 37 | public class FileServiceImpl implements FileService { |
29 | 38 |
|
30 | 39 | // 图片存储根路径 |
31 | 40 | @Value("${file.image.base-path}") |
32 | 41 | private String IMAGE_PATH; |
33 | 42 |
|
| 43 | + private final MinioConfig minioConfig; |
| 44 | + |
| 45 | + private final MinioClient minioClient; |
| 46 | + |
| 47 | + |
34 | 48 | @Override |
35 | 49 | public String uploadImage(MultipartFile imageFile, String path) { |
36 | 50 | if (imageFile.isEmpty()) { |
@@ -76,6 +90,134 @@ public String uploadImage(MultipartFile imageFile, String path) { |
76 | 90 | } |
77 | 91 | } |
78 | 92 |
|
| 93 | + /** |
| 94 | + * 上传图片文件到Minio |
| 95 | + * |
| 96 | + * @param imageFile 图片文件 |
| 97 | + * @param filePath 指定文件保存的路径(前缀) |
| 98 | + * @return 图片访问路径 |
| 99 | + */ |
| 100 | + @Override |
| 101 | + public String minioFileUpload(MultipartFile imageFile, String filePath) { |
| 102 | + String bucketName = minioConfig.getBucketName(); |
| 103 | + String datePath = java.time.LocalDate.now().toString(); |
| 104 | + String fullFilePath = String.join("/", (filePath != null ? filePath : ""), datePath, UUID.randomUUID() + "_" + imageFile.getOriginalFilename()); |
| 105 | + //创建存储桶(如果不存在) |
| 106 | + try { |
| 107 | + if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) { |
| 108 | + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); |
| 109 | + //设置访问权限 |
| 110 | + minioClient.setBucketPolicy(SetBucketPolicyArgs.builder() |
| 111 | + .bucket(bucketName) |
| 112 | + .config(getPublicReadPolicy(bucketName)) |
| 113 | + .build()); |
| 114 | + } |
| 115 | + } catch (Exception e) { |
| 116 | + log.error("创建存储桶失败: {}", e.getMessage()); |
| 117 | + throw new RuntimeException(BusinessStates.INTERNAL_SERVER_ERROR.getMessage()); |
| 118 | + } |
| 119 | + |
| 120 | + //上传文件 |
| 121 | + try (InputStream inputStream = imageFile.getInputStream()) { |
| 122 | + minioClient.putObject(PutObjectArgs.builder() |
| 123 | + .bucket(bucketName) |
| 124 | + .object(fullFilePath) |
| 125 | + .stream(inputStream, imageFile.getSize(), -1) |
| 126 | + .contentType(imageFile.getContentType()) |
| 127 | + .build()); |
| 128 | + } catch (Exception e) { |
| 129 | + log.error("上传文件失败: {}", e.getMessage()); |
| 130 | + } |
| 131 | + //这里不返回minioConfig.getEndpoint() (minio服务地址) 让前端自己做代理转发 |
| 132 | + return "/" + bucketName + fullFilePath; |
| 133 | + } |
| 134 | + |
| 135 | + /** |
| 136 | + * 删除minio中的文件 |
| 137 | + */ |
| 138 | + @Override |
| 139 | + @Async("fileTaskExecutor") |
| 140 | + public void deleteMinioFile(String fileUrl) { |
| 141 | + //截取相对路径url |
| 142 | + String[] splitUrl = fileUrl.split(minioConfig.getBucketName()); |
| 143 | + //检查存储桶是否存在 |
| 144 | + try { |
| 145 | + minioClient.bucketExists( |
| 146 | + BucketExistsArgs.builder().bucket(minioConfig.getBucketName()).build() |
| 147 | + ); |
| 148 | + } catch (Exception e) { |
| 149 | + log.error("存储桶不存在: {}", e.getMessage()); |
| 150 | + throw new RuntimeException(BusinessStates.METHOD_NOT_ALLOWED.getMessage()); |
| 151 | + } |
| 152 | + |
| 153 | + //接收文件url的是存储桶的相对路径,而不是完整的url访问路径 |
| 154 | + if (isFileExist(splitUrl[1])) { |
| 155 | + try { |
| 156 | + //删除文件 |
| 157 | + minioClient.removeObject(RemoveObjectArgs.builder() |
| 158 | + .bucket(minioConfig.getBucketName()) |
| 159 | + .object(splitUrl[1]) |
| 160 | + .build()); |
| 161 | + return; |
| 162 | + } catch (Exception e) { |
| 163 | + log.error("删除文件失败: {}", e.getMessage()); |
| 164 | + } |
| 165 | + } |
| 166 | + log.error("文件不存在: {}", fileUrl); |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * 判断文件是否存储 |
| 171 | + * fileUrl接收的是存储桶的相对路径,而不是完整的url访问路径 |
| 172 | + */ |
| 173 | + private boolean isFileExist(String fileUrl) { |
| 174 | + try { |
| 175 | + minioClient.statObject( |
| 176 | + StatObjectArgs.builder() |
| 177 | + .bucket(minioConfig.getBucketName()) |
| 178 | + .object(fileUrl) |
| 179 | + .build() |
| 180 | + ); |
| 181 | + return true; |
| 182 | + } catch (Exception e) { |
| 183 | + return false; |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + /** |
| 188 | + * url签名 |
| 189 | + */ |
| 190 | + private String getUrlSignature(String bucketName, String objectName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException { |
| 191 | + return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() |
| 192 | + .bucket(bucketName) |
| 193 | + .object(objectName) |
| 194 | + .method(Method.GET) |
| 195 | + .build()); |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * 存储桶配置 |
| 200 | + * "Principal": "*" 表示允许所有用户访问。 |
| 201 | + * "Action": "s3:GetObject" 表示允许下载/读取对象 |
| 202 | + * "Action": "s3:PutObject" 表示允许上传/写入对象 |
| 203 | + * %s 是格式化占位符,会被传入的 bucketName 替换 |
| 204 | + */ |
| 205 | + private String getPublicReadPolicy(String bucketName) { |
| 206 | + return """ |
| 207 | + { |
| 208 | + "Version":"2012-10-17", |
| 209 | + "Statement":[ |
| 210 | + { |
| 211 | + "Effect":"Allow", |
| 212 | + "Principal":"*", |
| 213 | + "Action":["s3:GetObject"], |
| 214 | + "Resource":["arn:aws:s3:::%s/*"] |
| 215 | + } |
| 216 | + ] |
| 217 | + } |
| 218 | + """.formatted(bucketName); |
| 219 | + } |
| 220 | + |
79 | 221 | /** |
80 | 222 | * 获取多质量图片输入流 |
81 | 223 | * |
@@ -153,7 +295,7 @@ public StreamingResponseBody getImageInputStream(String path, String date, Strin |
153 | 295 | @Override |
154 | 296 | @Deprecated |
155 | 297 | public void deleteFile(String filePath) { |
156 | | - Path imagePath = Paths.get(getBasePath(), filePath).toAbsolutePath().normalize() ; |
| 298 | + Path imagePath = Paths.get(getBasePath(), filePath).toAbsolutePath().normalize(); |
157 | 299 | //检查文件是否存在 |
158 | 300 | if (Files.exists(imagePath)) { |
159 | 301 | try { |
@@ -189,6 +331,7 @@ public void deleteFileAsync(String filePath) { |
189 | 331 |
|
190 | 332 | /** |
191 | 333 | * 获取基础路径 |
| 334 | + * |
192 | 335 | * @return 绝对路径 |
193 | 336 | */ |
194 | 337 | private String getBasePath() { |
|
0 commit comments