Skip to content

Commit b6d0ed6

Browse files
committed
s3 example - thumbnail creator (awslabs#613)
1 parent 0f6e2a2 commit b6d0ed6

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[package]
2+
name = "basic-s3-thumbnail"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# Starting in Rust 1.62 you can use `cargo add` to add dependencies
7+
# to your project.
8+
#
9+
# If you're using an older Rust version,
10+
# download cargo-edit(https://github.com/killercup/cargo-edit#installation)
11+
# to install the `add` subcommand.
12+
#
13+
# Running `cargo add DEPENDENCY_NAME` will
14+
# add the latest version of a dependency to the list,
15+
# and it will keep the alphabetic ordering for you.
16+
17+
[dependencies]
18+
aws_lambda_events = "0.7.2"
19+
lambda_runtime = { path = "../../lambda-runtime" }
20+
serde = "1.0.136"
21+
tokio = { version = "1", features = ["macros"] }
22+
tracing = { version = "0.1" }
23+
tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] }
24+
aws-config = "0.54.1"
25+
aws-sdk-s3 = "0.24.0"
26+
thumbnailer = "0.4.0"
27+
mime = "0.3.16"
28+
async-trait = "0.1.66"
29+
30+
[dev-dependencies]
31+
mockall = "0.11.3"
32+
tokio-test = "0.4.2"

