diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f38e8cc..79510bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,8 +26,10 @@ jobs: - name: "clippy --all" run: cargo clippy --all --all-features --tests -- -D warnings - - name: "cargo check" - run: cargo check --all --all-features + - run: cargo install cargo-all-features + + - name: Check all combinations of features can build + run: cargo check-all-features - name: "cargo test" run: | diff --git a/.gitignore b/.gitignore index df34866..834eff8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.vscode *.tif *.buf diff --git a/Cargo.toml b/Cargo.toml index 1c4ed93..e669dc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,14 +15,29 @@ flate2 = "1.0.20" futures = "0.3.31" jpeg = { package = "jpeg-decoder", version = "0.3.0", default-features = false } num_enum = "0.7.3" -object_store = "0.12" -# In the future we could make this feature-flagged, but for now we depend on -# object_store which uses reqwest. -reqwest = { version = "0.12", default-features = false } +object_store = { version = "0.12", optional = true } +reqwest = { version = "0.12", default-features = false, optional = true } thiserror = "1" -tokio = { version = "1.43.0", optional = true } +tokio = { version = "1.43.0", optional = true, default-features = false, features = [ + "io-util", + "sync", +] } weezl = "0.1.0" [dev-dependencies] +object_store = "0.12" tiff = "0.9.1" -tokio = { version = "1.9", features = ["macros", "fs", "rt-multi-thread"] } +tokio = { version = "1.9", features = [ + "macros", + "fs", + "rt-multi-thread", + "io-util", +] } + +[features] +default = ["object_store", "reqwest"] +tokio = ["dep:tokio"] +reqwest = ["dep:reqwest"] +object_store = ["dep:object_store"] + +[package.metadata.cargo-all-features] diff --git a/src/error.rs b/src/error.rs index 9f5b7f0..ddbb27e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,7 +9,7 @@ use thiserror::Error; pub enum AsyncTiffError { /// End of file error. #[error("End of File: expected to read {0} bytes, got {1}")] - EndOfFile(usize, usize), + EndOfFile(u64, u64), /// General error. #[error("General error: {0}")] @@ -24,6 +24,7 @@ pub enum AsyncTiffError { JPEGDecodingError(#[from] jpeg::Error), /// Error while fetching data using object store. + #[cfg(feature = "object_store")] #[error(transparent)] ObjectStore(#[from] object_store::Error), @@ -32,6 +33,7 @@ pub enum AsyncTiffError { InternalTIFFError(#[from] crate::tiff::TiffError), /// Reqwest error + #[cfg(feature = "reqwest")] #[error(transparent)] ReqwestError(#[from] reqwest::Error), diff --git a/src/reader.rs b/src/reader.rs index 8563077..1757dad 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -8,8 +8,8 @@ use std::sync::Arc; use byteorder::{BigEndian, LittleEndian, ReadBytesExt}; use bytes::buf::Reader; use bytes::{Buf, Bytes}; -use futures::future::{BoxFuture, FutureExt, TryFutureExt}; -use object_store::ObjectStore; +use futures::future::{BoxFuture, FutureExt}; +use futures::TryFutureExt; use crate::error::{AsyncTiffError, AsyncTiffResult}; @@ -67,45 +67,75 @@ impl AsyncFileReader for Box { } } -// #[cfg(feature = "tokio")] -// impl AsyncFileReader -// for T -// { -// fn get_bytes(&self, range: Range) -> BoxFuture<'_, AsyncTiffResult> { -// use tokio::io::{AsyncReadExt, AsyncSeekExt}; - -// async move { -// self.seek(std::io::SeekFrom::Start(range.start)).await?; - -// let to_read = (range.end - range.start).try_into().unwrap(); -// let mut buffer = Vec::with_capacity(to_read); -// let read = self.take(to_read as u64).read_to_end(&mut buffer).await?; -// if read != to_read { -// return Err(AsyncTiffError::EndOfFile(to_read, read)); -// } - -// Ok(buffer.into()) -// } -// .boxed() -// } -// } +/// A wrapper for things that implement [AsyncRead] and [AsyncSeek] to also implement +/// [AsyncFileReader]. +/// +/// This wrapper is needed because `AsyncRead` and `AsyncSeek` require mutable access to seek and +/// read data, while the `AsyncFileReader` trait requires immutable access to read data. +/// +/// This wrapper stores the inner reader in a `Mutex`. +/// +/// [AsyncRead]: tokio::io::AsyncRead +/// [AsyncSeek]: tokio::io::AsyncSeek +#[cfg(feature = "tokio")] +#[derive(Debug)] +pub struct TokioReader( + tokio::sync::Mutex, +); + +#[cfg(feature = "tokio")] +impl TokioReader { + /// Create a new TokioReader from a reader. + pub fn new(inner: T) -> Self { + Self(tokio::sync::Mutex::new(inner)) + } +} + +#[cfg(feature = "tokio")] +impl AsyncFileReader + for TokioReader +{ + fn get_bytes(&self, range: Range) -> BoxFuture<'_, AsyncTiffResult> { + use std::io::SeekFrom; + use tokio::io::{AsyncReadExt, AsyncSeekExt}; + + async move { + let mut file = self.0.lock().await; + + file.seek(SeekFrom::Start(range.start)).await?; + + let to_read = range.end - range.start; + let mut buffer = Vec::with_capacity(to_read as usize); + let read = file.read(&mut buffer).await? as u64; + if read != to_read { + return Err(AsyncTiffError::EndOfFile(to_read, read)); + } + + Ok(buffer.into()) + } + .boxed() + } +} /// An AsyncFileReader that reads from an [`ObjectStore`] instance. +#[cfg(feature = "object_store")] #[derive(Clone, Debug)] pub struct ObjectReader { - store: Arc, + store: Arc, path: object_store::path::Path, } +#[cfg(feature = "object_store")] impl ObjectReader { /// Creates a new [`ObjectReader`] for the provided [`ObjectStore`] and path /// /// [`ObjectMeta`] can be obtained using [`ObjectStore::list`] or [`ObjectStore::head`] - pub fn new(store: Arc, path: object_store::path::Path) -> Self { + pub fn new(store: Arc, path: object_store::path::Path) -> Self { Self { store, path } } } +#[cfg(feature = "object_store")] impl AsyncFileReader for ObjectReader { fn get_bytes(&self, range: Range) -> BoxFuture<'_, AsyncTiffResult> { let range = range.start as _..range.end as _; @@ -134,12 +164,14 @@ impl AsyncFileReader for ObjectReader { } /// An AsyncFileReader that reads from a URL using reqwest. +#[cfg(feature = "reqwest")] #[derive(Debug, Clone)] pub struct ReqwestReader { client: reqwest::Client, url: reqwest::Url, } +#[cfg(feature = "reqwest")] impl ReqwestReader { /// Construct a new ReqwestReader from a reqwest client and URL. pub fn new(client: reqwest::Client, url: reqwest::Url) -> Self { @@ -147,6 +179,7 @@ impl ReqwestReader { } } +#[cfg(feature = "reqwest")] impl AsyncFileReader for ReqwestReader { fn get_bytes(&self, range: Range) -> BoxFuture<'_, AsyncTiffResult> { let url = self.url.clone();