Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

actions: add HeadObject action #18

Merged
merged 5 commits into from
Jun 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ More examples can be found in the examples directory on GitHub.
* [`CreateBucket`][createbucket]
* [`DeleteBucket`][deletebucket]
* Basic methods
* [`HeadObject`][headobject]
* [`GetObject`][getobject]
* [`PutObject`][putobject]
* [`DeleteObject`][deleteobject]
Expand All @@ -65,6 +66,7 @@ More examples can be found in the examples directory on GitHub.
[createmultipart]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html
[deleteobject]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
[getobject]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
[headobject]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html
[listobjectsv2]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html
[putobject]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
[uploadpart]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPart
157 changes: 157 additions & 0 deletions src/actions/head_object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
use std::time::Duration;

use time::OffsetDateTime;
use url::Url;

use super::S3Action;
use crate::actions::Method;
use crate::signing::sign;
use crate::{Bucket, Credentials, Map};

/// Retrieve an object's metadata from S3, using a `HEAD` request.
///
/// Find out more about `HeadObject` from the [AWS API Reference][api]
///
/// [api]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html
#[derive(Debug, Clone)]
pub struct HeadObject<'a> {
bucket: &'a Bucket,
credentials: Option<&'a Credentials>,
object: &'a str,

query: Map<'a>,
headers: Map<'a>,
}

impl<'a> HeadObject<'a> {
#[inline]
pub fn new(bucket: &'a Bucket, credentials: Option<&'a Credentials>, object: &'a str) -> Self {
Self {
bucket,
credentials,
object,

query: Map::new(),
headers: Map::new(),
}
}

fn sign_with_time(&self, expires_in: Duration, time: &OffsetDateTime) -> Url {
let url = self.bucket.object_url(self.object).unwrap();

match self.credentials {
Some(credentials) => sign(
time,
Method::Head,
url,
credentials.key(),
credentials.secret(),
credentials.token(),
self.bucket.region(),
expires_in.as_secs(),
self.query.iter(),
self.headers.iter(),
),
None => crate::signing::util::add_query_params(url, self.query.iter()),
}
}
}

impl<'a> S3Action<'a> for HeadObject<'a> {
const METHOD: Method = Method::Head;

fn sign(&self, expires_in: Duration) -> Url {
let now = OffsetDateTime::now_utc();
self.sign_with_time(expires_in, &now)
}

fn query_mut(&mut self) -> &mut Map<'a> {
&mut self.query
}

fn headers_mut(&mut self) -> &mut Map<'a> {
&mut self.headers
}
}

