Skip to content

Commit b1ccc2f

Browse files
committed
thumbnail creator - improve based on review (awslabs#613)
1 parent 07cf14f commit b1ccc2f

File tree

6 files changed

+112
-121
lines changed

6 files changed

+112
-121
lines changed

examples/basic-s3-thumbnail/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ edition = "2021"
1717
[dependencies]
1818
aws_lambda_events = "0.7.2"
1919
lambda_runtime = { path = "../../lambda-runtime" }
20-
serde = "1.0.136"
20+
serde = "1"
2121
tokio = { version = "1", features = ["macros"] }
2222
tracing = { version = "0.1" }
2323
tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] }

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

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
use aws_config::meta::region::RegionProviderChain;
1+
use std::io::Cursor;
2+
23
use aws_lambda_events::{event::s3::S3Event, s3::S3EventRecord};
34
use aws_sdk_s3::Client as S3Client;
45
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
5-
use s3client::{GetFile, GetThumbnail, PutFile};
6+
use s3::{GetFile, PutFile};
7+
use thumbnailer::{create_thumbnails, ThumbnailSize};
68

7-
mod s3client;
9+
mod s3;
810

911
/**
1012
This lambda handler
@@ -19,38 +21,40 @@ Make sure that
1921
* this lambda only gets event from png file creation
2022
* this lambda has permission to put file into the "-thumbs" bucket
2123
*/
22-
pub(crate) async fn function_handler<T: PutFile + GetFile + GetThumbnail>(
24+
pub(crate) async fn function_handler<T: PutFile + GetFile>(
2325
event: LambdaEvent<S3Event>,
26+
size: u32,
2427
client: &T,
25-
) -> Result<String, String> {
26-
let result = Ok("".to_string());
28+
) -> Result<(), Error> {
2729
let records = event.payload.records;
30+
2831
for record in records.iter() {
2932
let (bucket, key) = get_file_props(record);
33+
3034
if bucket.is_empty() || key.is_empty() {
3135
// The event is not a create event or bucket/object key is missing
32-
println!("record skipped");
36+
tracing::info!("record skipped");
3337
continue;
3438
}
3539

3640
let reader = client.get_file(&bucket, &key).await;
3741

38-
if reader.is_none() {
42+
if reader.is_err() {
3943
continue;
4044
}
4145

42-
let thumbnail = client.get_thumbnail(reader.unwrap());
46+
let thumbnail = get_thumbnail(reader.unwrap(), size);
4347

4448
let mut thumbs_bucket = bucket.to_owned();
4549
thumbs_bucket.push_str("-thumbs");
4650

47-
// It uplaods the thumbnail into a bucket name suffixed with "-thumbs"
51+
// It uploads the thumbnail into a bucket name suffixed with "-thumbs"
4852
// So it needs file creation permission into that bucket
4953

50-
return client.put_file(&thumbs_bucket, &key, thumbnail).await;
54+
let _ = client.put_file(&thumbs_bucket, &key, thumbnail).await;
5155
}
5256

53-
return result;
57+
Ok(())
5458
}
5559

5660
fn get_file_props(record: &S3EventRecord) -> (String, String) {
@@ -59,6 +63,7 @@ fn get_file_props(record: &S3EventRecord) -> (String, String) {
5963
if record.event_name.is_none() {
6064
return empty_response;
6165
}
66+
6267
if !record.event_name.as_ref().unwrap().starts_with("ObjectCreated") {
6368
return empty_response;
6469
}
@@ -71,23 +76,24 @@ fn get_file_props(record: &S3EventRecord) -> (String, String) {
7176
let object_key = record.s3.object.key.to_owned().unwrap();
7277

7378
if bucket_name.is_empty() || object_key.is_empty() {
74-
println!("Bucket name or object_key is empty");
79+
tracing::info!("Bucket name or object_key is empty");
7580
return empty_response;
7681
}
7782

78-
println!("Bucket: {}, Object key: {}", bucket_name, object_key);
83+
tracing::info!("Bucket: {}, Object key: {}", bucket_name, object_key);
7984

80-
return (bucket_name, object_key);
85+
(bucket_name, object_key)
8186
}
8287

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);
88+
fn get_thumbnail(vec: Vec<u8>, size: u32) -> Vec<u8> {
89+
let reader = Cursor::new(vec);
90+
let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Custom((size, size))]).unwrap();
8791

88-
println!("client region {}", client.conf().region().unwrap().to_string());
92+
let thumbnail = thumbnails.pop().unwrap();
93+
let mut buf = Cursor::new(Vec::new());
94+
thumbnail.write_png(&mut buf).unwrap();
8995

90-
return client;
96+
buf.into_inner()
9197
}
9298

9399
#[tokio::main]
@@ -104,10 +110,11 @@ async fn main() -> Result<(), Error> {
104110
.without_time()
105111
.init();
106112

107-
let client = get_client().await;
113+
let shared_config = aws_config::load_from_env().await;
114+
let client = S3Client::new(&shared_config);
108115
let client_ref = &client;
109116

110-
let func = service_fn(move |event| async move { function_handler(event, client_ref).await });
117+
let func = service_fn(move |event| async move { function_handler(event, 128, client_ref).await });
111118

112119
run(func).await?;
113120

@@ -117,7 +124,9 @@ async fn main() -> Result<(), Error> {
117124
#[cfg(test)]
118125
mod tests {
119126
use std::collections::HashMap;
120-
use std::io::Cursor;
127+
use std::fs::File;
128+
use std::io::BufReader;
129+
use std::io::Read;
121130

122131
use super::*;
123132
use async_trait::async_trait;
@@ -127,12 +136,11 @@ mod tests {
127136
use aws_lambda_events::s3::S3Object;
128137
use aws_lambda_events::s3::S3RequestParameters;
129138
use aws_lambda_events::s3::S3UserIdentity;
130-
use aws_sdk_s3::types::ByteStream;
139+
use aws_sdk_s3::error::GetObjectError;
131140
use lambda_runtime::{Context, LambdaEvent};
132141
use mockall::mock;
133-
use mockall::predicate::eq;
134-
use s3client::GetFile;
135-
use s3client::PutFile;
142+
use s3::GetFile;
143+
use s3::PutFile;
136144

137145
#[tokio::test]
138146
async fn response_is_good() {
@@ -147,38 +155,43 @@ mod tests {
147155

148156
#[async_trait]
149157
impl GetFile for FakeS3Client {
150-
pub async fn get_file(&self, bucket: &str, key: &str) -> Option<Cursor<Vec<u8>>>;
158+
pub async fn get_file(&self, bucket: &str, key: &str) -> Result<Vec<u8>, GetObjectError>;
151159
}
152160
#[async_trait]
153161
impl PutFile for FakeS3Client {
154-
pub async fn put_file(&self, bucket: &str, key: &str, bytes: ByteStream) -> Result<String, String>;
155-
}
156-
157-
impl GetThumbnail for FakeS3Client {
158-
fn get_thumbnail(&self, reader: Cursor<Vec<u8>>) -> ByteStream;
162+
pub async fn put_file(&self, bucket: &str, key: &str, bytes: Vec<u8>) -> Result<String, String>;
159163
}
160164
}
161165

162166
let mut mock = MockFakeS3Client::new();
163167

164168
mock.expect_get_file()
165169
.withf(|b: &str, k: &str| b.eq(bucket) && k.eq(key))
166-
.returning(|_1, _2| Some(Cursor::new(b"IMAGE".to_vec())));
167-
168-
mock.expect_get_thumbnail()
169-
.with(eq(Cursor::new(b"IMAGE".to_vec())))
170-
.returning(|_| ByteStream::from_static(b"THUMBNAIL"));
170+
.returning(|_1, _2| Ok(get_file("testdata/image.png")));
171171

172172
mock.expect_put_file()
173-
.withf(|bu: &str, ke: &str, _by| bu.eq("test-bucket-thumbs") && ke.eq(key))
173+
.withf(|bu: &str, ke: &str, by| {
174+
let thumbnail = get_file("testdata/thumbnail.png");
175+
return bu.eq("test-bucket-thumbs") && ke.eq(key) && by == &thumbnail;
176+
})
174177
.returning(|_1, _2, _3| Ok("Done".to_string()));
175178

176179
let payload = get_s3_event("ObjectCreated", bucket, key);
177180
let event = LambdaEvent { payload, context };
178181

179-
let result = function_handler(event, &mock).await.unwrap();
182+
let result = function_handler(event, 10, &mock).await.unwrap();
183+
184+
assert_eq!((), result);
185+
}
186+
187+
fn get_file(name: &str) -> Vec<u8> {
188+
let f = File::open(name);
189+
let mut reader = BufReader::new(f.unwrap());
190+
let mut buffer = Vec::new();
191+
192+
reader.read_to_end(&mut buffer).unwrap();
180193

181-
assert_eq!("Done", result);
194+
return buffer;
182195
}
183196

184197
fn get_s3_event(event_name: &str, bucket_name: &str, object_key: &str) -> S3Event {

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

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

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

Lines changed: 0 additions & 78 deletions
This file was deleted.
282 Bytes
Loading
Loading

0 commit comments

Comments
 (0)