#![deny(missing_docs)]
use std::path::Path;
use std::sync::Arc;
use crate::WyrdClient;
use wyrd_spec::storage::{
DownloadPlan, PartUrlResponse, SinglePutComplete, UploadCompleteRequest,
UploadCompleteResponse, UploadId, UploadPlan,
};
use crate::storage::upload::{UploadHooks, UploadOutcome};
pub mod download;
pub mod error;
pub mod upload;
pub use download::DownloadOutcome;
pub use error::StorageClientError;
pub use upload::{ArtifactSource, FileSource, UploadProgress, UploadProgressSink};
pub type DownloadProgressSink = Arc<dyn Fn(u64, Option<u64>) + Send + Sync + 'static>;
#[derive(Clone)]
pub struct WyrdStorageClient {
client: WyrdClient,
}
impl WyrdStorageClient {
#[must_use]
pub fn new(client: &WyrdClient) -> Self {
Self {
client: client.clone(),
}
}
pub async fn upload_artifact<S: ArtifactSource>(
&self,
upload_id: &UploadId,
plan: &UploadPlan,
source: S,
idempotency_key: &str,
) -> Result<(), StorageClientError> {
self.upload_artifact_with_progress(upload_id, plan, source, idempotency_key, None)
.await
}
pub async fn upload_artifact_with_progress<S: ArtifactSource>(
&self,
upload_id: &UploadId,
plan: &UploadPlan,
source: S,
idempotency_key: &str,
progress: Option<UploadProgressSink>,
) -> Result<(), StorageClientError> {
let part_url_minter = upload::part_url_minter(self, upload_id, plan);
let progress_callback = progress.map(|sink| {
let upload_id = upload_id.clone();
Arc::new(move |uploaded_bytes, total_bytes| {
sink(UploadProgress {
upload_id: upload_id.clone(),
uploaded_bytes,
total_bytes,
});
}) as upload::ProgressCallback
});
let outcome = self
.upload(
plan,
source,
upload::UploadHooks {
idempotency_key,
progress: progress_callback,
part_url_minter,
},
)
.await?;
let request = outcome
.into_server_complete()
.or(match plan {
UploadPlan::LocalFs { .. } | UploadPlan::SinglePut { .. } => {
Some(UploadCompleteRequest::SinglePut(SinglePutComplete {}))
}
UploadPlan::Multipart { .. } | UploadPlan::AzureBlockBlob { .. } => None,
})
.ok_or(StorageClientError::PlanMismatch {
expected: "server-completable upload outcome",
actual: "provider upload reported complete without completion details",
})?;
self.complete(upload_id, &request, idempotency_key).await?;
Ok(())
}
pub(crate) async fn upload_evidence<S: ArtifactSource>(
&self,
plan: &UploadPlan,
source: S,
idempotency_key: &str,
) -> Result<(), StorageClientError> {
if !matches!(
plan,
UploadPlan::SinglePut { .. } | UploadPlan::LocalFs { .. }
) {
return Err(StorageClientError::PlanMismatch {
expected: "SinglePut",
actual: upload::plan_variant(plan),
});
}
self.upload(
plan,
source,
UploadHooks {
idempotency_key,
progress: None,
part_url_minter: None,
},
)
.await?;
Ok(())
}
async fn upload<S: ArtifactSource>(
&self,
plan: &UploadPlan,
source: S,
hooks: UploadHooks<'_>,
) -> Result<UploadOutcome, StorageClientError> {
upload::dispatch(&self.client, plan, source, hooks).await
}
async fn part_url(
&self,
upload_id: &UploadId,
part_number: u32,
) -> Result<String, StorageClientError> {
let path = format!("/v1/cards/upload/{upload_id}/part-url?part_number={part_number}");
let response: PartUrlResponse = self
.client
.http
.request_json(reqwest::Method::POST, &path, None::<&()>)
.await
.map_err(crate::storage::error::from_authenticated)?;
Ok(response.url)
}
async fn complete(
&self,
upload_id: &UploadId,
request: &UploadCompleteRequest,
idempotency_key: &str,
) -> Result<UploadCompleteResponse, StorageClientError> {
let path = format!("/v1/cards/upload/{upload_id}/complete");
self.client
.http
.submit_with_idempotency_key(reqwest::Method::POST, &path, request, idempotency_key)
.await
.map_err(crate::storage::error::from_authenticated)
}
pub async fn download(
&self,
plan: &DownloadPlan,
dest: &Path,
) -> Result<DownloadOutcome, StorageClientError> {
download::dispatch(&self.client, plan, dest).await
}
pub async fn download_verified(
&self,
plan: &DownloadPlan,
dest: &Path,
expected_sha256: &str,
expected_size_bytes: u64,
) -> Result<DownloadOutcome, StorageClientError> {
self.download_verified_with_progress(
plan,
dest,
expected_sha256,
expected_size_bytes,
Arc::new(|_, _| {}),
)
.await
}
pub async fn download_verified_with_progress(
&self,
plan: &DownloadPlan,
dest: &Path,
expected_sha256: &str,
expected_size_bytes: u64,
progress: DownloadProgressSink,
) -> Result<DownloadOutcome, StorageClientError> {
download::dispatch_verified(
&self.client,
plan,
dest,
expected_sha256,
expected_size_bytes,
progress,
)
.await
}
}