Skip to content

Commit 3888a1b

Browse files
authored
s3 example - thumbnail creator (#613) (#621)
1 parent 9cea0e3 commit 3888a1b

File tree

6 files changed

+345
-0
lines changed

6 files changed

+345
-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"
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: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
use std::io::Cursor;
2+
3+
use aws_lambda_events::{event::s3::S3Event, s3::S3EventRecord};
4+
use aws_sdk_s3::Client as S3Client;
5+
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
6+
use s3::{GetFile, PutFile};
7+
use thumbnailer::{create_thumbnails, ThumbnailSize};
8+
9+
mod s3;
10+
11+
/**
12+
This lambda handler
13+
* listen to file creation events
14+
* downloads the created file
15+
* creates a thumbnail from it
16+
* uploads the thumbnail to bucket "[original bucket name]-thumbs".
17+
18+
Make sure that
19+
* the created png file has no strange characters in the name
20+
* there is another bucket with "-thumbs" suffix in the name
21+
* this lambda only gets event from png file creation
22+
* this lambda has permission to put file into the "-thumbs" bucket
23+
*/
24+
pub(crate) async fn function_handler<T: PutFile + GetFile>(
25+
event: LambdaEvent<S3Event>,
26+
size: u32,
27+
client: &T,
28+
) -> Result<(), Error> {
29+
let records = event.payload.records;
30+
31+
for record in records.into_iter() {
32+
let (bucket, key) = match get_file_props(record) {
33+
Ok(touple) => touple,
34+
Err(msg) => {
35+
tracing::info!("Record skipped with reason: {}", msg);
36+
continue;
37+
}
38+
};
39+
40+
let image = match client.get_file(&bucket, &key).await {
41+
Ok(vec) => vec,
42+
Err(msg) => {
43+
tracing::info!("Can not get file from S3: {}", msg);
44+
continue;
45+
}
46+
};
47+
48+
let thumbnail = match get_thumbnail(image, size) {
49+
Ok(vec) => vec,
50+
Err(msg) => {
51+
tracing::info!("Can not create thumbnail: {}", msg);
52+
continue;
53+
}
54+
};
55+
56+
let mut thumbs_bucket = bucket.to_owned();
57+
thumbs_bucket.push_str("-thumbs");
58+
59+
// It uploads the thumbnail into a bucket name suffixed with "-thumbs"
60+
// So it needs file creation permission into that bucket
61+
62+
match client.put_file(&thumbs_bucket, &key, thumbnail).await {
63+
Ok(msg) => tracing::info!(msg),
64+
Err(msg) => tracing::info!("Can not upload thumbnail: {}", msg),
65+
}
66+
}
67+
68+
Ok(())
69+
}
70+
71+
fn get_file_props(record: S3EventRecord) -> Result<(String, String), String> {
72+
record
73+
.event_name
74+
.filter(|s| s.starts_with("ObjectCreated"))
75+
.ok_or("Wrong event")?;
76+
77+
let bucket = record
78+
.s3
79+
.bucket
80+
.name
81+
.filter(|s| !s.is_empty())
82+
.ok_or("No bucket name")?;
83+
84+
let key = record.s3.object.key.filter(|s| !s.is_empty()).ok_or("No object key")?;
85+
86+
Ok((bucket, key))
87+
}
88+
89+
fn get_thumbnail(vec: Vec<u8>, size: u32) -> Result<Vec<u8>, String> {
90+
let reader = Cursor::new(vec);
91+
let mime = mime::IMAGE_PNG;
92+
let sizes = [ThumbnailSize::Custom((size, size))];
93+
94+
let thumbnail = match create_thumbnails(reader, mime, sizes) {
95+
Ok(mut thumbnails) => thumbnails.pop().ok_or("No thumbnail created")?,
96+
Err(thumb_error) => return Err(thumb_error.to_string()),
97+
};
98+
99+
let mut buf = Cursor::new(Vec::new());
100+
101+
match thumbnail.write_png(&mut buf) {
102+
Ok(_) => Ok(buf.into_inner()),
103+
Err(_) => Err("Unknown error when Thumbnail::write_png".to_string()),
104+
}
105+
}
106+
107+
#[tokio::main]
108+
async fn main() -> Result<(), Error> {
109+
// required to enable CloudWatch error logging by the runtime
110+
tracing_subscriber::fmt()
111+
.with_max_level(tracing::Level::INFO)
112+
// disable printing the name of the module in every log line.
113+
.with_target(false)
114+
// this needs to be set to false, otherwise ANSI color codes will
115+
// show up in a confusing manner in CloudWatch logs.
116+
.with_ansi(false)
117+
// disabling time is handy because CloudWatch will add the ingestion time.
118+
.without_time()
119+
.init();
120+
121+
let shared_config = aws_config::load_from_env().await;
122+
let client = S3Client::new(&shared_config);
123+
let client_ref = &client;
124+
125+
let func = service_fn(move |event| async move { function_handler(event, 128, client_ref).await });
126+
127+
run(func).await?;
128+
129+
Ok(())
130+
}
131+
132+
#[cfg(test)]
133+
mod tests {
134+
use std::collections::HashMap;
135+
use std::fs::File;
136+
use std::io::BufReader;
137+
use std::io::Read;
138+
139+
use super::*;
140+
use async_trait::async_trait;
141+
use aws_lambda_events::chrono::DateTime;
142+
use aws_lambda_events::s3::S3Bucket;
143+
use aws_lambda_events::s3::S3Entity;
144+
use aws_lambda_events::s3::S3Object;
145+
use aws_lambda_events::s3::S3RequestParameters;
146+
use aws_lambda_events::s3::S3UserIdentity;
147+
use aws_sdk_s3::error::GetObjectError;
148+
use lambda_runtime::{Context, LambdaEvent};
149+
use mockall::mock;
150+
use s3::GetFile;
151+
use s3::PutFile;
152+
153+
#[tokio::test]
154+
async fn response_is_good() {
155+
let mut context = Context::default();
156+
context.request_id = "test-request-id".to_string();
157+
158+
let bucket = "test-bucket";
159+
let key = "test-key";
160+
161+
mock! {
162+
FakeS3Client {}
163+
164+
#[async_trait]
165+
impl GetFile for FakeS3Client {
166+
pub async fn get_file(&self, bucket: &str, key: &str) -> Result<Vec<u8>, GetObjectError>;
167+
}
168+
#[async_trait]
169+
impl PutFile for FakeS3Client {
170+
pub async fn put_file(&self, bucket: &str, key: &str, bytes: Vec<u8>) -> Result<String, String>;
171+
}
172+
}
173+
174+
let mut mock = MockFakeS3Client::new();
175+
176+
mock.expect_get_file()
177+
.withf(|b: &str, k: &str| b.eq(bucket) && k.eq(key))
178+
.returning(|_1, _2| Ok(get_file("testdata/image.png")));
179+
180+
mock.expect_put_file()
181+
.withf(|bu: &str, ke: &str, by| {
182+
let thumbnail = get_file("testdata/thumbnail.png");
183+
return bu.eq("test-bucket-thumbs") && ke.eq(key) && by == &thumbnail;
184+
})
185+
.returning(|_1, _2, _3| Ok("Done".to_string()));
186+
187+
let payload = get_s3_event("ObjectCreated", bucket, key);
188+
let event = LambdaEvent { payload, context };
189+
190+
let result = function_handler(event, 10, &mock).await.unwrap();
191+
192+
assert_eq!((), result);
193+
}
194+
195+
fn get_file(name: &str) -> Vec<u8> {
196+
let f = File::open(name);
197+
let mut reader = BufReader::new(f.unwrap());
198+
let mut buffer = Vec::new();
199+
200+
reader.read_to_end(&mut buffer).unwrap();
201+
202+
return buffer;
203+
}
204+
205+
fn get_s3_event(event_name: &str, bucket_name: &str, object_key: &str) -> S3Event {
206+
return S3Event {
207+
records: (vec![get_s3_event_record(event_name, bucket_name, object_key)]),
208+
};
209+
}
210+
211+
fn get_s3_event_record(event_name: &str, bucket_name: &str, object_key: &str) -> S3EventRecord {
212+
let s3_entity = S3Entity {
213+
schema_version: (Some(String::default())),
214+
configuration_id: (Some(String::default())),
215+
bucket: (S3Bucket {
216+
name: (Some(bucket_name.to_string())),
217+
owner_identity: (S3UserIdentity {
218+
principal_id: (Some(String::default())),
219+
}),
220+
arn: (Some(String::default())),
221+
}),
222+
object: (S3Object {
223+
key: (Some(object_key.to_string())),
224+
size: (Some(1)),
225+
url_decoded_key: (Some(String::default())),
226+
version_id: (Some(String::default())),
227+
e_tag: (Some(String::default())),
228+
sequencer: (Some(String::default())),
229+
}),
230+
};
231+
232+
return S3EventRecord {
233+
event_version: (Some(String::default())),
234+
event_source: (Some(String::default())),
235+
aws_region: (Some(String::default())),
236+
event_time: (DateTime::default()),
237+
event_name: (Some(event_name.to_string())),
238+
principal_id: (S3UserIdentity {
239+
principal_id: (Some("X".to_string())),
240+
}),
241+
request_parameters: (S3RequestParameters {
242+
source_ip_address: (Some(String::default())),
243+
}),
244+
response_elements: (HashMap::new()),
245+
s3: (s3_entity),
246+
};
247+
}
248+
}

examples/basic-s3-thumbnail/src/s3.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use async_trait::async_trait;
2+
use aws_sdk_s3::{error::GetObjectError, types::ByteStream, Client as S3Client};
3+
4+
#[async_trait]
5+
pub trait GetFile {
6+
async fn get_file(&self, bucket: &str, key: &str) -> Result<Vec<u8>, GetObjectError>;
7+
}
8+
9+
#[async_trait]
10+
pub trait PutFile {
11+
async fn put_file(&self, bucket: &str, key: &str, bytes: Vec<u8>) -> Result<String, String>;
12+
}
13+
14+
#[async_trait]
15+
impl GetFile for S3Client {
16+
async fn get_file(&self, bucket: &str, key: &str) -> Result<Vec<u8>, GetObjectError> {
17+
tracing::info!("get file bucket {}, key {}", bucket, key);
18+
19+
let output = self.get_object().bucket(bucket).key(key).send().await;
20+
21+
return match output {
22+
Ok(response) => {
23+
let bytes = response.body.collect().await.unwrap().to_vec();
24+
tracing::info!("Object is downloaded, size is {}", bytes.len());
25+
Ok(bytes)
26+
}
27+
Err(err) => {
28+
let service_err = err.into_service_error();
29+
let meta = service_err.meta();
30+
tracing::info!("Error from aws when downloding: {}", meta.to_string());
31+
Err(service_err)
32+
}
33+
};
34+
}
35+
}
36+
37+
#[async_trait]
38+
impl PutFile for S3Client {
39+
async fn put_file(&self, bucket: &str, key: &str, vec: Vec<u8>) -> Result<String, String> {
40+
tracing::info!("put file bucket {}, key {}", bucket, key);
41+
let bytes = ByteStream::from(vec);
42+
let result = self.put_object().bucket(bucket).key(key).body(bytes).send().await;
43+
44+
match result {
45+
Ok(_) => Ok(format!("Uploaded a file with key {} into {}", key, bucket)),
46+
Err(err) => Err(err.into_service_error().meta().message().unwrap().to_string()),
47+
}
48+
}
49+
}
282 Bytes
Loading
Loading

0 commit comments

Comments
 (0)