#[cfg(test)]
mod tests {
use time::PrimitiveDateTime;

use pretty_assertions::assert_eq;

use super::*;
use crate::{Bucket, Credentials};

#[test]
fn aws_example() {
let date = PrimitiveDateTime::parse(
"Fri, 24 May 2013 00:00:00 GMT",
"%a, %d %b %Y %-H:%M:%S GMT",
)
.unwrap()
.assume_utc();
let expires_in = Duration::from_secs(86400);

let endpoint = "https://s3.amazonaws.com".parse().unwrap();
let bucket = Bucket::new(endpoint, false, "examplebucket", "us-east-1").unwrap();
let credentials = Credentials::new(
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
);

let action = HeadObject::new(&bucket, Some(&credentials), "test.txt");

let url = action.sign_with_time(expires_in, &date);
let expected = "https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=f9c58dec0c3cada1e6f133547c7b6b2ef9d7df87447a785ad1b23079005271e5";

assert_eq!(expected, url.as_str());
}

#[test]
fn aws_example_custom_query() {
let date = PrimitiveDateTime::parse(
"Fri, 24 May 2013 00:00:00 GMT",
"%a, %d %b %Y %-H:%M:%S GMT",
)
.unwrap()
.assume_utc();
let expires_in = Duration::from_secs(86400);

let endpoint = "https://s3.amazonaws.com".parse().unwrap();
let bucket = Bucket::new(endpoint, false, "examplebucket", "us-east-1").unwrap();
let credentials = Credentials::new(
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
);

let mut action = HeadObject::new(&bucket, Some(&credentials), "test.txt");
action
.query_mut()
.insert("response-content-type", "text/plain");

let url = action.sign_with_time(expires_in, &date);
let expected = "https://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&response-content-type=text%2Fplain&X-Amz-Signature=cbdb1e433786bd2f0dc61c3ad4d3a32687c9a1a7e8c6ee170a2ea805c59247f9";

assert_eq!(expected, url.as_str());
}

#[test]
fn anonymous_custom_query() {
let expires_in = Duration::from_secs(86400);

let endpoint = "https://s3.amazonaws.com".parse().unwrap();
let bucket = Bucket::new(endpoint, false, "examplebucket", "us-east-1").unwrap();

let mut action = HeadObject::new(&bucket, None, "test.txt");
action
.query_mut()
.insert("response-content-type", "text/plain");

let url = action.sign(expires_in);
let expected =
"https://examplebucket.s3.amazonaws.com/test.txt?response-content-type=text%2Fplain";

assert_eq!(expected, url.as_str());
}
}
2 changes: 2 additions & 0 deletions src/actions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub use self::create_bucket::CreateBucket;
pub use self::delete_bucket::DeleteBucket;
pub use self::delete_object::DeleteObject;
pub use self::get_object::GetObject;
pub use self::head_object::HeadObject;
#[doc(inline)]
pub use self::list_objects_v2::{ListObjectsV2, ListObjectsV2Response};
pub use self::multipart_upload::abort::AbortMultipartUpload;
Expand All @@ -21,6 +22,7 @@ mod create_bucket;
mod delete_bucket;
mod delete_object;
mod get_object;
mod head_object;
pub mod list_objects_v2;
mod multipart_upload;
mod put_object;
Expand Down
15 changes: 14 additions & 1 deletion src/bucket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use url::{ParseError, Url};

use crate::actions::{
AbortMultipartUpload, CompleteMultipartUpload, CreateBucket, CreateMultipartUpload,
DeleteBucket, DeleteObject, GetObject, ListObjectsV2, PutObject, UploadPart,
DeleteBucket, DeleteObject, GetObject, HeadObject, ListObjectsV2, PutObject, UploadPart,
};
use crate::signing::util::percent_encode_path;
use crate::Credentials;
Expand Down Expand Up @@ -128,6 +128,17 @@ impl Bucket {
// === Basic actions ===

impl Bucket {
/// Retrieve an object's metadata from S3, using a `HEAD` request.
///
/// See [`HeadObject`] for more details.
pub fn head_object<'a>(
&'a self,
credentials: Option<&'a Credentials>,
object: &'a str,
) -> HeadObject<'a> {
HeadObject::new(self, credentials, object)
}

/// Retrieve an object from S3, using a `GET` request.
///
/// See [`GetObject`] for more details.
Expand Down Expand Up @@ -311,7 +322,9 @@ mod tests {
);

let _ = bucket.create_bucket(&credentials);
let _ = bucket.delete_bucket(&credentials);

let _ = bucket.head_object(Some(&credentials), "duck.jpg");
let _ = bucket.get_object(Some(&credentials), "duck.jpg");
let _ = bucket.list_objects_v2(Some(&credentials));
let _ = bucket.put_object(Some(&credentials), "duck.jpg");
Expand Down
2 changes: 1 addition & 1 deletion src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl Method {
/// assert_eq!(Method::Get.to_str(), "GET");
/// ```
#[inline]
pub fn to_str(&self) -> &'static str {
pub fn to_str(self) -> &'static str {
match self {
Self::Head => "HEAD",
Self::Get => "GET",
Expand Down
19 changes: 19 additions & 0 deletions tests/upload_download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@ async fn test1() {
.error_for_status()
.expect("PutObject unexpected status code");

let action = bucket.head_object(Some(&credentials), "test.txt");
let url = action.sign(Duration::from_secs(60));

let resp = client
.head(url)
.send()
.await
.expect("send HeadObject")
.error_for_status()
.expect("HeadObject unexpected status code");

let content_length = resp
.headers()
.get("content-length")
.expect("Content-Length header")
.to_str()
.expect("Content-Length to_str()");
assert_eq!(content_length, "1024");

let action = bucket.get_object(Some(&credentials), "test.txt");
let url = action.sign(Duration::from_secs(60));

Expand Down