实现S3分块上传支持大文件 - 超过10MB的文件使用分块上传(Multipart Upload) - 每个分块5MB,支持最大5GB文件 - 解决S3服务商413错误
This commit is contained in:
parent
2697494934
commit
c7728df3fb
@ -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<CompletedPart> 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客户端
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user