From 07cf14f7b15a2857a01a34db41ea50c66ed8951b Mon Sep 17 00:00:00 2001 From: Peter Borkuti Date: Sun, 19 Mar 2023 20:18:59 +0100 Subject: [PATCH 1/3] s3 example - thumbnail creator (#613) --- examples/basic-s3-thumbnail/Cargo.toml | 32 +++ examples/basic-s3-thumbnail/README.md | 16 ++ examples/basic-s3-thumbnail/src/main.rs | 227 ++++++++++++++++++++ examples/basic-s3-thumbnail/src/s3client.rs | 78 +++++++ 4 files changed, 353 insertions(+) create mode 100644 examples/basic-s3-thumbnail/Cargo.toml create mode 100644 examples/basic-s3-thumbnail/README.md create mode 100644 examples/basic-s3-thumbnail/src/main.rs create mode 100644 examples/basic-s3-thumbnail/src/s3client.rs diff --git a/examples/basic-s3-thumbnail/Cargo.toml b/examples/basic-s3-thumbnail/Cargo.toml new file mode 100644 index 00000000..797f3338 --- /dev/null +++ b/examples/basic-s3-thumbnail/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "basic-s3-thumbnail" +version = "0.1.0" +edition = "2021" + +# Starting in Rust 1.62 you can use `cargo add` to add dependencies +# to your project. +# +# If you're using an older Rust version, +# download cargo-edit(https://github.com/killercup/cargo-edit#installation) +# to install the `add` subcommand. +# +# Running `cargo add DEPENDENCY_NAME` will +# add the latest version of a dependency to the list, +# and it will keep the alphabetic ordering for you. + +[dependencies] +aws_lambda_events = "0.7.2" +lambda_runtime = { path = "../../lambda-runtime" } +serde = "1.0.136" +tokio = { version = "1", features = ["macros"] } +tracing = { version = "0.1" } +tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] } +aws-config = "0.54.1" +aws-sdk-s3 = "0.24.0" +thumbnailer = "0.4.0" +mime = "0.3.16" +async-trait = "0.1.66" + +[dev-dependencies] +mockall = "0.11.3" +tokio-test = "0.4.2" diff --git a/examples/basic-s3-thumbnail/README.md b/examples/basic-s3-thumbnail/README.md new file mode 100644 index 00000000..de2d56f8 --- /dev/null +++ b/examples/basic-s3-thumbnail/README.md @@ -0,0 +1,16 @@ +# AWS Lambda Function that uses S3 + +This example processes S3 events. If the event is a CREATE event, +it downloads the created file, generates a thumbnail from it +(it assumes that the file is an image) and uploads it to S3 into a bucket named +[original-bucket-name]-thumbs. + +## Build & Deploy + +1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation) +2. Build the function with `cargo lambda build --release` +3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE` + +## Build for ARM 64 + +Build the function with `cargo lambda build --release --arm64` \ No newline at end of file diff --git a/examples/basic-s3-thumbnail/src/main.rs b/examples/basic-s3-thumbnail/src/main.rs new file mode 100644 index 00000000..6af7f463 --- /dev/null +++ b/examples/basic-s3-thumbnail/src/main.rs @@ -0,0 +1,227 @@ +use aws_config::meta::region::RegionProviderChain; +use aws_lambda_events::{event::s3::S3Event, s3::S3EventRecord}; +use aws_sdk_s3::Client as S3Client; +use lambda_runtime::{run, service_fn, Error, LambdaEvent}; +use s3client::{GetFile, GetThumbnail, PutFile}; + +mod s3client; + +/** +This lambda handler + * listen to file creation events + * downloads the created file + * creates a thumbnail from it + * uploads the thumbnail to bucket "[original bucket name]-thumbs". + +Make sure that + * the created png file has no strange characters in the name + * there is another bucket with "-thumbs" suffix in the name + * this lambda only gets event from png file creation + * this lambda has permission to put file into the "-thumbs" bucket +*/ +pub(crate) async fn function_handler( + event: LambdaEvent, + client: &T, +) -> Result { + let result = Ok("".to_string()); + let records = event.payload.records; + for record in records.iter() { + let (bucket, key) = get_file_props(record); + if bucket.is_empty() || key.is_empty() { + // The event is not a create event or bucket/object key is missing + println!("record skipped"); + continue; + } + + let reader = client.get_file(&bucket, &key).await; + + if reader.is_none() { + continue; + } + + let thumbnail = client.get_thumbnail(reader.unwrap()); + + let mut thumbs_bucket = bucket.to_owned(); + thumbs_bucket.push_str("-thumbs"); + + // It uplaods the thumbnail into a bucket name suffixed with "-thumbs" + // So it needs file creation permission into that bucket + + return client.put_file(&thumbs_bucket, &key, thumbnail).await; + } + + return result; +} + +fn get_file_props(record: &S3EventRecord) -> (String, String) { + let empty_response = ("".to_string(), "".to_string()); + + if record.event_name.is_none() { + return empty_response; + } + if !record.event_name.as_ref().unwrap().starts_with("ObjectCreated") { + return empty_response; + } + + if record.s3.bucket.name.is_none() || record.s3.object.key.is_none() { + return empty_response; + } + + let bucket_name = record.s3.bucket.name.to_owned().unwrap(); + let object_key = record.s3.object.key.to_owned().unwrap(); + + if bucket_name.is_empty() || object_key.is_empty() { + println!("Bucket name or object_key is empty"); + return empty_response; + } + + println!("Bucket: {}, Object key: {}", bucket_name, object_key); + + return (bucket_name, object_key); +} + +async fn get_client() -> S3Client { + let region_provider = RegionProviderChain::default_provider().or_else("us-east-2"); + let config = aws_config::from_env().region(region_provider).load().await; + let client = S3Client::new(&config); + + println!("client region {}", client.conf().region().unwrap().to_string()); + + return client; +} + +#[tokio::main] +async fn main() -> Result<(), Error> { + // required to enable CloudWatch error logging by the runtime + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + // disable printing the name of the module in every log line. + .with_target(false) + // this needs to be set to false, otherwise ANSI color codes will + // show up in a confusing manner in CloudWatch logs. + .with_ansi(false) + // disabling time is handy because CloudWatch will add the ingestion time. + .without_time() + .init(); + + let client = get_client().await; + let client_ref = &client; + + let func = service_fn(move |event| async move { function_handler(event, client_ref).await }); + + run(func).await?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + use std::io::Cursor; + + use super::*; + use async_trait::async_trait; + use aws_lambda_events::chrono::DateTime; + use aws_lambda_events::s3::S3Bucket; + use aws_lambda_events::s3::S3Entity; + use aws_lambda_events::s3::S3Object; + use aws_lambda_events::s3::S3RequestParameters; + use aws_lambda_events::s3::S3UserIdentity; + use aws_sdk_s3::types::ByteStream; + use lambda_runtime::{Context, LambdaEvent}; + use mockall::mock; + use mockall::predicate::eq; + use s3client::GetFile; + use s3client::PutFile; + + #[tokio::test] + async fn response_is_good() { + let mut context = Context::default(); + context.request_id = "test-request-id".to_string(); + + let bucket = "test-bucket"; + let key = "test-key"; + + mock! { + FakeS3Client {} + + #[async_trait] + impl GetFile for FakeS3Client { + pub async fn get_file(&self, bucket: &str, key: &str) -> Option>>; + } + #[async_trait] + impl PutFile for FakeS3Client { + pub async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result; + } + + impl GetThumbnail for FakeS3Client { + fn get_thumbnail(&self, reader: Cursor>) -> ByteStream; + } + } + + let mut mock = MockFakeS3Client::new(); + + mock.expect_get_file() + .withf(|b: &str, k: &str| b.eq(bucket) && k.eq(key)) + .returning(|_1, _2| Some(Cursor::new(b"IMAGE".to_vec()))); + + mock.expect_get_thumbnail() + .with(eq(Cursor::new(b"IMAGE".to_vec()))) + .returning(|_| ByteStream::from_static(b"THUMBNAIL")); + + mock.expect_put_file() + .withf(|bu: &str, ke: &str, _by| bu.eq("test-bucket-thumbs") && ke.eq(key)) + .returning(|_1, _2, _3| Ok("Done".to_string())); + + let payload = get_s3_event("ObjectCreated", bucket, key); + let event = LambdaEvent { payload, context }; + + let result = function_handler(event, &mock).await.unwrap(); + + assert_eq!("Done", result); + } + + fn get_s3_event(event_name: &str, bucket_name: &str, object_key: &str) -> S3Event { + return S3Event { + records: (vec![get_s3_event_record(event_name, bucket_name, object_key)]), + }; + } + + fn get_s3_event_record(event_name: &str, bucket_name: &str, object_key: &str) -> S3EventRecord { + let s3_entity = S3Entity { + schema_version: (Some(String::default())), + configuration_id: (Some(String::default())), + bucket: (S3Bucket { + name: (Some(bucket_name.to_string())), + owner_identity: (S3UserIdentity { + principal_id: (Some(String::default())), + }), + arn: (Some(String::default())), + }), + object: (S3Object { + key: (Some(object_key.to_string())), + size: (Some(1)), + url_decoded_key: (Some(String::default())), + version_id: (Some(String::default())), + e_tag: (Some(String::default())), + sequencer: (Some(String::default())), + }), + }; + + return S3EventRecord { + event_version: (Some(String::default())), + event_source: (Some(String::default())), + aws_region: (Some(String::default())), + event_time: (DateTime::default()), + event_name: (Some(event_name.to_string())), + principal_id: (S3UserIdentity { + principal_id: (Some("X".to_string())), + }), + request_parameters: (S3RequestParameters { + source_ip_address: (Some(String::default())), + }), + response_elements: (HashMap::new()), + s3: (s3_entity), + }; + } +} diff --git a/examples/basic-s3-thumbnail/src/s3client.rs b/examples/basic-s3-thumbnail/src/s3client.rs new file mode 100644 index 00000000..3215a428 --- /dev/null +++ b/examples/basic-s3-thumbnail/src/s3client.rs @@ -0,0 +1,78 @@ +use std::io::Cursor; + +use async_trait::async_trait; +use aws_sdk_s3::{types::ByteStream, Client as S3Client}; +use thumbnailer::{create_thumbnails, ThumbnailSize}; + +#[async_trait] +pub trait GetFile { + async fn get_file(&self, bucket: &str, key: &str) -> Option>>; +} + +#[async_trait] +pub trait PutFile { + async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result; +} + +pub trait GetThumbnail { + fn get_thumbnail(&self, reader: Cursor>) -> ByteStream; +} + +impl GetThumbnail for S3Client { + fn get_thumbnail(&self, reader: Cursor>) -> ByteStream { + let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Small]).unwrap(); + + let thumbnail = thumbnails.pop().unwrap(); + let mut buf = Cursor::new(Vec::new()); + thumbnail.write_png(&mut buf).unwrap(); + + return ByteStream::from(buf.into_inner()); + } +} + +#[async_trait] +impl GetFile for S3Client { + async fn get_file(&self, bucket: &str, key: &str) -> Option>> { + println!("get file bucket {}, key {}", bucket, key); + + let output = self.get_object().bucket(bucket).key(key).send().await; + + let mut reader = None; + + if output.as_ref().ok().is_some() { + let bytes = output.ok().unwrap().body.collect().await.unwrap().to_vec(); + println!("Object is downloaded, size is {}", bytes.len()); + reader = Some(Cursor::new(bytes)); + } else if output.as_ref().err().is_some() { + let err = output.err().unwrap(); + let service_err = err.into_service_error(); + let meta = service_err.meta(); + println!("Error from aws when downloding: {}", meta.to_string()); + } else { + println!("Unknown error when downloading"); + } + + return reader; + } +} + +#[async_trait] +impl PutFile for S3Client { + async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result { + println!("put file bucket {}, key {}", bucket, key); + let result = self.put_object().bucket(bucket).key(key).body(bytes).send().await; + + if result.as_ref().is_ok() { + return Ok(format!("Uploaded a file with key {} into {}", key, bucket)); + } + + return Err(result + .err() + .unwrap() + .into_service_error() + .meta() + .message() + .unwrap() + .to_string()); + } +} From 6984cdd31fb444001b134577e54bfcaad7dd7aab Mon Sep 17 00:00:00 2001 From: Peter Borkuti Date: Fri, 24 Mar 2023 18:57:56 +0100 Subject: [PATCH 2/3] thumbnail creator - improve based on review (#613) --- examples/basic-s3-thumbnail/Cargo.toml | 2 +- examples/basic-s3-thumbnail/src/main.rs | 97 ++++++++++-------- examples/basic-s3-thumbnail/src/s3.rs | 49 +++++++++ examples/basic-s3-thumbnail/src/s3client.rs | 78 -------------- .../basic-s3-thumbnail/testdata/image.png | Bin 0 -> 282 bytes .../basic-s3-thumbnail/testdata/thumbnail.png | Bin 0 -> 82 bytes 6 files changed, 105 insertions(+), 121 deletions(-) create mode 100644 examples/basic-s3-thumbnail/src/s3.rs delete mode 100644 examples/basic-s3-thumbnail/src/s3client.rs create mode 100644 examples/basic-s3-thumbnail/testdata/image.png create mode 100644 examples/basic-s3-thumbnail/testdata/thumbnail.png diff --git a/examples/basic-s3-thumbnail/Cargo.toml b/examples/basic-s3-thumbnail/Cargo.toml index 797f3338..dfa6d69b 100644 --- a/examples/basic-s3-thumbnail/Cargo.toml +++ b/examples/basic-s3-thumbnail/Cargo.toml @@ -17,7 +17,7 @@ edition = "2021" [dependencies] aws_lambda_events = "0.7.2" lambda_runtime = { path = "../../lambda-runtime" } -serde = "1.0.136" +serde = "1" tokio = { version = "1", features = ["macros"] } tracing = { version = "0.1" } tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] } diff --git a/examples/basic-s3-thumbnail/src/main.rs b/examples/basic-s3-thumbnail/src/main.rs index 6af7f463..8737499a 100644 --- a/examples/basic-s3-thumbnail/src/main.rs +++ b/examples/basic-s3-thumbnail/src/main.rs @@ -1,10 +1,12 @@ -use aws_config::meta::region::RegionProviderChain; +use std::io::Cursor; + use aws_lambda_events::{event::s3::S3Event, s3::S3EventRecord}; use aws_sdk_s3::Client as S3Client; use lambda_runtime::{run, service_fn, Error, LambdaEvent}; -use s3client::{GetFile, GetThumbnail, PutFile}; +use s3::{GetFile, PutFile}; +use thumbnailer::{create_thumbnails, ThumbnailSize}; -mod s3client; +mod s3; /** This lambda handler @@ -19,38 +21,40 @@ Make sure that * this lambda only gets event from png file creation * this lambda has permission to put file into the "-thumbs" bucket */ -pub(crate) async fn function_handler( +pub(crate) async fn function_handler( event: LambdaEvent, + size: u32, client: &T, -) -> Result { - let result = Ok("".to_string()); +) -> Result<(), Error> { let records = event.payload.records; + for record in records.iter() { let (bucket, key) = get_file_props(record); + if bucket.is_empty() || key.is_empty() { // The event is not a create event or bucket/object key is missing - println!("record skipped"); + tracing::info!("record skipped"); continue; } let reader = client.get_file(&bucket, &key).await; - if reader.is_none() { + if reader.is_err() { continue; } - let thumbnail = client.get_thumbnail(reader.unwrap()); + let thumbnail = get_thumbnail(reader.unwrap(), size); let mut thumbs_bucket = bucket.to_owned(); thumbs_bucket.push_str("-thumbs"); - // It uplaods the thumbnail into a bucket name suffixed with "-thumbs" + // It uploads the thumbnail into a bucket name suffixed with "-thumbs" // So it needs file creation permission into that bucket - return client.put_file(&thumbs_bucket, &key, thumbnail).await; + let _ = client.put_file(&thumbs_bucket, &key, thumbnail).await; } - return result; + Ok(()) } fn get_file_props(record: &S3EventRecord) -> (String, String) { @@ -59,6 +63,7 @@ fn get_file_props(record: &S3EventRecord) -> (String, String) { if record.event_name.is_none() { return empty_response; } + if !record.event_name.as_ref().unwrap().starts_with("ObjectCreated") { return empty_response; } @@ -71,23 +76,24 @@ fn get_file_props(record: &S3EventRecord) -> (String, String) { let object_key = record.s3.object.key.to_owned().unwrap(); if bucket_name.is_empty() || object_key.is_empty() { - println!("Bucket name or object_key is empty"); + tracing::info!("Bucket name or object_key is empty"); return empty_response; } - println!("Bucket: {}, Object key: {}", bucket_name, object_key); + tracing::info!("Bucket: {}, Object key: {}", bucket_name, object_key); - return (bucket_name, object_key); + (bucket_name, object_key) } -async fn get_client() -> S3Client { - let region_provider = RegionProviderChain::default_provider().or_else("us-east-2"); - let config = aws_config::from_env().region(region_provider).load().await; - let client = S3Client::new(&config); +fn get_thumbnail(vec: Vec, size: u32) -> Vec { + let reader = Cursor::new(vec); + let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Custom((size, size))]).unwrap(); - println!("client region {}", client.conf().region().unwrap().to_string()); + let thumbnail = thumbnails.pop().unwrap(); + let mut buf = Cursor::new(Vec::new()); + thumbnail.write_png(&mut buf).unwrap(); - return client; + buf.into_inner() } #[tokio::main] @@ -104,10 +110,11 @@ async fn main() -> Result<(), Error> { .without_time() .init(); - let client = get_client().await; + let shared_config = aws_config::load_from_env().await; + let client = S3Client::new(&shared_config); let client_ref = &client; - let func = service_fn(move |event| async move { function_handler(event, client_ref).await }); + let func = service_fn(move |event| async move { function_handler(event, 128, client_ref).await }); run(func).await?; @@ -117,7 +124,9 @@ async fn main() -> Result<(), Error> { #[cfg(test)] mod tests { use std::collections::HashMap; - use std::io::Cursor; + use std::fs::File; + use std::io::BufReader; + use std::io::Read; use super::*; use async_trait::async_trait; @@ -127,12 +136,11 @@ mod tests { use aws_lambda_events::s3::S3Object; use aws_lambda_events::s3::S3RequestParameters; use aws_lambda_events::s3::S3UserIdentity; - use aws_sdk_s3::types::ByteStream; + use aws_sdk_s3::error::GetObjectError; use lambda_runtime::{Context, LambdaEvent}; use mockall::mock; - use mockall::predicate::eq; - use s3client::GetFile; - use s3client::PutFile; + use s3::GetFile; + use s3::PutFile; #[tokio::test] async fn response_is_good() { @@ -147,15 +155,11 @@ mod tests { #[async_trait] impl GetFile for FakeS3Client { - pub async fn get_file(&self, bucket: &str, key: &str) -> Option>>; + pub async fn get_file(&self, bucket: &str, key: &str) -> Result, GetObjectError>; } #[async_trait] impl PutFile for FakeS3Client { - pub async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result; - } - - impl GetThumbnail for FakeS3Client { - fn get_thumbnail(&self, reader: Cursor>) -> ByteStream; + pub async fn put_file(&self, bucket: &str, key: &str, bytes: Vec) -> Result; } } @@ -163,22 +167,31 @@ mod tests { mock.expect_get_file() .withf(|b: &str, k: &str| b.eq(bucket) && k.eq(key)) - .returning(|_1, _2| Some(Cursor::new(b"IMAGE".to_vec()))); - - mock.expect_get_thumbnail() - .with(eq(Cursor::new(b"IMAGE".to_vec()))) - .returning(|_| ByteStream::from_static(b"THUMBNAIL")); + .returning(|_1, _2| Ok(get_file("testdata/image.png"))); mock.expect_put_file() - .withf(|bu: &str, ke: &str, _by| bu.eq("test-bucket-thumbs") && ke.eq(key)) + .withf(|bu: &str, ke: &str, by| { + let thumbnail = get_file("testdata/thumbnail.png"); + return bu.eq("test-bucket-thumbs") && ke.eq(key) && by == &thumbnail; + }) .returning(|_1, _2, _3| Ok("Done".to_string())); let payload = get_s3_event("ObjectCreated", bucket, key); let event = LambdaEvent { payload, context }; - let result = function_handler(event, &mock).await.unwrap(); + let result = function_handler(event, 10, &mock).await.unwrap(); + + assert_eq!((), result); + } + + fn get_file(name: &str) -> Vec { + let f = File::open(name); + let mut reader = BufReader::new(f.unwrap()); + let mut buffer = Vec::new(); + + reader.read_to_end(&mut buffer).unwrap(); - assert_eq!("Done", result); + return buffer; } fn get_s3_event(event_name: &str, bucket_name: &str, object_key: &str) -> S3Event { diff --git a/examples/basic-s3-thumbnail/src/s3.rs b/examples/basic-s3-thumbnail/src/s3.rs new file mode 100644 index 00000000..83ef7bc7 --- /dev/null +++ b/examples/basic-s3-thumbnail/src/s3.rs @@ -0,0 +1,49 @@ +use async_trait::async_trait; +use aws_sdk_s3::{error::GetObjectError, types::ByteStream, Client as S3Client}; + +#[async_trait] +pub trait GetFile { + async fn get_file(&self, bucket: &str, key: &str) -> Result, GetObjectError>; +} + +#[async_trait] +pub trait PutFile { + async fn put_file(&self, bucket: &str, key: &str, bytes: Vec) -> Result; +} + +#[async_trait] +impl GetFile for S3Client { + async fn get_file(&self, bucket: &str, key: &str) -> Result, GetObjectError> { + tracing::info!("get file bucket {}, key {}", bucket, key); + + let output = self.get_object().bucket(bucket).key(key).send().await; + + return match output { + Ok(response) => { + let bytes = response.body.collect().await.unwrap().to_vec(); + tracing::info!("Object is downloaded, size is {}", bytes.len()); + Ok(bytes) + } + Err(err) => { + let service_err = err.into_service_error(); + let meta = service_err.meta(); + tracing::info!("Error from aws when downloding: {}", meta.to_string()); + Err(service_err) + } + }; + } +} + +#[async_trait] +impl PutFile for S3Client { + async fn put_file(&self, bucket: &str, key: &str, vec: Vec) -> Result { + tracing::info!("put file bucket {}, key {}", bucket, key); + let bytes = ByteStream::from(vec); + let result = self.put_object().bucket(bucket).key(key).body(bytes).send().await; + + match result { + Ok(_) => Ok(format!("Uploaded a file with key {} into {}", key, bucket)), + Err(err) => Err(err.into_service_error().meta().message().unwrap().to_string()), + } + } +} diff --git a/examples/basic-s3-thumbnail/src/s3client.rs b/examples/basic-s3-thumbnail/src/s3client.rs deleted file mode 100644 index 3215a428..00000000 --- a/examples/basic-s3-thumbnail/src/s3client.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::io::Cursor; - -use async_trait::async_trait; -use aws_sdk_s3::{types::ByteStream, Client as S3Client}; -use thumbnailer::{create_thumbnails, ThumbnailSize}; - -#[async_trait] -pub trait GetFile { - async fn get_file(&self, bucket: &str, key: &str) -> Option>>; -} - -#[async_trait] -pub trait PutFile { - async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result; -} - -pub trait GetThumbnail { - fn get_thumbnail(&self, reader: Cursor>) -> ByteStream; -} - -impl GetThumbnail for S3Client { - fn get_thumbnail(&self, reader: Cursor>) -> ByteStream { - let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Small]).unwrap(); - - let thumbnail = thumbnails.pop().unwrap(); - let mut buf = Cursor::new(Vec::new()); - thumbnail.write_png(&mut buf).unwrap(); - - return ByteStream::from(buf.into_inner()); - } -} - -#[async_trait] -impl GetFile for S3Client { - async fn get_file(&self, bucket: &str, key: &str) -> Option>> { - println!("get file bucket {}, key {}", bucket, key); - - let output = self.get_object().bucket(bucket).key(key).send().await; - - let mut reader = None; - - if output.as_ref().ok().is_some() { - let bytes = output.ok().unwrap().body.collect().await.unwrap().to_vec(); - println!("Object is downloaded, size is {}", bytes.len()); - reader = Some(Cursor::new(bytes)); - } else if output.as_ref().err().is_some() { - let err = output.err().unwrap(); - let service_err = err.into_service_error(); - let meta = service_err.meta(); - println!("Error from aws when downloding: {}", meta.to_string()); - } else { - println!("Unknown error when downloading"); - } - - return reader; - } -} - -#[async_trait] -impl PutFile for S3Client { - async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result { - println!("put file bucket {}, key {}", bucket, key); - let result = self.put_object().bucket(bucket).key(key).body(bytes).send().await; - - if result.as_ref().is_ok() { - return Ok(format!("Uploaded a file with key {} into {}", key, bucket)); - } - - return Err(result - .err() - .unwrap() - .into_service_error() - .meta() - .message() - .unwrap() - .to_string()); - } -} diff --git a/examples/basic-s3-thumbnail/testdata/image.png b/examples/basic-s3-thumbnail/testdata/image.png new file mode 100644 index 0000000000000000000000000000000000000000..078d155f6bf6735eb087eb0195b3e35f9f424d04 GIT binary patch literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^DIm-UBp4!QuJ{S0SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=cu>$S;_Ip=|P53lJ~K z+uenM@oty!5+IMg#M9T6{W-Ikn3DL(-uF5{ArVg(#}JFt$q5pyixWh8ngSjC85meA z7#KARcm4s&tCqM%l%ynf4NqV|ChEy=Vy|59;VQAR!XXWJ= d863$M85tR8F)&*H Date: Sat, 1 Apr 2023 15:17:22 +0200 Subject: [PATCH 3/3] thumbnail creator - improve based on 2nd review (#613) --- examples/basic-s3-thumbnail/src/main.rs | 94 ++++++++++++++----------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/examples/basic-s3-thumbnail/src/main.rs b/examples/basic-s3-thumbnail/src/main.rs index 8737499a..4ed7249f 100644 --- a/examples/basic-s3-thumbnail/src/main.rs +++ b/examples/basic-s3-thumbnail/src/main.rs @@ -28,22 +28,30 @@ pub(crate) async fn function_handler( ) -> Result<(), Error> { let records = event.payload.records; - for record in records.iter() { - let (bucket, key) = get_file_props(record); - - if bucket.is_empty() || key.is_empty() { - // The event is not a create event or bucket/object key is missing - tracing::info!("record skipped"); - continue; - } - - let reader = client.get_file(&bucket, &key).await; + for record in records.into_iter() { + let (bucket, key) = match get_file_props(record) { + Ok(touple) => touple, + Err(msg) => { + tracing::info!("Record skipped with reason: {}", msg); + continue; + } + }; - if reader.is_err() { - continue; - } + let image = match client.get_file(&bucket, &key).await { + Ok(vec) => vec, + Err(msg) => { + tracing::info!("Can not get file from S3: {}", msg); + continue; + } + }; - let thumbnail = get_thumbnail(reader.unwrap(), size); + let thumbnail = match get_thumbnail(image, size) { + Ok(vec) => vec, + Err(msg) => { + tracing::info!("Can not create thumbnail: {}", msg); + continue; + } + }; let mut thumbs_bucket = bucket.to_owned(); thumbs_bucket.push_str("-thumbs"); @@ -51,49 +59,49 @@ pub(crate) async fn function_handler( // It uploads the thumbnail into a bucket name suffixed with "-thumbs" // So it needs file creation permission into that bucket - let _ = client.put_file(&thumbs_bucket, &key, thumbnail).await; + match client.put_file(&thumbs_bucket, &key, thumbnail).await { + Ok(msg) => tracing::info!(msg), + Err(msg) => tracing::info!("Can not upload thumbnail: {}", msg), + } } Ok(()) } -fn get_file_props(record: &S3EventRecord) -> (String, String) { - let empty_response = ("".to_string(), "".to_string()); +fn get_file_props(record: S3EventRecord) -> Result<(String, String), String> { + record + .event_name + .filter(|s| s.starts_with("ObjectCreated")) + .ok_or("Wrong event")?; - if record.event_name.is_none() { - return empty_response; - } - - if !record.event_name.as_ref().unwrap().starts_with("ObjectCreated") { - return empty_response; - } - - if record.s3.bucket.name.is_none() || record.s3.object.key.is_none() { - return empty_response; - } + let bucket = record + .s3 + .bucket + .name + .filter(|s| !s.is_empty()) + .ok_or("No bucket name")?; - let bucket_name = record.s3.bucket.name.to_owned().unwrap(); - let object_key = record.s3.object.key.to_owned().unwrap(); + let key = record.s3.object.key.filter(|s| !s.is_empty()).ok_or("No object key")?; - if bucket_name.is_empty() || object_key.is_empty() { - tracing::info!("Bucket name or object_key is empty"); - return empty_response; - } - - tracing::info!("Bucket: {}, Object key: {}", bucket_name, object_key); - - (bucket_name, object_key) + Ok((bucket, key)) } -fn get_thumbnail(vec: Vec, size: u32) -> Vec { +fn get_thumbnail(vec: Vec, size: u32) -> Result, String> { let reader = Cursor::new(vec); - let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Custom((size, size))]).unwrap(); + let mime = mime::IMAGE_PNG; + let sizes = [ThumbnailSize::Custom((size, size))]; + + let thumbnail = match create_thumbnails(reader, mime, sizes) { + Ok(mut thumbnails) => thumbnails.pop().ok_or("No thumbnail created")?, + Err(thumb_error) => return Err(thumb_error.to_string()), + }; - let thumbnail = thumbnails.pop().unwrap(); let mut buf = Cursor::new(Vec::new()); - thumbnail.write_png(&mut buf).unwrap(); - buf.into_inner() + match thumbnail.write_png(&mut buf) { + Ok(_) => Ok(buf.into_inner()), + Err(_) => Err("Unknown error when Thumbnail::write_png".to_string()), + } } #[tokio::main]