Skip to content

Commit

Permalink
actions: add CopyObjects action
Browse files Browse the repository at this point in the history
  • Loading branch information
Sporif committed Jan 26, 2022
1 parent 9e10f5f commit 1055109
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 2 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ More examples can be found in the examples directory on GitHub.
* [`HeadObject`][headobject]
* [`GetObject`][getobject]
* [`PutObject`][putobject]
* [`CopyObject`][copyobject]
* [`DeleteObject`][deleteobject]
* [`DeleteObjects`][deleteobjects]
* [`ListObjectsV2`][listobjectsv2]
Expand All @@ -67,6 +68,7 @@ More examples can be found in the examples directory on GitHub.
[createbucket]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateBucket.html
[deletebucket]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucket.html
[createmultipart]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html
[copyobject]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
[deleteobject]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html
[deleteobjects]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
[getobject]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
Expand Down
151 changes: 151 additions & 0 deletions src/actions/copy_object.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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};

/// Create a copy of an object that is already stored in S3, using a `PUT` request.
///
/// Find out more about CopyObject from the [AWS API Reference][api]
///
/// [api]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
#[derive(Debug, Clone)]
pub struct CopyObject<'a> {
bucket: &'a Bucket,
credentials: Option<&'a Credentials>,
src_object: &'a str,
dst_object: &'a str,

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

impl<'a> CopyObject<'a> {
#[inline]
pub fn new(
bucket: &'a Bucket,
credentials: Option<&'a Credentials>,
src_object: &'a str,
dst_object: &'a str,
) -> Self {
let mut query = Map::new();
query.insert(
"x-amz-copy-source",
format!("{}/{}", bucket.name(), src_object),
);

Self {
bucket,
credentials,
src_object,
dst_object,

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

impl<'a> S3Action<'a> for CopyObject<'a> {
const METHOD: Method = Method::Put;

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
}

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

match self.credentials {
Some(credentials) => sign(
time,
Method::Put,
url,
credentials.key(),
credentials.secret(),
credentials.token(),
self.bucket.region(),
expires_in.as_secs(),
self.query.iter(),
self.headers.iter(),
),
None => {
url.query_pairs_mut().append_pair(
"x-amz-copy-source",
format!("{}/{}", self.bucket.name(), self.src_object).as_str(),
);
url
}
}
}
}

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

use pretty_assertions::assert_eq;

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

#[test]
fn aws_example() {
// Fri, 24 May 2013 00:00:00 GMT
let date = OffsetDateTime::from_unix_timestamp(1369353600).unwrap();
let expires_in = Duration::from_secs(86400);

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

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

let url = action.sign_with_time(expires_in, &date);
let expected = "https://examplebucket.s3.amazonaws.com/test_copy.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-copy-source=examplebucket%2Ftest.txt&X-Amz-Signature=760326dbb90c424f6b5dcfa5f8473754f44cb4c05c173416feb1b9306dc64d35";

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,
UrlStyle::VirtualHost,
"examplebucket",
"us-east-1",
)
.unwrap();

let action = CopyObject::new(&bucket, None, "test.txt", "test_copy.txt");
let url = action.sign(expires_in);
let expected = "https://examplebucket.s3.amazonaws.com/test_copy.txt?x-amz-copy-source=examplebucket%2Ftest.txt";

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 @@ -4,6 +4,7 @@ use std::time::Duration;

use url::Url;

pub use self::copy_object::CopyObject;
pub use self::create_bucket::CreateBucket;
pub use self::delete_bucket::DeleteBucket;
pub use self::delete_object::DeleteObject;
Expand All @@ -25,6 +26,7 @@ pub use self::multipart_upload::upload::UploadPart;
pub use self::put_object::PutObject;
use crate::{Map, Method};

mod copy_object;
mod create_bucket;
mod delete_bucket;
mod delete_object;
Expand Down
16 changes: 14 additions & 2 deletions src/bucket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use std::fmt::{self, Display};
use url::{ParseError, Url};

use crate::actions::{
AbortMultipartUpload, CreateBucket, DeleteBucket, DeleteObject, GetObject, HeadObject,
PutObject, UploadPart,
AbortMultipartUpload, CopyObject, CreateBucket, DeleteBucket, DeleteObject, GetObject,
HeadObject, PutObject, UploadPart,
};
#[cfg(feature = "full")]
use crate::actions::{
Expand Down Expand Up @@ -207,6 +207,18 @@ impl Bucket {
PutObject::new(self, credentials, object)
}

/// Create a copy of an object in S3, using a `PUT` request.
///
/// See [`CopyObject`] for more details.
pub fn copy_object<'a>(
&'a self,
credentials: Option<&'a Credentials>,
src_object: &'a str,
dst_object: &'a str,
) -> CopyObject<'a> {
CopyObject::new(self, credentials, src_object, dst_object)
}

/// Delete an object from S3, using a `DELETE` request.
///
/// See [`DeleteObject`] for more details.
Expand Down
65 changes: 65 additions & 0 deletions tests/copy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::time::Duration;

use rusty_s3::actions::{ListObjectsV2, S3Action};

mod common;

#[tokio::test]
async fn test_copy() {
let (bucket, credentials, client) = common::bucket().await;

let action = bucket.list_objects_v2(Some(&credentials));
let url = action.sign(Duration::from_secs(60));
let resp = client
.get(url)
.send()
.await
.expect("send ListObjectsV2")
.error_for_status()
.expect("ListObjectsV2 unexpected status code");
let text = resp.text().await.expect("ListObjectsV2 read respose body");
let list = ListObjectsV2::parse_response(&text).expect("ListObjectsV2 parse response");

assert!(list.contents.is_empty());

assert_eq!(list.max_keys, 4500);
assert!(list.common_prefixes.is_empty());
assert!(list.next_continuation_token.is_none());
assert!(list.start_after.is_none());

let body = vec![b'r'; 1024];

let action = bucket.put_object(Some(&credentials), "test.txt");
let url = action.sign(Duration::from_secs(60));
client
.put(url)
.body(body.clone())
.send()
.await
.expect("send PutObject")
.error_for_status()
.expect("PutObject unexpected status code");

let action = bucket.copy_object(Some(&credentials), "test.txt", "test_copy.txt");
let url = action.sign(Duration::from_secs(60));
client
.put(url)
.send()
.await
.expect("send CopyObject")
.error_for_status()
.expect("CopyObject unexpected status code");

let action = bucket.list_objects_v2(Some(&credentials));
let url = action.sign(Duration::from_secs(60));
let resp = client
.get(url)
.send()
.await
.expect("send ListObjectsV2")
.error_for_status()
.expect("ListObjectsV2 unexpected status code");
let text = resp.text().await.expect("ListObjectsV2 read respose body");
let list = ListObjectsV2::parse_response(&text).expect("ListObjectsV2 parse response");
assert_eq!(list.contents.len(), 2);
}

0 comments on commit 1055109

Please sign in to comment.