pub mod blob_store;
pub mod error;
pub mod name;
#[cfg(feature = "artifact-local")]
pub mod local;
use std::path::Path;
use bytes::Bytes;
use futures_util::stream::{once, unfold};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use crate::blob_store::ByteStream;
use crate::error::ArtifactError;
const READ_CHUNK_BYTES: usize = 64 * 1024;
pub fn stream_from_bytes(bytes: impl Into<Bytes>) -> ByteStream {
let bytes = bytes.into();
Box::pin(once(async move { Ok(bytes) }))
}
pub fn stream_from_file(file: File) -> ByteStream {
Box::pin(unfold(Some(file), |state| async move {
let mut file = state?;
let mut buf = vec![0u8; READ_CHUNK_BYTES];
match file.read(&mut buf).await {
Ok(0) => None,
Ok(read) => {
buf.truncate(read);
Some((Ok(Bytes::from(buf)), Some(file)))
}
Err(err) => Some((Err(ArtifactError::from(err)), None)),
}
}))
}
pub async fn stream_from_path(path: impl AsRef<Path>) -> Result<ByteStream, ArtifactError> {
let path = path.as_ref();
let file = File::open(path).await.map_err(|err| match err.kind() {
std::io::ErrorKind::NotFound => ArtifactError::NotFound(path.display().to_string()),
_ => ArtifactError::from(err),
})?;
Ok(stream_from_file(file))
}
pub mod prelude {
pub use crate::blob_store::{BlobDigest, BlobFuture, BlobStore, ByteStream};
pub use crate::error::ArtifactError;
pub use crate::name::{
MAX_ARTIFACT_NAME_LEN, guess_content_type, storage_key, validate_artifact_name,
};
pub use crate::{stream_from_bytes, stream_from_file, stream_from_path};
#[cfg(feature = "artifact-local")]
pub use crate::local::{DEFAULT_MAX_ARTIFACT_BYTES, LocalBlobStore};
}
#[cfg(test)]
mod tests {
use futures_util::TryStreamExt;
use super::*;
#[tokio::test]
async fn stream_from_bytes_yields_the_whole_buffer() {
let chunks: Vec<Bytes> = stream_from_bytes(b"hello".to_vec())
.try_collect()
.await
.expect("collect");
assert_eq!(chunks.concat(), b"hello");
}
#[tokio::test]
async fn stream_from_bytes_supports_an_empty_buffer() {
let chunks: Vec<Bytes> = stream_from_bytes(Vec::new())
.try_collect()
.await
.expect("collect");
assert!(chunks.concat().is_empty());
}
#[tokio::test]
async fn stream_from_path_reads_a_file_larger_than_one_chunk() {
let dir = tempfile::TempDir::new().expect("temp dir");
let path = dir.path().join("big.bin");
let payload: Vec<u8> = (0..200_000u32).map(|i| (i % 256) as u8).collect();
std::fs::write(&path, &payload).expect("write");
let chunks: Vec<Bytes> = stream_from_path(&path)
.await
.expect("open")
.try_collect()
.await
.expect("collect");
assert_eq!(chunks.concat(), payload);
assert!(chunks.len() > 1, "large file should stream in chunks");
}
#[tokio::test]
async fn stream_from_path_reports_a_missing_file_as_not_found() {
let result = stream_from_path("/nonexistent/ironflow/artifact").await;
assert!(matches!(result, Err(ArtifactError::NotFound(_))));
}
}