use aws_sdk_s3::{
error::SdkError,
operation::head_object::HeadObjectError,
primitives::{ByteStream, DateTime, DateTimeFormat},
Client, Error,
};
use tracing::{error, warn};
const KEY: &str = "key";
const BODY: &str = "Hello, world!";
/// Demonstrate how `if-modified-since` reports that matching objects haven't
/// changed.
///
/// # Steps
/// - Create a bucket.
/// - Put an object in the bucket.
/// - Get the bucket headers.
/// - Get the bucket headers again but only if modified.
/// - Delete the bucket.
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt::init();
// Get a new UUID to use when creating a unique bucket name.
let uuid = uuid::Uuid::new_v4();
// Load the AWS configuration from the environment.
let client = Client::new(&aws_config::load_from_env().await);
// Generate a unique bucket name using the previously generated UUID.
// Then create a new bucket with that name.
let bucket_name = format!("if-modified-since-{uuid}");
client
.create_bucket()
.bucket(bucket_name.clone())
.send()
.await?;
// Create a new object in the bucket whose name is `KEY` and whose
// contents are `BODY`.
let put_object_output = client
.put_object()
.bucket(bucket_name.as_str())
.key(KEY)
.body(ByteStream::from_static(BODY.as_bytes()))
.send()
.await;
// If the `PutObject` succeeded, get the eTag string from it. Otherwise,
// report an error and return an empty string.
let e_tag_1 = match put_object_output {
Ok(put_object) => put_object.e_tag.unwrap(),
Err(err) => {
error!("{err:?}");
String::new()
}
};
// Request the object's headers.
let head_object_output = client
.head_object()
.bucket(bucket_name.as_str())
.key(KEY)
.send()
.await;
// If the `HeadObject` request succeeded, create a tuple containing the
// values of the headers `last-modified` and `etag`. If the request
// failed, return the error in a tuple instead.
let (last_modified, e_tag_2) = match head_object_output {
Ok(head_object) => (
Ok(head_object.last_modified().cloned().unwrap()),
head_object.e_tag.unwrap(),
),
Err(err) => (Err(err), String::new()),
};
warn!("last modified: {last_modified:?}");
assert_eq!(
e_tag_1, e_tag_2,
"PutObject and first GetObject had differing eTags"
);
println!("First value of last_modified: {last_modified:?}");
println!("First tag: {}\n", e_tag_1);
// Send a second `HeadObject` request. This time, the `if_modified_since`
// option is specified, giving the `last_modified` value returned by the
// first call to `HeadObject`.
//
// Since the object hasn't been changed, and there are no other objects in
// the bucket, there should be no matching objects.
let head_object_output = client
.head_object()
.bucket(bucket_name.as_str())
.key(KEY)
.if_modified_since(last_modified.unwrap())
.send()
.await;
// If the `HeadObject` request succeeded, the result is a typle containing
// the `last_modified` and `e_tag_1` properties. This is _not_ the expected
// result.
//
// The _expected_ result of the second call to `HeadObject` is an
// `SdkError::ServiceError` containing the HTTP error response. If that's
// the case and the HTTP status is 304 (not modified), the output is a
// tuple containing the values of the HTTP `last-modified` and `etag`
// headers.
//
// If any other HTTP error occurred, the error is returned as an
// `SdkError::ServiceError`.
let (last_modified, e_tag_2): (Result<DateTime, SdkError<HeadObjectError>>, String) =
match head_object_output {
Ok(head_object) => (
Ok(head_object.last_modified().cloned().unwrap()),
head_object.e_tag.unwrap(),
),
Err(err) => match err {
SdkError::ServiceError(err) => {
// Get the raw HTTP response. If its status is 304, the
// object has not changed. This is the expected code path.
let http = err.raw();
match http.status().as_u16() {
// If the HTTP status is 304: Not Modified, return a
// tuple containing the values of the HTTP
// `last-modified` and `etag` headers.
304 => (
Ok(DateTime::from_str(
http.headers().get("last-modified").unwrap(),
DateTimeFormat::HttpDate,
)
.unwrap()),
http.headers().get("etag").map(|t| t.into()).unwrap(),
),
// Any other HTTP status code is returned as an
// `SdkError::ServiceError`.
_ => (Err(SdkError::ServiceError(err)), String::new()),
}
}
// Any other kind of error is returned in a tuple containing the
// error and an empty string.
_ => (Err(err), String::new()),
},
};
warn!("last modified: {last_modified:?}");
assert_eq!(
e_tag_1, e_tag_2,
"PutObject and second HeadObject had different eTags"
);
println!("Second value of last modified: {last_modified:?}");
println!("Second tag: {}", e_tag_2);
// Clean up by deleting the object and the bucket.
client
.delete_object()
.bucket(bucket_name.as_str())
.key(KEY)
.send()
.await?;
client
.delete_bucket()
.bucket(bucket_name.as_str())
.send()
.await?;
Ok(())
}