mod azure_block_blob;
mod local_fs;
mod multipart;
mod reader;
mod single_put;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use crate::WyrdClient;
use bytes::Bytes;
use futures_util::stream::{BoxStream, StreamExt};
use sha2::{Digest, Sha256};
use tokio::io::AsyncReadExt;
use tokio_util::io::ReaderStream;
use wyrd_spec::storage::{UploadCompleteRequest, UploadId, UploadPlan};
use crate::storage::WyrdStorageClient;
use crate::storage::error::StorageClientError;
use reader::SourceReader;
#[cfg(test)]
mod tests;
pub trait ArtifactSource: Send {
fn size_hint(&self) -> Option<u64>;
fn into_stream(self) -> BoxStream<'static, Result<Bytes, StorageClientError>>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UploadProgress {
pub upload_id: UploadId,
pub uploaded_bytes: u64,
pub total_bytes: Option<u64>,
}
pub type UploadProgressSink = Arc<dyn Fn(UploadProgress) + Send + Sync + 'static>;
impl ArtifactSource for Vec<u8> {
fn size_hint(&self) -> Option<u64> {
Some(self.len() as u64)
}
fn into_stream(self) -> BoxStream<'static, Result<Bytes, StorageClientError>> {
futures_util::stream::once(async move { Ok(Bytes::from(self)) }).boxed()
}
}
impl ArtifactSource for Bytes {
fn size_hint(&self) -> Option<u64> {
Some(self.len() as u64)
}
fn into_stream(self) -> BoxStream<'static, Result<Bytes, StorageClientError>> {
futures_util::stream::once(async move { Ok(self) }).boxed()
}
}
#[derive(Debug, Clone)]
pub struct FileSource {
path: PathBuf,
size: Option<u64>,
}
impl FileSource {
#[must_use]
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
size: None,
}
}
pub async fn open(path: impl Into<PathBuf>) -> Result<Self, StorageClientError> {
let path = path.into();
let size = tokio::fs::metadata(&path).await?.len();
Ok(Self {
path,
size: Some(size),
})
}
}
impl ArtifactSource for FileSource {
fn size_hint(&self) -> Option<u64> {
self.size
}
fn into_stream(self) -> BoxStream<'static, Result<Bytes, StorageClientError>> {
async_stream::try_stream! {
let file = tokio::fs::File::open(self.path).await?;
let mut reader = ReaderStream::new(file);
while let Some(chunk) = reader.next().await {
yield chunk.map_err(StorageClientError::Io)?;
}
}
.boxed()
}
}
pub(crate) async fn sha256_file(path: &Path) -> std::io::Result<(u64, [u8; 32])> {
let mut file = tokio::fs::File::open(path).await?;
let mut hasher = Sha256::new();
let mut size = 0_u64;
let mut buffer = vec![0_u8; 64 * 1024];
loop {
let read = file.read(&mut buffer).await?;
if read == 0 {
return Ok((size, hasher.finalize().into()));
}
size += read as u64;
hasher.update(&buffer[..read]);
}
}
pub(crate) type PartUrlFuture<'a> =
Pin<Box<dyn Future<Output = Result<String, StorageClientError>> + Send + 'a>>;
pub(crate) type PartUrlMinter<'a> = Box<dyn FnMut(u32) -> PartUrlFuture<'a> + Send + Sync + 'a>;
pub(crate) type ProgressCallback = Arc<dyn Fn(u64, Option<u64>) + Send + Sync + 'static>;
pub(crate) struct UploadHooks<'a> {
pub idempotency_key: &'a str,
pub progress: Option<ProgressCallback>,
pub part_url_minter: Option<PartUrlMinter<'a>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum UploadOutcome {
Uploaded,
NeedsServerComplete(UploadCompleteRequest),
}
impl UploadOutcome {
#[must_use]
pub fn into_server_complete(self) -> Option<UploadCompleteRequest> {
match self {
Self::Uploaded => None,
Self::NeedsServerComplete(request) => Some(request),
}
}
}
pub(crate) async fn dispatch<S: ArtifactSource>(
client: &WyrdClient,
plan: &UploadPlan,
source: S,
mut hooks: UploadHooks<'_>,
) -> Result<UploadOutcome, StorageClientError> {
validate_plan(plan)?;
let total = source.size_hint();
let reader = SourceReader::new(source.into_stream());
match plan {
UploadPlan::LocalFs { .. } => local_fs::upload(client, plan, reader, total, &hooks).await,
UploadPlan::SinglePut { .. } => {
single_put::upload(client, plan, reader, total, &hooks).await
}
UploadPlan::Multipart { .. } => {
multipart::upload(client, plan, reader, total, &mut hooks).await
}
UploadPlan::AzureBlockBlob { .. } => {
azure_block_blob::upload(client, plan, reader, total, &hooks).await
}
}
}
fn validate_plan(plan: &UploadPlan) -> Result<(), StorageClientError> {
match plan {
UploadPlan::Multipart {
part_count,
part_size_bytes,
..
} if *part_count == 0 || *part_size_bytes == 0 => Err(StorageClientError::PlanInvalid(
"multipart part count and size must be positive",
)),
UploadPlan::AzureBlockBlob {
block_size_bytes,
block_count_planned,
..
} if *block_size_bytes == 0 || *block_count_planned == 0 => Err(
StorageClientError::PlanInvalid("Azure block count and size must be positive"),
),
_ => Ok(()),
}
}
pub(crate) fn checked_size(value: u64, name: &'static str) -> Result<usize, StorageClientError> {
let size = usize::try_from(value).map_err(|_| StorageClientError::PlanInvalid(name))?;
if size > isize::MAX as usize {
return Err(StorageClientError::PlanInvalid(name));
}
Ok(size)
}
pub(crate) fn report(progress: Option<&ProgressCallback>, uploaded: u64, total: Option<u64>) {
if let Some(progress) = progress {
progress(uploaded, total);
}
}
pub(crate) fn part_url_minter<'a>(
storage: &'a WyrdStorageClient,
upload_id: &'a UploadId,
plan: &UploadPlan,
) -> Option<PartUrlMinter<'a>> {
if !matches!(plan, UploadPlan::Multipart { .. }) {
return None;
}
let storage = storage.clone();
let upload_id = upload_id.clone();
Some(Box::new(move |part_number| {
let storage = storage.clone();
let upload_id = upload_id.clone();
Box::pin(async move { storage.part_url(&upload_id, part_number).await })
}))
}
pub(crate) fn plan_variant(plan: &UploadPlan) -> &'static str {
match plan {
UploadPlan::LocalFs { .. } => "LocalFs",
UploadPlan::SinglePut { .. } => "SinglePut",
UploadPlan::Multipart { .. } => "Multipart",
UploadPlan::AzureBlockBlob { .. } => "AzureBlockBlob",
}
}