From c7728df3fb043c0db20f355be27305a1ac865e3a Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 14 Jan 2026 17:07:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0S3=E5=88=86=E5=9D=97=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=94=AF=E6=8C=81=E5=A4=A7=E6=96=87=E4=BB=B6=20-=20?= =?UTF-8?q?=E8=B6=85=E8=BF=8710MB=E7=9A=84=E6=96=87=E4=BB=B6=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=88=86=E5=9D=97=E4=B8=8A=E4=BC=A0(Multipart=20Uploa?= =?UTF-8?q?d)=20-=20=E6=AF=8F=E4=B8=AA=E5=88=86=E5=9D=975MB=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9C=80=E5=A4=A75GB=E6=96=87=E4=BB=B6=20-?= =?UTF-8?q?=20=E8=A7=A3=E5=86=B3S3=E6=9C=8D=E5=8A=A1=E5=95=86413=E9=94=99?= =?UTF-8?q?=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/designhub/service/S3Service.java | 140 ++++++++++++++++-- 1 file changed, 131 insertions(+), 9 deletions(-) diff --git a/backend/src/main/java/com/designhub/service/S3Service.java b/backend/src/main/java/com/designhub/service/S3Service.java index de23be6..2483fe9 100644 --- a/backend/src/main/java/com/designhub/service/S3Service.java +++ b/backend/src/main/java/com/designhub/service/S3Service.java @@ -17,13 +17,24 @@ import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.model.CreateMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.CreateMultipartUploadResponse; +import software.amazon.awssdk.services.s3.model.UploadPartRequest; +import software.amazon.awssdk.services.s3.model.UploadPartResponse; +import software.amazon.awssdk.services.s3.model.CompleteMultipartUploadRequest; +import software.amazon.awssdk.services.s3.model.CompletedMultipartUpload; +import software.amazon.awssdk.services.s3.model.CompletedPart; +import software.amazon.awssdk.services.s3.model.AbortMultipartUploadRequest; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import net.coobird.thumbnailator.Thumbnails; @@ -34,6 +45,11 @@ import net.coobird.thumbnailator.Thumbnails; public class S3Service { private static final Logger log = LoggerFactory.getLogger(S3Service.class); + + // 分块上传的阈值:超过10MB使用分块上传 + private static final long MULTIPART_THRESHOLD = 10 * 1024 * 1024; // 10MB + // 每个分块的大小:5MB(S3要求最小5MB,最大5GB) + private static final int PART_SIZE = 5 * 1024 * 1024; // 5MB @Autowired private S3ConfigMapper s3ConfigMapper; @@ -72,15 +88,22 @@ public class S3Service { try { // 创建S3客户端 S3Client s3Client = createS3Client(config); - - // 上传文件 - PutObjectRequest putObjectRequest = PutObjectRequest.builder() - .bucket(config.getBucket()) - .key(filePath) - .contentType(file.getContentType()) - .build(); - - s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + + // 根据文件大小选择上传方式 + if (file.getSize() > MULTIPART_THRESHOLD) { + // 大文件使用分块上传 + log.info("文件大小 {} bytes,使用分块上传: {}", file.getSize(), originalFilename); + uploadMultipart(s3Client, config, file, filePath); + } else { + // 小文件使用普通上传 + log.info("文件大小 {} bytes,使用普通上传: {}", file.getSize(), originalFilename); + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(config.getBucket()) + .key(filePath) + .contentType(file.getContentType()) + .build(); + s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); + } // 生成文件URL String fileUrl = config.getPublicDomain() + "/" + filePath; @@ -107,6 +130,105 @@ public class S3Service { throw new RuntimeException("文件上传失败: " + e.getMessage()); } } + + /** + * 分块上传大文件 + */ + private void uploadMultipart(S3Client s3Client, S3Config config, MultipartFile file, String filePath) throws IOException { + String uploadId = null; + + try { + // 1. 初始化分块上传 + CreateMultipartUploadRequest createRequest = CreateMultipartUploadRequest.builder() + .bucket(config.getBucket()) + .key(filePath) + .contentType(file.getContentType()) + .build(); + + CreateMultipartUploadResponse createResponse = s3Client.createMultipartUpload(createRequest); + uploadId = createResponse.uploadId(); + log.info("分块上传初始化成功,uploadId: {}", uploadId); + + // 2. 上传各个分块 + List completedParts = new ArrayList<>(); + InputStream inputStream = file.getInputStream(); + byte[] buffer = new byte[PART_SIZE]; + int partNumber = 1; + int bytesRead; + long totalBytesUploaded = 0; + + while ((bytesRead = readFully(inputStream, buffer)) > 0) { + UploadPartRequest uploadPartRequest = UploadPartRequest.builder() + .bucket(config.getBucket()) + .key(filePath) + .uploadId(uploadId) + .partNumber(partNumber) + .build(); + + UploadPartResponse uploadPartResponse = s3Client.uploadPart( + uploadPartRequest, + RequestBody.fromBytes(bytesRead == PART_SIZE ? buffer : java.util.Arrays.copyOf(buffer, bytesRead)) + ); + + completedParts.add(CompletedPart.builder() + .partNumber(partNumber) + .eTag(uploadPartResponse.eTag()) + .build()); + + totalBytesUploaded += bytesRead; + log.info("分块 {} 上传完成,已上传 {} / {} bytes", partNumber, totalBytesUploaded, file.getSize()); + partNumber++; + } + + inputStream.close(); + + // 3. 完成分块上传 + CompletedMultipartUpload completedUpload = CompletedMultipartUpload.builder() + .parts(completedParts) + .build(); + + CompleteMultipartUploadRequest completeRequest = CompleteMultipartUploadRequest.builder() + .bucket(config.getBucket()) + .key(filePath) + .uploadId(uploadId) + .multipartUpload(completedUpload) + .build(); + + s3Client.completeMultipartUpload(completeRequest); + log.info("分块上传完成,共 {} 个分块", completedParts.size()); + + } catch (Exception e) { + // 如果上传失败,取消分块上传 + if (uploadId != null) { + try { + AbortMultipartUploadRequest abortRequest = AbortMultipartUploadRequest.builder() + .bucket(config.getBucket()) + .key(filePath) + .uploadId(uploadId) + .build(); + s3Client.abortMultipartUpload(abortRequest); + log.info("分块上传已取消,uploadId: {}", uploadId); + } catch (Exception abortEx) { + log.error("取消分块上传失败", abortEx); + } + } + throw e; + } + } + + /** + * 读取指定长度的数据到缓冲区 + */ + private int readFully(InputStream inputStream, byte[] buffer) throws IOException { + int totalBytesRead = 0; + int bytesRead; + + while (totalBytesRead < buffer.length && (bytesRead = inputStream.read(buffer, totalBytesRead, buffer.length - totalBytesRead)) != -1) { + totalBytesRead += bytesRead; + } + + return totalBytesRead; + } /** * 创建S3客户端