Skip to content

Commit

Permalink
feat(core): implement if_modified_since and if_unmodified_since for s…
Browse files Browse the repository at this point in the history
…tat_with (#5528)
  • Loading branch information
meteorgan authored Jan 9, 2025
1 parent 90c64a1 commit 86ba75d
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 3 deletions.
24 changes: 24 additions & 0 deletions core/src/raw/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,8 @@ impl OpReader {
pub struct OpStat {
if_match: Option<String>,
if_none_match: Option<String>,
if_modified_since: Option<DateTime<Utc>>,
if_unmodified_since: Option<DateTime<Utc>>,
override_content_type: Option<String>,
override_cache_control: Option<String>,
override_content_disposition: Option<String>,
Expand Down Expand Up @@ -545,6 +547,28 @@ impl OpStat {
self.if_none_match.as_deref()
}

/// Set the If-Modified-Since of the option
pub fn with_if_modified_since(mut self, v: DateTime<Utc>) -> Self {
self.if_modified_since = Some(v);
self
}

/// Get If-Modified-Since from option
pub fn if_modified_since(&self) -> Option<DateTime<Utc>> {
self.if_modified_since
}

/// Set the If-Unmodified-Since of the option
pub fn with_if_unmodified_since(mut self, v: DateTime<Utc>) -> Self {
self.if_unmodified_since = Some(v);
self
}

/// Get If-Unmodified-Since from option
pub fn if_unmodified_since(&self) -> Option<DateTime<Utc>> {
self.if_unmodified_since
}

/// Sets the content-disposition header that should be sent back by the remote read operation.
pub fn with_override_content_disposition(mut self, content_disposition: &str) -> Self {
self.override_content_disposition = Some(content_disposition.into());
Expand Down
2 changes: 2 additions & 0 deletions core/src/services/s3/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,8 @@ impl Access for S3Backend {
stat_has_content_encoding: true,
stat_with_if_match: true,
stat_with_if_none_match: true,
stat_with_if_modified_since: true,
stat_with_if_unmodified_since: true,
stat_with_override_cache_control: !self.core.disable_stat_with_override,
stat_with_override_content_disposition: !self.core.disable_stat_with_override,
stat_with_override_content_type: !self.core.disable_stat_with_override,
Expand Down
14 changes: 13 additions & 1 deletion core/src/services/s3/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,11 +338,23 @@ impl S3Core {
if let Some(if_none_match) = args.if_none_match() {
req = req.header(IF_NONE_MATCH, if_none_match);
}

if let Some(if_match) = args.if_match() {
req = req.header(IF_MATCH, if_match);
}

if let Some(if_modified_since) = args.if_modified_since() {
req = req.header(
IF_MODIFIED_SINCE,
format_datetime_into_http_date(if_modified_since),
);
}
if let Some(if_unmodified_since) = args.if_unmodified_since() {
req = req.header(
IF_UNMODIFIED_SINCE,
format_datetime_into_http_date(if_unmodified_since),
);
}

let req = req.body(Buffer::new()).map_err(new_request_build_error)?;

Ok(req)
Expand Down
4 changes: 4 additions & 0 deletions core/src/types/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ pub struct Capability {
pub stat_with_if_match: bool,
/// Indicates if conditional stat operations using If-None-Match are supported.
pub stat_with_if_none_match: bool,
/// Indicates if conditional stat operations using If-Modified-Since are supported.
pub stat_with_if_modified_since: bool,
/// Indicates if conditional stat operations using If-Unmodified-Since are supported.
pub stat_with_if_unmodified_since: bool,
/// Indicates if Cache-Control header override is supported during stat operations.
pub stat_with_override_cache_control: bool,
/// Indicates if Content-Disposition header override is supported during stat operations.
Expand Down
44 changes: 42 additions & 2 deletions core/src/types/operator/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ impl Operator {
///
/// This feature can be used to check if the file's `ETag` matches the given `ETag`.
///
/// If file exists and it's etag doesn't match, an error with kind [`ErrorKind::ConditionNotMatch`]
/// If file exists, and it's etag doesn't match, an error with kind [`ErrorKind::ConditionNotMatch`]
/// will be returned.
///
/// ```
Expand All @@ -276,7 +276,7 @@ impl Operator {
///
/// This feature can be used to check if the file's `ETag` doesn't match the given `ETag`.
///
/// If file exists and it's etag match, an error with kind [`ErrorKind::ConditionNotMatch`]
/// If file exists, and it's etag match, an error with kind [`ErrorKind::ConditionNotMatch`]
/// will be returned.
///
/// ```
Expand All @@ -289,6 +289,46 @@ impl Operator {
/// # }
/// ```
///
/// ## `if_modified_since`
///
/// set `if_modified_since` for this `stat` request.
///
/// This feature can be used to check if the file has been modified since the given time.
///
/// If file exists, and it's not modified after the given time, an error with kind [`ErrorKind::ConditionNotMatch`]
/// will be returned.
///
/// ```
/// # use opendal::Result;
/// use opendal::Operator;
/// use chrono::Utc;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut metadata = op.stat_with("path/to/file").if_modified_since(Utc::now()).await?;
/// # Ok(())
/// # }
/// ```
///
/// ## `if_unmodified_since`
///
/// set `if_unmodified_since` for this `stat` request.
///
/// This feature can be used to check if the file has NOT been modified since the given time.
///
/// If file exists, and it's modified after the given time, an error with kind [`ErrorKind::ConditionNotMatch`]
/// will be returned.
///
/// ```
/// # use opendal::Result;
/// use opendal::Operator;
/// use chrono::Utc;
///
/// # async fn test(op: Operator) -> Result<()> {
/// let mut metadata = op.stat_with("path/to/file").if_unmodified_since(Utc::now()).await?;
/// # Ok(())
/// # }
/// ```
///
/// ## `version`
///
/// Set `version` for this `stat` request.
Expand Down
10 changes: 10 additions & 0 deletions core/src/types/operator/operator_futures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,16 @@ impl<F: Future<Output = Result<Metadata>>> FutureStat<F> {
self.map(|args| args.with_if_none_match(v))
}

/// Set the If-Modified-Since for this operation.
pub fn if_modified_since(self, v: DateTime<Utc>) -> Self {
self.map(|args| args.with_if_modified_since(v))
}

/// Set the If-Unmodified-Since for this operation.
pub fn if_unmodified_since(self, v: DateTime<Utc>) -> Self {
self.map(|args| args.with_if_unmodified_since(v))
}

/// Set the version for this operation.
pub fn version(self, v: &str) -> Self {
self.map(|args| args.with_version(v))
Expand Down
63 changes: 63 additions & 0 deletions core/tests/behavior/async_stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use anyhow::Result;
use http::StatusCode;
use log::warn;
use reqwest::Url;
use tokio::time::sleep;

pub fn tests(op: &Operator, tests: &mut Vec<Trial>) {
let cap = op.info().full_capability();
Expand All @@ -38,6 +39,8 @@ pub fn tests(op: &Operator, tests: &mut Vec<Trial>) {
test_stat_not_exist,
test_stat_with_if_match,
test_stat_with_if_none_match,
test_stat_with_if_modified_since,
test_stat_with_if_unmodified_since,
test_stat_with_override_cache_control,
test_stat_with_override_content_disposition,
test_stat_with_override_content_type,
Expand Down Expand Up @@ -244,6 +247,66 @@ pub async fn test_stat_with_if_none_match(op: Operator) -> Result<()> {
Ok(())
}

/// Stat file with if_modified_since should succeed, otherwise get a ConditionNotMatch error.
pub async fn test_stat_with_if_modified_since(op: Operator) -> Result<()> {
if !op.info().full_capability().stat_with_if_modified_since {
return Ok(());
}

let (path, content, _) = TEST_FIXTURE.new_file(op.clone());

op.write(&path, content.clone())
.await
.expect("write must succeed");

let meta = op.stat(&path).await?;
assert_eq!(meta.mode(), EntryMode::FILE);
assert_eq!(meta.content_length(), content.len() as u64);

let since = meta.last_modified().unwrap() - Duration::from_secs(1);
let res = op.stat_with(&path).if_modified_since(since).await?;
assert_eq!(res.last_modified(), meta.last_modified());

sleep(Duration::from_secs(1)).await;

let since = meta.last_modified().unwrap() + Duration::from_secs(1);
let res = op.stat_with(&path).if_modified_since(since).await;
assert!(res.is_err());
assert_eq!(res.err().unwrap().kind(), ErrorKind::ConditionNotMatch);

Ok(())
}

/// Stat file with if_unmodified_since should succeed, otherwise get a ConditionNotMatch error.
pub async fn test_stat_with_if_unmodified_since(op: Operator) -> Result<()> {
if !op.info().full_capability().stat_with_if_unmodified_since {
return Ok(());
}

let (path, content, _) = TEST_FIXTURE.new_file(op.clone());

op.write(&path, content.clone())
.await
.expect("write must succeed");

let meta = op.stat(&path).await?;
assert_eq!(meta.mode(), EntryMode::FILE);
assert_eq!(meta.content_length(), content.len() as u64);

let since = meta.last_modified().unwrap() - Duration::from_secs(1);
let res = op.stat_with(&path).if_unmodified_since(since).await;
assert!(res.is_err());
assert_eq!(res.err().unwrap().kind(), ErrorKind::ConditionNotMatch);

sleep(Duration::from_secs(1)).await;

let since = meta.last_modified().unwrap() + Duration::from_secs(1);
let res = op.stat_with(&path).if_unmodified_since(since).await?;
assert_eq!(res.last_modified(), meta.last_modified());

Ok(())
}

/// Stat file with override-cache-control should succeed.
pub async fn test_stat_with_override_cache_control(op: Operator) -> Result<()> {
if !(op.info().full_capability().stat_with_override_cache_control
Expand Down

0 comments on commit 86ba75d

Please sign in to comment.