This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs-object-store.git
The following commit(s) were added to refs/heads/main by this push:
new f0a772c feat: refactor GetOptions with builder, add binary examples
(#517)
f0a772c is described below
commit f0a772cd49d2ebb1f19f487ccd93d705f48dc891
Author: peasee <[email protected]>
AuthorDate: Sun Oct 26 20:40:22 2025 +1000
feat: refactor GetOptions with builder, add binary examples (#517)
* feat: Add get_range_opts, refactor GetOptions with builder
* test: Fix test
* refactor: Remove _opts functions, cargo fmt
* chore: Clippy
* docs: Add examples
* fix: CI
* fix: Silly wasm things
* docs: Add examples inline to docs
* fix: Update builder inputs to options
* test: fix tests
* fix: Bang my head on my keyboard
* fix: Revert change trying to remove docs tests from wasm test
---
Cargo.toml | 2 +-
src/buffered.rs | 32 +----
src/integration.rs | 96 ++++----------
src/lib.rs | 334 ++++++++++++++++++++++++++++++++++++++++++++++--
tests/get_range_file.rs | 33 ++++-
tests/http.rs | 10 +-
6 files changed, 381 insertions(+), 126 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 015e50f..137d2c5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -105,4 +105,4 @@ features = ["js"]
[[test]]
name = "get_range_file"
path = "tests/get_range_file.rs"
-required-features = ["fs"]
+required-features = ["fs"]
\ No newline at end of file
diff --git a/src/buffered.rs b/src/buffered.rs
index 00bea05..9ec3285 100644
--- a/src/buffered.rs
+++ b/src/buffered.rs
@@ -590,13 +590,7 @@ mod tests {
writer.write_all(&[0; 5]).await.unwrap();
writer.shutdown().await.unwrap();
let response = store
- .get_opts(
- &path,
- GetOptions {
- head: true,
- ..Default::default()
- },
- )
+ .get_opts(&path, GetOptions::new().with_head(true))
.await
.unwrap();
assert_eq!(response.meta.size, 25);
@@ -610,13 +604,7 @@ mod tests {
writer.write_all(&[0; 20]).await.unwrap();
writer.shutdown().await.unwrap();
let response = store
- .get_opts(
- &path,
- GetOptions {
- head: true,
- ..Default::default()
- },
- )
+ .get_opts(&path, GetOptions::new().with_head(true))
.await
.unwrap();
assert_eq!(response.meta.size, 40);
@@ -640,13 +628,7 @@ mod tests {
.unwrap();
writer.shutdown().await.unwrap();
let response = store
- .get_opts(
- &path,
- GetOptions {
- head: true,
- ..Default::default()
- },
- )
+ .get_opts(&path, GetOptions::new().with_head(true))
.await
.unwrap();
assert_eq!(response.meta.size, 25);
@@ -664,13 +646,7 @@ mod tests {
.unwrap();
writer.shutdown().await.unwrap();
let response = store
- .get_opts(
- &path,
- GetOptions {
- head: true,
- ..Default::default()
- },
- )
+ .get_opts(&path, GetOptions::new().with_head(true))
.await
.unwrap();
assert_eq!(response.meta.size, 40);
diff --git a/src/integration.rs b/src/integration.rs
index 316986c..1f769b1 100644
--- a/src/integration.rs
+++ b/src/integration.rs
@@ -114,10 +114,7 @@ pub async fn put_get_delete_list(storage: &DynObjectStore)
{
let bytes = range_result.unwrap();
assert_eq!(bytes, data.slice(range.start as usize..range.end as usize));
- let opts = GetOptions {
- range: Some(GetRange::Bounded(2..5)),
- ..Default::default()
- };
+ let opts = GetOptions::new().with_range(Some(GetRange::Bounded(2..5)));
let result = storage.get_opts(&location, opts).await.unwrap();
// Data is `"arbitrary data"`, length 14 bytes
assert_eq!(result.meta.size, 14); // Should return full object size (#5272)
@@ -131,20 +128,14 @@ pub async fn put_get_delete_list(storage:
&DynObjectStore) {
// Should be a non-fatal error
out_of_range_result.unwrap_err();
- let opts = GetOptions {
- range: Some(GetRange::Bounded(2..100)),
- ..Default::default()
- };
+ let opts = GetOptions::new().with_range(Some(GetRange::Bounded(2..100)));
let result = storage.get_opts(&location, opts).await.unwrap();
assert_eq!(result.range, 2..14);
assert_eq!(result.meta.size, 14);
let bytes = result.bytes().await.unwrap();
assert_eq!(bytes, b"bitrary data".as_ref());
- let opts = GetOptions {
- range: Some(GetRange::Suffix(2)),
- ..Default::default()
- };
+ let opts = GetOptions::new().with_range(Some(GetRange::Suffix(2)));
match storage.get_opts(&location, opts).await {
Ok(result) => {
assert_eq!(result.range, 12..14);
@@ -156,10 +147,7 @@ pub async fn put_get_delete_list(storage: &DynObjectStore)
{
Err(e) => panic!("{e}"),
}
- let opts = GetOptions {
- range: Some(GetRange::Suffix(100)),
- ..Default::default()
- };
+ let opts = GetOptions::new().with_range(Some(GetRange::Suffix(100)));
match storage.get_opts(&location, opts).await {
Ok(result) => {
assert_eq!(result.range, 0..14);
@@ -171,20 +159,14 @@ pub async fn put_get_delete_list(storage:
&DynObjectStore) {
Err(e) => panic!("{e}"),
}
- let opts = GetOptions {
- range: Some(GetRange::Offset(3)),
- ..Default::default()
- };
+ let opts = GetOptions::new().with_range(Some(GetRange::Offset(3)));
let result = storage.get_opts(&location, opts).await.unwrap();
assert_eq!(result.range, 3..14);
assert_eq!(result.meta.size, 14);
let bytes = result.bytes().await.unwrap();
assert_eq!(bytes, b"itrary data".as_ref());
- let opts = GetOptions {
- range: Some(GetRange::Offset(100)),
- ..Default::default()
- };
+ let opts = GetOptions::new().with_range(Some(GetRange::Offset(100)));
storage.get_opts(&location, opts).await.unwrap_err();
let ranges = vec![0..1, 2..3, 0..5];
@@ -520,76 +502,55 @@ pub async fn get_opts(storage: &dyn ObjectStore) {
storage.put(&path, "foo".into()).await.unwrap();
let meta = storage.head(&path).await.unwrap();
- let options = GetOptions {
- if_unmodified_since: Some(meta.last_modified),
- ..GetOptions::default()
- };
+ let options =
GetOptions::new().with_if_unmodified_since(Some(meta.last_modified));
match storage.get_opts(&path, options).await {
Ok(_) | Err(Error::NotSupported { .. }) => {}
Err(e) => panic!("{e}"),
}
- let options = GetOptions {
- if_unmodified_since: Some(meta.last_modified +
chrono::Duration::try_hours(10).unwrap()),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_if_unmodified_since(Some(
+ meta.last_modified + chrono::Duration::try_hours(10).unwrap(),
+ ));
match storage.get_opts(&path, options).await {
Ok(_) | Err(Error::NotSupported { .. }) => {}
Err(e) => panic!("{e}"),
}
- let options = GetOptions {
- if_unmodified_since: Some(meta.last_modified -
chrono::Duration::try_hours(10).unwrap()),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_if_unmodified_since(Some(
+ meta.last_modified - chrono::Duration::try_hours(10).unwrap(),
+ ));
match storage.get_opts(&path, options).await {
Err(Error::Precondition { .. } | Error::NotSupported { .. }) => {}
d => panic!("{d:?}"),
}
- let options = GetOptions {
- if_modified_since: Some(meta.last_modified),
- ..GetOptions::default()
- };
+ let options =
GetOptions::new().with_if_modified_since(Some(meta.last_modified));
match storage.get_opts(&path, options).await {
Err(Error::NotModified { .. } | Error::NotSupported { .. }) => {}
d => panic!("{d:?}"),
}
- let options = GetOptions {
- if_modified_since: Some(meta.last_modified -
chrono::Duration::try_hours(10).unwrap()),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_if_modified_since(Some(
+ meta.last_modified - chrono::Duration::try_hours(10).unwrap(),
+ ));
match storage.get_opts(&path, options).await {
Ok(_) | Err(Error::NotSupported { .. }) => {}
Err(e) => panic!("{e}"),
}
let tag = meta.e_tag.unwrap();
- let options = GetOptions {
- if_match: Some(tag.clone()),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_if_match(Some(tag.clone()));
storage.get_opts(&path, options).await.unwrap();
- let options = GetOptions {
- if_match: Some("invalid".to_string()),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_if_match(Some("invalid".to_string()));
let err = storage.get_opts(&path, options).await.unwrap_err();
assert!(matches!(err, Error::Precondition { .. }), "{err}");
- let options = GetOptions {
- if_none_match: Some(tag.clone()),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_if_none_match(Some(tag.clone()));
let err = storage.get_opts(&path, options).await.unwrap_err();
assert!(matches!(err, Error::NotModified { .. }), "{err}");
- let options = GetOptions {
- if_none_match: Some("invalid".to_string()),
- ..GetOptions::default()
- };
+ let options =
GetOptions::new().with_if_none_match(Some("invalid".to_string()));
storage.get_opts(&path, options).await.unwrap();
let result = storage.put(&path, "test".into()).await.unwrap();
@@ -599,26 +560,17 @@ pub async fn get_opts(storage: &dyn ObjectStore) {
let meta = storage.head(&path).await.unwrap();
assert_eq!(meta.e_tag.unwrap(), new_tag);
- let options = GetOptions {
- if_match: Some(new_tag),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_if_match(Some(new_tag.clone()));
storage.get_opts(&path, options).await.unwrap();
- let options = GetOptions {
- if_match: Some(tag),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_if_match(Some(tag));
let err = storage.get_opts(&path, options).await.unwrap_err();
assert!(matches!(err, Error::Precondition { .. }), "{err}");
if let Some(version) = meta.version {
storage.put(&path, "bar".into()).await.unwrap();
- let options = GetOptions {
- version: Some(version),
- ..GetOptions::default()
- };
+ let options = GetOptions::new().with_version(Some(version));
// Can retrieve previous version
let get_opts = storage.get_opts(&path, options).await.unwrap();
diff --git a/src/lib.rs b/src/lib.rs
index bb9f8b1..e37fb69 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -324,6 +324,32 @@
//! # }
//! ```
//!
+//! To retrieve ranges from a versioned object, use [`ObjectStore::get_opts`]
by specifying the range in the [`GetOptions`].
+//!
+//! ```ignore-wasm32
+//! # use object_store::local::LocalFileSystem;
+//! # use object_store::ObjectStore;
+//! # use object_store::GetOptions;
+//! # use std::sync::Arc;
+//! # use bytes::Bytes;
+//! # use tokio::io::AsyncWriteExt;
+//! # use object_store::path::Path;
+//! # fn get_object_store() -> Arc<dyn ObjectStore> {
+//! # Arc::new(LocalFileSystem::new())
+//! # }
+//! # async fn get_range_with_options() {
+//! #
+//! let object_store: Arc<dyn ObjectStore> = get_object_store();
+//! let path = Path::from("data/large_file");
+//! let ranges = vec![90..100, 400..600, 0..10];
+//! for range in ranges {
+//! let opts = GetOptions::default().with_range(Some(range));
+//! let data = object_store.get_opts(&path, opts).await.unwrap();
+//! // Do something with the data
+//! }
+//! # }
+//! ``````
+//!
//! # Vectored Write
//!
//! When writing data it is often the case that the size of the output is not
known ahead of time.
@@ -403,10 +429,7 @@
//! Some(e) => match e.refreshed_at.elapsed() <
Duration::from_secs(10) {
//! true => e.data.clone(), // Return cached data
//! false => { // Check if remote version has changed
-//! let opts = GetOptions {
-//! if_none_match: Some(e.e_tag.clone()),
-//! ..GetOptions::default()
-//! };
+//! let opts =
GetOptions::new().with_if_none_match(Some(e.e_tag.clone()));
//! match self.store.get_opts(&path, opts).await {
//! Ok(d) => e.data = d.bytes().await?,
//! Err(Error::NotModified { .. }) => {} // Data has
not changed
@@ -634,22 +657,196 @@ pub trait ObjectStore: std::fmt::Display + Send + Sync +
Debug + 'static {
) -> Result<Box<dyn MultipartUpload>>;
/// Return the bytes that are stored at the specified location.
+ ///
+ /// ## Example
+ ///
+ /// This example uses a basic local filesystem object store to get an
object.
+ ///
+ /// ```ignore-wasm32
+ /// # use object_store::local::LocalFileSystem;
+ /// # use tempfile::tempdir;
+ /// # use object_store::{path::Path, ObjectStore};
+ /// async fn get_example() {
+ /// let tmp = tempdir().unwrap();
+ /// let store = LocalFileSystem::new_with_prefix(tmp.path()).unwrap();
+ /// let location = Path::from("example.txt");
+ /// let content = b"Hello, Object Store!";
+ ///
+ /// // Put the object into the store
+ /// store
+ /// .put(&location, content.as_ref().into())
+ /// .await
+ /// .expect("Failed to put object");
+ ///
+ /// // Get the object from the store
+ /// let get_result = store.get(&location).await.expect("Failed to get
object");
+ /// let bytes = get_result.bytes().await.expect("Failed to read
bytes");
+ /// println!("Retrieved content: {}", String::from_utf8_lossy(&bytes));
+ /// }
+ /// ```
async fn get(&self, location: &Path) -> Result<GetResult> {
self.get_opts(location, GetOptions::default()).await
}
/// Perform a get request with options
+ ///
+ /// ## Example
+ ///
+ /// This example uses a basic local filesystem object store to get an
object with a specific etag.
+ /// On the local filesystem, supplying an invalid etag will error.
+ /// Versioned object stores will return the specified object version, if
it exists.
+ ///
+ /// ```ignore-wasm32
+ /// # use object_store::local::LocalFileSystem;
+ /// # use tempfile::tempdir;
+ /// # use object_store::{path::Path, ObjectStore, GetOptions};
+ /// async fn get_opts_example() {
+ /// let tmp = tempdir().unwrap();
+ /// let store = LocalFileSystem::new_with_prefix(tmp.path()).unwrap();
+ /// let location = Path::from("example.txt");
+ /// let content = b"Hello, Object Store!";
+ ///
+ /// // Put the object into the store
+ /// store
+ /// .put(&location, content.as_ref().into())
+ /// .await
+ /// .expect("Failed to put object");
+ ///
+ /// // Get the object from the store to figure out the right etag
+ /// let result: object_store::GetResult =
store.get(&location).await.expect("Failed to get object");
+ ///
+ /// let etag = result.meta.e_tag.expect("ETag should be present");
+ ///
+ /// // Get the object from the store with range and etag
+ /// let bytes = store
+ /// .get_opts(
+ /// &location,
+ /// GetOptions::new()
+ /// .with_if_match(Some(etag.clone())),
+ /// )
+ /// .await
+ /// .expect("Failed to get object with range and etag")
+ /// .bytes()
+ /// .await
+ /// .expect("Failed to read bytes");
+ ///
+ /// println!(
+ /// "Retrieved with ETag {}: {}",
+ /// etag,
+ /// String::from_utf8_lossy(&bytes)
+ /// );
+ ///
+ /// // Show that if the etag does not match, we get an error
+ /// let wrong_etag = "wrong-etag".to_string();
+ /// match store
+ /// .get_opts(
+ /// &location,
+ /// GetOptions::new().with_if_match(Some(wrong_etag))
+ /// )
+ /// .await
+ /// {
+ /// Ok(_) => println!("Unexpectedly succeeded with wrong ETag"),
+ /// Err(e) => println!("On a non-versioned object store, getting
an invalid ETag ('wrong-etag') results in an error as expected: {}", e),
+ /// }
+ /// }
+ /// ```
+ ///
+ /// To retrieve a range of bytes from a versioned object, specify the
range in the [`GetOptions`] supplied to this method.
+ ///
+ /// ```ignore-wasm32
+ /// # use object_store::local::LocalFileSystem;
+ /// # use tempfile::tempdir;
+ /// # use object_store::{path::Path, ObjectStore, GetOptions};
+ /// async fn get_opts_range_example() {
+ /// let tmp = tempdir().unwrap();
+ /// let store = LocalFileSystem::new_with_prefix(tmp.path()).unwrap();
+ /// let location = Path::from("example.txt");
+ /// let content = b"Hello, Object Store!";
+ ///
+ /// // Put the object into the store
+ /// store
+ /// .put(&location, content.as_ref().into())
+ /// .await
+ /// .expect("Failed to put object");
+ ///
+ /// // Get the object from the store to figure out the right etag
+ /// let result: object_store::GetResult =
store.get(&location).await.expect("Failed to get object");
+ ///
+ /// let etag = result.meta.e_tag.expect("ETag should be present");
+ ///
+ /// // Get the object from the store with range and etag
+ /// let bytes = store
+ /// .get_opts(
+ /// &location,
+ /// GetOptions::new()
+ /// .with_range(Some(0..5))
+ /// .with_if_match(Some(etag.clone())),
+ /// )
+ /// .await
+ /// .expect("Failed to get object with range and etag")
+ /// .bytes()
+ /// .await
+ /// .expect("Failed to read bytes");
+ ///
+ /// println!(
+ /// "Retrieved range [0-5] with ETag {}: {}",
+ /// etag,
+ /// String::from_utf8_lossy(&bytes)
+ /// );
+ ///
+ /// // Show that if the etag does not match, we get an error
+ /// let wrong_etag = "wrong-etag".to_string();
+ /// match store
+ /// .get_opts(
+ /// &location,
+ ///
GetOptions::new().with_range(Some(0..5)).with_if_match(Some(wrong_etag))
+ /// )
+ /// .await
+ /// {
+ /// Ok(_) => println!("Unexpectedly succeeded with wrong ETag"),
+ /// Err(e) => println!("On a non-versioned object store, getting
an invalid ETag ('wrong-etag') results in an error as expected: {}", e),
+ /// }
+ /// }
+ /// ```
async fn get_opts(&self, location: &Path, options: GetOptions) ->
Result<GetResult>;
/// Return the bytes that are stored at the specified location
/// in the given byte range.
///
- /// See [`GetRange::Bounded`] for more details on how `range` gets
interpreted
+ /// See [`GetRange::Bounded`] for more details on how `range` gets
interpreted.
+ ///
+ /// To retrieve a range of bytes from a versioned object, use
[`ObjectStore::get_opts`] by specifying the range in the [`GetOptions`].
+ ///
+ /// ## Examples
+ ///
+ /// This example uses a basic local filesystem object store to get a byte
range from an object.
+ ///
+ /// ```ignore-wasm32
+ /// # use object_store::local::LocalFileSystem;
+ /// # use tempfile::tempdir;
+ /// # use object_store::{path::Path, ObjectStore};
+ /// async fn get_range_example() {
+ /// let tmp = tempdir().unwrap();
+ /// let store = LocalFileSystem::new_with_prefix(tmp.path()).unwrap();
+ /// let location = Path::from("example.txt");
+ /// let content = b"Hello, Object Store!";
+ ///
+ /// // Put the object into the store
+ /// store
+ /// .put(&location, content.as_ref().into())
+ /// .await
+ /// .expect("Failed to put object");
+ ///
+ /// // Get the object from the store
+ /// let bytes = store
+ /// .get_range(&location, 0..5)
+ /// .await
+ /// .expect("Failed to get object");
+ /// println!("Retrieved range [0-5]: {}",
String::from_utf8_lossy(&bytes));
+ /// }
+ /// ```
async fn get_range(&self, location: &Path, range: Range<u64>) ->
Result<Bytes> {
- let options = GetOptions {
- range: Some(range.into()),
- ..Default::default()
- };
+ let options = GetOptions::new().with_range(Some(range));
self.get_opts(location, options).await?.bytes().await
}
@@ -666,10 +863,7 @@ pub trait ObjectStore: std::fmt::Display + Send + Sync +
Debug + 'static {
/// Return the metadata for the specified location
async fn head(&self, location: &Path) -> Result<ObjectMeta> {
- let options = GetOptions {
- head: true,
- ..Default::default()
- };
+ let options = GetOptions::new().with_head(true);
Ok(self.get_opts(location, options).await?.meta)
}
@@ -1035,6 +1229,83 @@ impl GetOptions {
}
Ok(())
}
+
+ /// Create a new [`GetOptions`]
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ /// Sets the `if_match` condition.
+ ///
+ /// See [`GetOptions::if_match`]
+ #[must_use]
+ pub fn with_if_match(mut self, etag: Option<impl Into<String>>) -> Self {
+ self.if_match = etag.map(Into::into);
+ self
+ }
+
+ /// Sets the `if_none_match` condition.
+ ///
+ /// See [`GetOptions::if_none_match`]
+ #[must_use]
+ pub fn with_if_none_match(mut self, etag: Option<impl Into<String>>) ->
Self {
+ self.if_none_match = etag.map(Into::into);
+ self
+ }
+
+ /// Sets the `if_modified_since` condition.
+ ///
+ /// See [`GetOptions::if_modified_since`]
+ #[must_use]
+ pub fn with_if_modified_since(mut self, dt: Option<impl
Into<DateTime<Utc>>>) -> Self {
+ self.if_modified_since = dt.map(Into::into);
+ self
+ }
+
+ /// Sets the `if_unmodified_since` condition.
+ ///
+ /// See [`GetOptions::if_unmodified_since`]
+ #[must_use]
+ pub fn with_if_unmodified_since(mut self, dt: Option<impl
Into<DateTime<Utc>>>) -> Self {
+ self.if_unmodified_since = dt.map(Into::into);
+ self
+ }
+
+ /// Sets the `range` condition.
+ ///
+ /// See [`GetOptions::range`]
+ #[must_use]
+ pub fn with_range(mut self, range: Option<impl Into<GetRange>>) -> Self {
+ self.range = range.map(Into::into);
+ self
+ }
+
+ /// Sets the `version` condition.
+ ///
+ /// See [`GetOptions::version`]
+ #[must_use]
+ pub fn with_version(mut self, version: Option<impl Into<String>>) -> Self {
+ self.version = version.map(Into::into);
+ self
+ }
+
+ /// Sets the `head` condition.
+ ///
+ /// See [`GetOptions::head`]
+ #[must_use]
+ pub fn with_head(mut self, head: impl Into<bool>) -> Self {
+ self.head = head.into();
+ self
+ }
+
+ /// Sets the `extensions` condition.
+ ///
+ /// See [`GetOptions::extensions`]
+ #[must_use]
+ pub fn with_extensions(mut self, extensions: Extensions) -> Self {
+ self.extensions = extensions;
+ self
+ }
}
/// Result for a get request
@@ -1670,4 +1941,41 @@ mod tests {
extensions.insert("test-key");
assert!(extensions.get::<&str>().is_some());
}
+
+ #[test]
+ fn test_get_options_builder() {
+ let dt = Utc::now();
+ let extensions = Extensions::new();
+
+ let options = GetOptions::new();
+
+ // assert defaults
+ assert_eq!(options.if_match, None);
+ assert_eq!(options.if_none_match, None);
+ assert_eq!(options.if_modified_since, None);
+ assert_eq!(options.if_unmodified_since, None);
+ assert_eq!(options.range, None);
+ assert_eq!(options.version, None);
+ assert!(!options.head);
+ assert!(options.extensions.get::<&str>().is_none());
+
+ let options = options
+ .with_if_match(Some("etag-match"))
+ .with_if_none_match(Some("etag-none-match"))
+ .with_if_modified_since(Some(dt))
+ .with_if_unmodified_since(Some(dt))
+ .with_range(Some(0..100))
+ .with_version(Some("version-1"))
+ .with_head(true)
+ .with_extensions(extensions.clone());
+
+ assert_eq!(options.if_match, Some("etag-match".to_string()));
+ assert_eq!(options.if_none_match, Some("etag-none-match".to_string()));
+ assert_eq!(options.if_modified_since, Some(dt));
+ assert_eq!(options.if_unmodified_since, Some(dt));
+ assert_eq!(options.range, Some(GetRange::Bounded(0..100)));
+ assert_eq!(options.version, Some("version-1".to_string()));
+ assert!(options.head);
+ assert_eq!(options.extensions.get::<&str>(), extensions.get::<&str>());
+ }
}
diff --git a/tests/get_range_file.rs b/tests/get_range_file.rs
index d5ac8e3..04317af 100644
--- a/tests/get_range_file.rs
+++ b/tests/get_range_file.rs
@@ -115,11 +115,36 @@ async fn test_get_opts_over_range() {
let expected = Bytes::from_static(b"hello world");
store.put(&path, expected.clone().into()).await.unwrap();
- let opts = GetOptions {
- range: Some(GetRange::Bounded(0..(expected.len() as u64 * 2))),
- ..Default::default()
- };
+ let opts =
+ GetOptions::new().with_range(Some(GetRange::Bounded(0..(expected.len()
as u64 * 2))));
let res = store.get_opts(&path, opts).await.unwrap();
assert_eq!(res.range, 0..expected.len() as u64);
assert_eq!(res.bytes().await.unwrap(), expected);
}
+
+#[tokio::test]
+async fn test_get_range_opts_with_etag() {
+ let tmp = tempdir().unwrap();
+ let store = MyStore(LocalFileSystem::new_with_prefix(tmp.path()).unwrap());
+ let path = Path::from("foo");
+
+ let expected = Bytes::from_static(b"hello world");
+ store.put(&path, expected.clone().into()).await.unwrap();
+
+ // pull the file to get the etag
+ let file = store.get(&path).await.unwrap();
+ let etag = file.meta.e_tag.clone().unwrap();
+
+ let opts = GetOptions::new()
+ .with_if_match(Some(etag))
+ .with_range(Some(0..(expected.len() as u64 * 2)));
+ let res = store.get_opts(&path, opts).await.unwrap();
+ assert_eq!(res.bytes().await.unwrap(), expected);
+
+ // pulling a file with an invalid etag should fail
+ let opts = GetOptions::new()
+ .with_if_match(Some("invalid-etag"))
+ .with_range(Some(0..(expected.len() as u64 * 2)));
+ let err = store.get_opts(&path, opts).await;
+ assert!(err.is_err());
+}
diff --git a/tests/http.rs b/tests/http.rs
index cb0b7d6..6c41792 100644
--- a/tests/http.rs
+++ b/tests/http.rs
@@ -36,10 +36,7 @@ async fn test_http_store_gzip() {
let _ = http_store
.get_opts(
&Path::parse("LICENSE.txt").unwrap(),
- GetOptions {
- range: Some(GetRange::Bounded(0..100)),
- ..Default::default()
- },
+ GetOptions::new().with_range(Some(GetRange::Bounded(0..100))),
)
.await
.unwrap();
@@ -56,10 +53,7 @@ async fn basic_wasm_get() {
let _ = http_store
.get_opts(
&Path::parse("LICENSE.txt").unwrap(),
- GetOptions {
- range: Some(GetRange::Bounded(0..100)),
- ..Default::default()
- },
+ GetOptions::new().with_range(Some(GetRange::Bounded(0..100))),
)
.await
.unwrap();