examples/basic-s3-thumbnail/README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# AWS Lambda Function that uses S3
2+
3+
This example processes S3 events. If the event is a CREATE event,
4+
it downloads the created file, generates a thumbnail from it
5+
(it assumes that the file is an image) and uploads it to S3 into a bucket named
6+
[original-bucket-name]-thumbs.
7+
8+
## Build & Deploy
9+
10+
1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation)
11+
2. Build the function with `cargo lambda build --release`
12+
3. Deploy the function to AWS Lambda with `cargo lambda deploy --iam-role YOUR_ROLE`
13+
14+
## Build for ARM 64
15+
16+
Build the function with `cargo lambda build --release --arm64`
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
use aws_config::meta::region::RegionProviderChain;
2+
use aws_lambda_events::{event::s3::S3Event, s3::S3EventRecord};
3+
use aws_sdk_s3::Client as S3Client;
4+
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
5+
use s3client::{GetFile, GetThumbnail, PutFile};
6+
7+
mod s3client;
8+
9+
/**
10+
This lambda handler
11+
* listen to file creation events
12+
* downloads the created file
13+
* creates a thumbnail from it
14+
* uploads the thumbnail to bucket "[original bucket name]-thumbs".
15+
16+
Make sure that
17+
* the created png file has no strange characters in the name
18+
* there is another bucket with "-thumbs" suffix in the name
19+
* this lambda only gets event from png file creation
20+
* this lambda has permission to put file into the "-thumbs" bucket
21+
*/
22+
pub(crate) async fn function_handler<T: PutFile + GetFile + GetThumbnail>(
23+
event: LambdaEvent<S3Event>,
24+
client: &T,
25+
) -> Result<String, String> {
26+
let result = Ok("".to_string());
27+
let records = event.payload.records;
28+
for record in records.iter() {
29+
let (bucket, key) = get_file_props(record);
30+
if bucket.is_empty() || key.is_empty() {
31+
// The event is not a create event or bucket/object key is missing
32+
println!("record skipped");
33+
continue;
34+
}
35+
36+
let reader = client.get_file(&bucket, &key).await;
37+
38+
if reader.is_none() {
39+
continue;
40+
}
41+
42+
let thumbnail = client.get_thumbnail(reader.unwrap());
43+
44+
let mut thumbs_bucket = bucket.to_owned();
45+
thumbs_bucket.push_str("-thumbs");
46+
47+
// It uplaods the thumbnail into a bucket name suffixed with "-thumbs"
48+
// So it needs file creation permission into that bucket
49+
50+
return client.put_file(&thumbs_bucket, &key, thumbnail).await;
51+
}
52+
53+
return result;
54+
}
55+
56+
fn get_file_props(record: &S3EventRecord) -> (String, String) {
57+
let empty_response = ("".to_string(), "".to_string());
58+
59+
if record.event_name.is_none() {
60+
return empty_response;
61+
}
62+
if !record.event_name.as_ref().unwrap().starts_with("ObjectCreated") {
63+
return empty_response;
64+
}
65+
66+
if record.s3.bucket.name.is_none() || record.s3.object.key.is_none() {
67+
return empty_response;
68+
}
69+
70+
let bucket_name = record.s3.bucket.name.to_owned().unwrap();
71+
let object_key = record.s3.object.key.to_owned().unwrap();
72+
73+
if bucket_name.is_empty() || object_key.is_empty() {
74+
println!("Bucket name or object_key is empty");
75+
return empty_response;
76+
}
77+
78+
println!("Bucket: {}, Object key: {}", bucket_name, object_key);
79+
80+
return (bucket_name, object_key);
81+
}
82+
83+
async fn get_client() -> S3Client {
84+
let region_provider = RegionProviderChain::default_provider().or_else("us-east-2");
85+
let config = aws_config::from_env().region(region_provider).load().await;
86+
let client = S3Client::new(&config);
87+
88+
println!("client region {}", client.conf().region().unwrap().to_string());
89+
90+
return client;
91+
}
92+
93+
#[tokio::main]
94+
async fn main() -> Result<(), Error> {
95+
set_tracing();
96+
set_handler().await
97+
}
98+
99+
async fn set_handler() -> Result<(), Error> {
100+
let client = get_client().await;
101+
let client_ref = &client;
102+
103+
let func = service_fn(move |event| async move { function_handler(event, client_ref).await });
104+
105+
run(func).await?;
106+
107+
Ok(())
108+
}
109+
110+
fn set_tracing() {
111+
// required to enable CloudWatch error logging by the runtime
112+
tracing_subscriber::fmt()
113+
.with_max_level(tracing::Level::INFO)
114+
// disable printing the name of the module in every log line.
115+
.with_target(false)
116+
// this needs to be set to false, otherwise ANSI color codes will
117+
// show up in a confusing manner in CloudWatch logs.
118+
.with_ansi(false)
119+
// disabling time is handy because CloudWatch will add the ingestion time.
120+
.without_time()
121+
.init();
122+
}
123+
124+
#[cfg(test)]
125+
mod tests {
126+
use std::collections::HashMap;
127+
use std::io::Cursor;
128+
129+
use super::*;
130+
use async_trait::async_trait;
131+
use aws_lambda_events::chrono::DateTime;
132+
use aws_lambda_events::s3::S3Bucket;
133+
use aws_lambda_events::s3::S3Entity;
134+
use aws_lambda_events::s3::S3Object;
135+
use aws_lambda_events::s3::S3RequestParameters;
136+
use aws_lambda_events::s3::S3UserIdentity;
137+
use aws_sdk_s3::types::ByteStream;
138+
use lambda_runtime::{Context, LambdaEvent};
139+
use mockall::mock;
140+
use mockall::predicate::eq;
141+
use s3client::GetFile;
142+
use s3client::PutFile;
143+
144+
#[tokio::test]
145+
async fn response_is_good() {
146+
let mut context = Context::default();
147+
context.request_id = "test-request-id".to_string();
148+
149+
let bucket = "test-bucket";
150+
let key = "test-key";
151+
152+
mock! {
153+
FakeS3Client {}
154+
155+
#[async_trait]
156+
impl GetFile for FakeS3Client {
157+
pub async fn get_file(&self, bucket: &str, key: &str) -> Option<Cursor<Vec<u8>>>;
158+
}
159+
#[async_trait]
160+
impl PutFile for FakeS3Client {
161+
pub async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result<String, String>;
162+
}
163+
164+
impl GetThumbnail for FakeS3Client {
165+
fn get_thumbnail(&self, reader: Cursor<Vec<u8>>) -> ByteStream;
166+
}
167+
}
168+
169+
let mut mock = MockFakeS3Client::new();
170+
171+
mock.expect_get_file()
172+
.withf(|b: &str, k: &str| b.eq(bucket) && k.eq(key))
173+
.returning(|_1, _2| Some(Cursor::new(b"IMAGE".to_vec())));
174+
175+
mock.expect_get_thumbnail()
176+
.with(eq(Cursor::new(b"IMAGE".to_vec())))
177+
.returning(|_| ByteStream::from_static(b"THUMBNAIL"));
178+
179+
mock.expect_put_file()
180+
.withf(|bu: &str, ke: &str, _by| bu.eq("test-bucket-thumbs") && ke.eq(key))
181+
.returning(|_1, _2, _3| Ok("Done".to_string()));
182+
183+
let payload = get_s3_event("ObjectCreated", bucket, key);
184+
let event = LambdaEvent { payload, context };
185+
186+
let result = function_handler(event, &mock).await.unwrap();
187+
188+
assert_eq!("Done", result);
189+
}
190+
191+
fn get_s3_event(event_name: &str, bucket_name: &str, object_key: &str) -> S3Event {
192+
return S3Event {
193+
records: (vec![get_s3_event_record(event_name, bucket_name, object_key)]),
194+
};
195+
}
196+
197+
fn get_s3_event_record(event_name: &str, bucket_name: &str, object_key: &str) -> S3EventRecord {
198+
let s3_entity = S3Entity {
199+
schema_version: (Some(String::default())),
200+
configuration_id: (Some(String::default())),
201+
bucket: (S3Bucket {
202+
name: (Some(bucket_name.to_string())),
203+
owner_identity: (S3UserIdentity {
204+
principal_id: (Some(String::default())),
205+
}),
206+
arn: (Some(String::default())),
207+
}),
208+
object: (S3Object {
209+
key: (Some(object_key.to_string())),
210+
size: (Some(1)),
211+
url_decoded_key: (Some(String::default())),
212+
version_id: (Some(String::default())),
213+
e_tag: (Some(String::default())),
214+
sequencer: (Some(String::default())),
215+
}),
216+
};
217+
218+
return S3EventRecord {
219+
event_version: (Some(String::default())),
220+
event_source: (Some(String::default())),
221+
aws_region: (Some(String::default())),
222+
event_time: (DateTime::default()),
223+
event_name: (Some(event_name.to_string())),
224+
principal_id: (S3UserIdentity {
225+
principal_id: (Some("X".to_string())),
226+
}),
227+
request_parameters: (S3RequestParameters {
228+
source_ip_address: (Some(String::default())),
229+
}),
230+
response_elements: (HashMap::new()),
231+
s3: (s3_entity),
232+
};
233+
}
234+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use std::io::Cursor;
2+
3+
use async_trait::async_trait;
4+
use aws_sdk_s3::{types::ByteStream, Client as S3Client};
5+
use thumbnailer::{create_thumbnails, ThumbnailSize};
6+
7+
#[async_trait]
8+
pub trait GetFile {
9+
async fn get_file(&self, bucket: &str, key: &str) -> Option<Cursor<Vec<u8>>>;
10+
}
11+
12+
#[async_trait]
13+
pub trait PutFile {
14+
async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result<String, String>;
15+
}
16+
17+
pub trait GetThumbnail {
18+
fn get_thumbnail(&self, reader: Cursor<Vec<u8>>) -> ByteStream;
19+
}
20+
21+
impl GetThumbnail for S3Client {
22+
fn get_thumbnail(&self, reader: Cursor<Vec<u8>>) -> ByteStream {
23+
let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Small]).unwrap();
24+
25+
let thumbnail = thumbnails.pop().unwrap();
26+
let mut buf = Cursor::new(Vec::new());
27+
thumbnail.write_png(&mut buf).unwrap();
28+
29+
return ByteStream::from(buf.into_inner());
30+
}
31+
}
32+
33+
#[async_trait]
34+
impl GetFile for S3Client {
35+
async fn get_file(&self, bucket: &str, key: &str) -> Option<Cursor<Vec<u8>>> {
36+
println!("get file bucket {}, key {}", bucket, key);
37+
38+
let output = self.get_object().bucket(bucket).key(key).send().await;
39+
40+
let mut reader = None;
41+
42+
if output.as_ref().ok().is_some() {
43+
let bytes = output.ok().unwrap().body.collect().await.unwrap().to_vec();
44+
println!("Object is downloaded, size is {}", bytes.len());
45+
reader = Some(Cursor::new(bytes));
46+
} else if output.as_ref().err().is_some() {
47+
let err = output.err().unwrap();
48+
let service_err = err.into_service_error();
49+
let meta = service_err.meta();
50+
println!("Error from aws when downloding: {}", meta.to_string());
51+
} else {
52+
println!("Unknown error when downloading");
53+
}
54+
55+
return reader;
56+
}
57+
}
58+
59+
#[async_trait]
60+
impl PutFile for S3Client {
61+
async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result<String, String> {
62+
println!("put file bucket {}, key {}", bucket, key);
63+
let result = self.put_object().bucket(bucket).key(key).body(bytes).send().await;
64+
65+
if result.as_ref().is_ok() {
66+
return Ok(format!("Uploaded a file with key {} into {}", key, bucket));
67+
}
68+
69+
return Err(result
70+
.err()
71+
.unwrap()
72+
.into_service_error()
73+
.meta()
74+
.message()
75+
.unwrap()
76+
.to_string());
77+
}
78+
}

0 commit comments

Comments
 (0)