|
|
@@ -0,0 +1,215 @@
|
|
|
+package com.ruoyi.web.core.service;
|
|
|
+
|
|
|
+import com.ruoyi.web.core.config.AwsS3Config;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
+import software.amazon.awssdk.core.sync.RequestBody;
|
|
|
+import software.amazon.awssdk.services.s3.S3Client;
|
|
|
+import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
|
|
+import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
|
|
|
+import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
|
|
+import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
|
|
|
+import software.amazon.awssdk.services.s3.model.S3Exception;
|
|
|
+import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
|
|
+import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
|
|
+import java.time.Duration;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.nio.file.Paths;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.UUID;
|
|
|
+
|
|
|
+/**
|
|
|
+ * AWS S3文件存储服务
|
|
|
+ *
|
|
|
+ * @author ruoyi
|
|
|
+ */
|
|
|
+@Service
|
|
|
+public class AwsS3FileService {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(AwsS3FileService.class);
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private S3Client s3Client;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private AwsS3Config awsS3Config;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传文件到S3
|
|
|
+ *
|
|
|
+ * @param file 上传的文件
|
|
|
+ * @return 文件访问URL
|
|
|
+ * @throws IOException
|
|
|
+ */
|
|
|
+ public String uploadFile(MultipartFile file) throws IOException {
|
|
|
+ try {
|
|
|
+ // 验证文件大小(最大100MB)
|
|
|
+ if (file.getSize() > 100 * 1024 * 1024) {
|
|
|
+ throw new IOException("文件大小超过限制(最大100MB)");
|
|
|
+ }
|
|
|
+
|
|
|
+ String fileName = generateFileName(file.getOriginalFilename());
|
|
|
+ String key = awsS3Config.getPathPrefix() + fileName;
|
|
|
+
|
|
|
+ // 设置Content-Disposition头,强制浏览器下载而非预览
|
|
|
+ String contentDisposition = "attachment; filename=\"" + file.getOriginalFilename() + "\"";
|
|
|
+
|
|
|
+ PutObjectRequest putObjectRequest = PutObjectRequest.builder()
|
|
|
+ .bucket(awsS3Config.getBucketName())
|
|
|
+ .key(key)
|
|
|
+ .contentType(file.getContentType())
|
|
|
+ .contentDisposition(contentDisposition)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ try (InputStream inputStream = file.getInputStream()) {
|
|
|
+ s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, file.getSize()));
|
|
|
+ }
|
|
|
+
|
|
|
+ String fileUrl = awsS3Config.getUrlPrefix() + key;
|
|
|
+ log.info("文件上传成功: {}, URL: {}", fileName, fileUrl);
|
|
|
+
|
|
|
+ return fileUrl;
|
|
|
+
|
|
|
+ } catch (S3Exception e) {
|
|
|
+ String errorMsg = "S3文件上传失败: " + e.awsErrorDetails().errorMessage();
|
|
|
+ log.error(errorMsg);
|
|
|
+ throw new IOException(errorMsg);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("文件上传失败", e);
|
|
|
+ throw new IOException("文件上传失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除S3中的文件
|
|
|
+ *
|
|
|
+ * @param fileUrl 文件URL
|
|
|
+ * @return 是否删除成功
|
|
|
+ */
|
|
|
+ public boolean deleteFile(String fileUrl) {
|
|
|
+ try {
|
|
|
+ // 从URL中提取key
|
|
|
+ String key = extractKeyFromUrl(fileUrl);
|
|
|
+
|
|
|
+ DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder()
|
|
|
+ .bucket(awsS3Config.getBucketName())
|
|
|
+ .key(key)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ s3Client.deleteObject(deleteObjectRequest);
|
|
|
+ log.info("文件删除成功: {}", key);
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } catch (S3Exception e) {
|
|
|
+ log.error("S3文件删除失败: {}", e.awsErrorDetails().errorMessage());
|
|
|
+ return false;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("文件删除失败", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查文件是否存在
|
|
|
+ *
|
|
|
+ * @param fileUrl 文件URL
|
|
|
+ * @return 是否存在
|
|
|
+ */
|
|
|
+ public boolean fileExists(String fileUrl) {
|
|
|
+ try {
|
|
|
+ String key = extractKeyFromUrl(fileUrl);
|
|
|
+
|
|
|
+ HeadObjectRequest headObjectRequest = HeadObjectRequest.builder()
|
|
|
+ .bucket(awsS3Config.getBucketName())
|
|
|
+ .key(key)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ // 使用headObject检查文件存在性,避免下载文件内容
|
|
|
+ s3Client.headObject(headObjectRequest);
|
|
|
+ return true;
|
|
|
+
|
|
|
+ } catch (S3Exception e) {
|
|
|
+ if ("NoSuchKey".equals(e.awsErrorDetails().errorCode()) ||
|
|
|
+ "404 Not Found".equals(e.awsErrorDetails().errorMessage())) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ log.error("检查文件存在性失败: {}", e.awsErrorDetails().errorMessage());
|
|
|
+ return false;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("检查文件存在性失败", e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成唯一的文件名
|
|
|
+ *
|
|
|
+ * @param originalFileName 原始文件名
|
|
|
+ * @return 生成的文件名
|
|
|
+ */
|
|
|
+ private String generateFileName(String originalFileName) {
|
|
|
+ String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
|
|
|
+ String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
|
|
|
+ String extension = "";
|
|
|
+
|
|
|
+ if (originalFileName != null && originalFileName.contains(".")) {
|
|
|
+ extension = originalFileName.substring(originalFileName.lastIndexOf("."));
|
|
|
+ }
|
|
|
+
|
|
|
+ return timestamp + "_" + uuid + extension;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从文件URL中提取S3 key
|
|
|
+ *
|
|
|
+ * @param fileUrl 文件URL
|
|
|
+ * @return S3 key
|
|
|
+ */
|
|
|
+ private String extractKeyFromUrl(String fileUrl) {
|
|
|
+ if (fileUrl.startsWith(awsS3Config.getUrlPrefix())) {
|
|
|
+ return fileUrl.substring(awsS3Config.getUrlPrefix().length());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果URL不匹配前缀,尝试从完整路径中提取
|
|
|
+ String bucketPart = awsS3Config.getBucketName() + "/";
|
|
|
+ int bucketIndex = fileUrl.indexOf(bucketPart);
|
|
|
+ if (bucketIndex != -1) {
|
|
|
+ return fileUrl.substring(bucketIndex + bucketPart.length());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果无法提取,返回原URL(可能是相对路径)
|
|
|
+ return fileUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取文件下载URL(预签名URL,适用于私有文件)
|
|
|
+ *
|
|
|
+ * @param fileUrl 文件URL
|
|
|
+ * @param expirationMinutes 过期时间(分钟)
|
|
|
+ * @return 预签名URL
|
|
|
+ */
|
|
|
+ public String getDownloadUrl(String fileUrl, int expirationMinutes) {
|
|
|
+ try {
|
|
|
+ String key = extractKeyFromUrl(fileUrl);
|
|
|
+
|
|
|
+ // 对于LocalStack,直接返回公开访问URL
|
|
|
+ if (awsS3Config.getEndpointUrl() != null && !awsS3Config.getEndpointUrl().trim().isEmpty()) {
|
|
|
+ return awsS3Config.getUrlPrefix() + key;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对于真实的AWS S3,这里可以生成预签名URL
|
|
|
+ // 由于LocalStack不需要预签名,我们直接返回公开URL
|
|
|
+ return awsS3Config.getUrlPrefix() + key;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("生成下载URL失败", e);
|
|
|
+ return fileUrl;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|