use super::*;
pub const MULTIPART_THRESHOLD: u64 = 64 * 1024 * 1024;
pub const MULTIPART_PART_SIZE: u64 = 16 * 1024 * 1024;
pub fn should_multipart(size: u64, threshold: u64) -> bool {
size >= threshold
}
pub fn plan_part_lengths(total_size: u64, part_size: u64) -> Vec<u64> {
if total_size == 0 || part_size == 0 {
return Vec::new();
}
let full = total_size / part_size;
let remainder = total_size % part_size;
let mut out = Vec::with_capacity(full as usize + if remainder > 0 { 1 } else { 0 });
for _ in 0..full {
out.push(part_size);
}
if remainder > 0 {
out.push(remainder);
}
out
}
impl AwsClient {
async fn abort_multipart(&self, bucket: &str, key: &str, upload_id: &str) {
if let Err(e) = self
.s3
.abort_multipart_upload()
.bucket(bucket)
.key(key)
.upload_id(upload_id)
.send()
.await
{
tracing::warn!(
target: "ebman::aws",
bucket, key, upload_id,
error = %e,
"AbortMultipartUpload failed — uploaded parts may be left billed"
);
}
}
pub async fn upload_bundle(
&self,
bucket: &str,
key: &str,
path: &std::path::Path,
) -> Result<()> {
self.upload_bundle_with(bucket, key, path, MULTIPART_THRESHOLD, MULTIPART_PART_SIZE)
.await
}
pub async fn upload_bundle_with(
&self,
bucket: &str,
key: &str,
path: &std::path::Path,
multipart_threshold: u64,
part_size: u64,
) -> Result<()> {
use aws_sdk_s3::primitives::ByteStream;
let metadata = tokio::fs::metadata(path)
.await
.wrap_err_with(|| format!("stat bundle {}", path.display()))?;
let size = metadata.len();
if !should_multipart(size, multipart_threshold) {
let body = ByteStream::from_path(path)
.await
.wrap_err_with(|| format!("read {}", path.display()))?;
self.s3
.put_object()
.bucket(bucket)
.key(key)
.body(body)
.send()
.await
.wrap_err_with(|| format!("S3 PutObject {bucket}/{key} failed"))?;
return Ok(());
}
let create = self
.s3
.create_multipart_upload()
.bucket(bucket)
.key(key)
.send()
.await
.wrap_err_with(|| format!("S3 CreateMultipartUpload {bucket}/{key} failed"))?;
let upload_id = create
.upload_id()
.ok_or_else(|| eyre!("CreateMultipartUpload returned no UploadId"))?
.to_string();
let plan = plan_part_lengths(size, part_size);
let mut completed_parts: Vec<aws_sdk_s3::types::CompletedPart> =
Vec::with_capacity(plan.len());
use tokio::io::AsyncReadExt;
let mut file = match tokio::fs::File::open(path).await {
Ok(f) => f,
Err(e) => {
self.abort_multipart(bucket, key, &upload_id).await;
return Err(eyre!("open {} for multipart upload: {e}", path.display()));
}
};
for (idx, part_len) in plan.iter().enumerate() {
let part_number = (idx + 1) as i32;
let mut buf = vec![0u8; *part_len as usize];
if let Err(e) = file.read_exact(&mut buf).await {
self.abort_multipart(bucket, key, &upload_id).await;
return Err(eyre!(
"read part {part_number} from {}: {e}",
path.display()
));
}
let resp = match self
.s3
.upload_part()
.bucket(bucket)
.key(key)
.upload_id(&upload_id)
.part_number(part_number)
.body(ByteStream::from(buf))
.send()
.await
{
Ok(r) => r,
Err(e) => {
self.abort_multipart(bucket, key, &upload_id).await;
return Err(e).wrap_err_with(|| {
format!("S3 UploadPart {part_number} of {bucket}/{key} failed")
});
}
};
let e_tag = match resp.e_tag() {
Some(t) => t.to_string(),
None => {
self.abort_multipart(bucket, key, &upload_id).await;
return Err(eyre!(
"S3 UploadPart {part_number} of {bucket}/{key} returned no ETag"
));
}
};
completed_parts.push(
aws_sdk_s3::types::CompletedPart::builder()
.part_number(part_number)
.e_tag(e_tag)
.build(),
);
}
let completed = aws_sdk_s3::types::CompletedMultipartUpload::builder()
.set_parts(Some(completed_parts))
.build();
if let Err(e) = self
.s3
.complete_multipart_upload()
.bucket(bucket)
.key(key)
.upload_id(&upload_id)
.multipart_upload(completed)
.send()
.await
{
self.abort_multipart(bucket, key, &upload_id).await;
return Err(e)
.wrap_err_with(|| format!("S3 CompleteMultipartUpload {bucket}/{key} failed"));
}
Ok(())
}
}