use std::path::Path;
use rusty_s3::actions::{
AbortMultipartUpload, CompleteMultipartUpload, CreateMultipartUpload, S3Action, UploadPart,
};
use tokio::io::{AsyncReadExt, AsyncSeekExt};
use super::keyspace::Keyspace;
use crate::error::Error;
pub(crate) const SINGLE_PUT_CEILING: u64 = 5 * 1024 * 1024 * 1024;
const SMALLEST_PART: u64 = 5 * 1024 * 1024;
const MOST_PARTS: u64 = 10_000;
const PART: u64 = 64 * 1024 * 1024;
fn part_size(length: u64) -> u64 {
PART.max(length.div_ceil(MOST_PARTS)).max(SMALLEST_PART)
}
pub(crate) async fn put(
keys: &Keyspace,
key: &str,
staged: &Path,
length: u64,
) -> Result<(), Error> {
put_in_parts(keys, key, staged, length, part_size(length)).await
}
async fn put_in_parts(
keys: &Keyspace,
key: &str,
staged: &Path,
length: u64,
size: u64,
) -> Result<(), Error> {
let upload = begin(keys, key).await?;
tracing::info!(
key,
length,
part_size = size,
"an object over the single-request ceiling is going up in parts"
);
match parts(keys, key, &upload, staged, length, size).await {
Ok(etags) => finish(keys, key, &upload, etags).await,
Err(error) => {
if let Err(abandoned) = abort(keys, key, &upload).await {
tracing::warn!(
%abandoned,
key,
"an interrupted multipart upload could not be aborted, so its parts stay until \
a lifecycle rule removes them"
);
}
Err(error)
}
}
}
async fn begin(keys: &Keyspace, key: &str) -> Result<String, Error> {
let action = CreateMultipartUpload::new(keys.bucket(), Some(keys.credentials()), key);
let url = action.sign(keys.lifetime());
let response = keys
.client()
.post(url)
.header(reqwest::header::CONTENT_LENGTH, 0)
.send()
.await
.map_err(|_| unreachable())?;
let body = keys.expect_success(response, "start a multipart upload").await?.text().await.map_err(|error| {
Error::Storage(std::io::Error::other(format!(
"the object store gave an unreadable answer when starting a multipart upload: {error}"
)))
})?;
CreateMultipartUpload::parse_response(&body)
.map(|parsed| parsed.upload_id().to_owned())
.map_err(|error| {
Error::Storage(std::io::Error::other(format!(
"the object store named no upload id: {error}"
)))
})
}
async fn parts(
keys: &Keyspace,
key: &str,
upload: &str,
staged: &Path,
length: u64,
size: u64,
) -> Result<Vec<String>, Error> {
let count = u16::try_from(length.div_ceil(size)).map_err(|_| {
Error::Storage(std::io::Error::other(
"this object needs more parts than one upload may have",
))
})?;
let mut etags = Vec::with_capacity(count.into());
for index in 0..count {
let offset = u64::from(index) * size;
let this = size.min(length - offset);
let mut file = tokio::fs::File::open(staged).await?;
file.seek(std::io::SeekFrom::Start(offset)).await?;
let stream = tokio_util::io::ReaderStream::new(file.take(this));
let action = UploadPart::new(
keys.bucket(),
Some(keys.credentials()),
key,
index + 1,
upload,
);
let response = keys
.client()
.put(action.sign(keys.lifetime()))
.header(reqwest::header::CONTENT_LENGTH, this)
.body(reqwest::Body::wrap_stream(stream))
.send()
.await
.map_err(|_| unreachable())?;
let response = keys.expect_success(response, "write a part").await?;
let etag = response
.headers()
.get(reqwest::header::ETAG)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| {
Error::Storage(std::io::Error::other("the object store tagged no part"))
})?;
etags.push(etag.to_owned());
}
Ok(etags)
}
async fn finish(keys: &Keyspace, key: &str, upload: &str, etags: Vec<String>) -> Result<(), Error> {
let action = CompleteMultipartUpload::new(
keys.bucket(),
Some(keys.credentials()),
key,
upload,
etags.iter().map(String::as_str),
);
let url = action.sign(keys.lifetime());
let body = action.body();
let response = keys
.client()
.post(url)
.header(reqwest::header::CONTENT_LENGTH, body.len())
.body(body)
.send()
.await
.map_err(|_| unreachable())?;
keys.expect_success(response, "assemble a multipart upload")
.await?;
Ok(())
}
async fn abort(keys: &Keyspace, key: &str, upload: &str) -> Result<(), Error> {
let action = AbortMultipartUpload::new(keys.bucket(), Some(keys.credentials()), key, upload);
let response = keys
.client()
.delete(action.sign(keys.lifetime()))
.send()
.await
.map_err(|_| unreachable())?;
keys.expect_success(response, "abort a multipart upload")
.await?;
Ok(())
}
fn unreachable() -> Error {
Error::Storage(std::io::Error::other("the object store is unreachable"))
}
#[cfg(test)]
mod tests;