Skip to content

Commit aa80e74

Browse files
authored
Examples basic s3 object lambda thumbnail (#664)
* fix example basic-s3-thumbnail test * basic-s3-object-lambda-thumbnail example (#625) Forwards a thumbnail to the user instead of the requested file
1 parent b2451e9 commit aa80e74

File tree

8 files changed

+346
-3
lines changed

8 files changed

+346
-3
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[package]
2+
name = "basic-s3-object-lambda-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.8.3"
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.55.3"
25+
aws-sdk-s3 = "0.28.0"
26+
thumbnailer = "0.4.0"
27+
mime = "0.3.16"
28+
async-trait = "0.1.66"
29+
ureq = "2.6.2"
30+
aws-smithy-http = "0.55.3"
31+
32+
[dev-dependencies]
33+
mockall = "0.11.3"
34+
tokio-test = "0.4.2"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# AWS S3 Object Lambda Function
2+
3+
It uses a GetObject event and it returns with a thumbnail instead of the real
4+
object from the S3 bucket.
5+
The thumbnail was tested only witn PNG files.
6+
7+
## Build & Deploy
8+
9+
1. Install [cargo-lambda](https://github.com/cargo-lambda/cargo-lambda#installation)
10+
2. Build the function with `cargo lambda build --release --arm64 --output-format zip`
11+
3. Upload the bootstrap.zip file from the directory:`target/lambda/basic-s3-object-lambda-thumbnail/`
12+
13+
## Setup on AWS S3
14+
15+
1. You need a bucket and upload a PNG file to that bucket
16+
2. Set Access Point for that bucket
17+
3. Set Object Lambda Access Point for the access point and use the uploaded lambda function as a transformer
18+
19+
## Set Up on AWS Lambda
20+
21+
0. Click on Code tab
22+
1. Runtime settings - runtime: Custom runtime on Amazon Linux 2
23+
2. Runtime settings - Architecture: arm64
24+
25+
## Set Up on AWS IAM
26+
27+
1. Click on Roles
28+
2. Search the lambda function name
29+
3. Add the permission: AmazonS3ObjectLambdaExecutionRolePolicy
30+
31+
## How to check this lambda
32+
33+
1. Go to S3
34+
2. Click on Object Lambda Access Point
35+
3. Click on your object lambda access point name
36+
4. click on one uploaded PNG file
37+
5. Click on the activated Open button
38+
39+
### Expected:
40+
A new browser tab opens with a 128x128 thumbnail
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
use std::{error, io::Cursor};
2+
3+
use aws_lambda_events::s3::object_lambda::{GetObjectContext, S3ObjectLambdaEvent};
4+
use aws_sdk_s3::Client as S3Client;
5+
use lambda_runtime::{run, service_fn, Error, LambdaEvent};
6+
use s3::{GetFile, SendFile};
7+
use thumbnailer::{create_thumbnails, ThumbnailSize};
8+
9+
mod s3;
10+
11+
/**
12+
This s3 object lambda handler
13+
* downloads the asked file
14+
* creates a PNG thumbnail from it
15+
* forwards it to the browser
16+
*/
17+
pub(crate) async fn function_handler<T: SendFile + GetFile>(
18+
event: LambdaEvent<S3ObjectLambdaEvent>,
19+
size: u32,
20+
client: &T,
21+
) -> Result<String, Box<dyn error::Error>> {
22+
tracing::info!("handler starts");
23+
24+
let context: GetObjectContext = event.payload.get_object_context.unwrap();
25+
26+
let route = context.output_route;
27+
let token = context.output_token;
28+
let s3_url = context.input_s3_url;
29+
30+
tracing::info!("Route: {}, s3_url: {}", route, s3_url);
31+
32+
let image = client.get_file(s3_url)?;
33+
tracing::info!("Image loaded. Length: {}", image.len());
34+
35+
let thumbnail = get_thumbnail(image, size);
36+
tracing::info!("thumbnail created. Length: {}", thumbnail.len());
37+
38+
// It sends the thumbnail back to the user
39+
40+
client.send_file(route, token, thumbnail).await
41+
42+
/*
43+
match client.send_file(route, token, thumbnail).await {
44+
Ok(msg) => tracing::info!(msg),
45+
Err(msg) => tracing::info!(msg)
46+
};
47+
48+
tracing::info!("handler ends");
49+
50+
Ok(())
51+
*/
52+
}
53+
54+
fn get_thumbnail(vec: Vec<u8>, size: u32) -> Vec<u8> {
55+
let reader = Cursor::new(vec);
56+
let mut thumbnails = create_thumbnails(reader, mime::IMAGE_PNG, [ThumbnailSize::Custom((size, size))]).unwrap();
57+
58+
let thumbnail = thumbnails.pop().unwrap();
59+
let mut buf = Cursor::new(Vec::new());
60+
thumbnail.write_png(&mut buf).unwrap();
61+
62+
buf.into_inner()
63+
}
64+
65+
#[tokio::main]
66+
async fn main() -> Result<(), Error> {
67+
// required to enable CloudWatch error logging by the runtime
68+
tracing_subscriber::fmt()
69+
.with_max_level(tracing::Level::TRACE)
70+
// disable printing the name of the module in every log line.
71+
.with_target(false)
72+
// this needs to be set to false, otherwise ANSI color codes will
73+
// show up in a confusing manner in CloudWatch logs.
74+
.with_ansi(false)
75+
// disabling time is handy because CloudWatch will add the ingestion time.
76+
.without_time()
77+
.init();
78+
79+
let shared_config = aws_config::load_from_env().await;
80+
let client = S3Client::new(&shared_config);
81+
let client_ref = &client;
82+
83+
let func = service_fn(move |event| async move { function_handler(event, 128, client_ref).await });
84+
85+
let _ = run(func).await;
86+
87+
Ok(())
88+
}
89+
90+
#[cfg(test)]
91+
mod tests {
92+
use std::fs::File;
93+
use std::io::BufReader;
94+
use std::io::Read;
95+
96+
use super::*;
97+
use async_trait::async_trait;
98+
use aws_lambda_events::s3::object_lambda::Configuration;
99+
use aws_lambda_events::s3::object_lambda::HeadObjectContext;
100+
use aws_lambda_events::s3::object_lambda::ListObjectsContext;
101+
use aws_lambda_events::s3::object_lambda::ListObjectsV2Context;
102+
use aws_lambda_events::s3::object_lambda::UserIdentity;
103+
use aws_lambda_events::s3::object_lambda::UserRequest;
104+
use aws_lambda_events::serde_json::json;
105+
use lambda_runtime::{Context, LambdaEvent};
106+
use mockall::mock;
107+
use s3::GetFile;
108+
use s3::SendFile;
109+
110+
#[tokio::test]
111+
async fn response_is_good() {
112+
mock! {
113+
FakeS3Client {}
114+
115+
#[async_trait]
116+
impl GetFile for FakeS3Client {
117+
pub fn get_file(&self, url: String) -> Result<Vec<u8>, Box<dyn error::Error>>;
118+
}
119+
#[async_trait]
120+
impl SendFile for FakeS3Client {
121+
pub async fn send_file(&self, route: String, token: String, vec: Vec<u8>) -> Result<String, Box<dyn error::Error>>;
122+
}
123+
}
124+
125+
let mut mock = MockFakeS3Client::new();
126+
127+
mock.expect_get_file()
128+
.withf(|u: &String| u.eq("S3_URL"))
129+
.returning(|_1| Ok(get_file("testdata/image.png")));
130+
131+
mock.expect_send_file()
132+
.withf(|r: &String, t: &String, by| {
133+
let thumbnail = get_file("testdata/thumbnail.png");
134+
return r.eq("O_ROUTE") && t.eq("O_TOKEN") && by == &thumbnail;
135+
})
136+
.returning(|_1, _2, _3| Ok("File sent.".to_string()));
137+
138+
let payload = get_s3_event();
139+
let context = Context::default();
140+
let event = LambdaEvent { payload, context };
141+
142+
let result = function_handler(event, 10, &mock).await.unwrap();
143+
144+
assert_eq!(("File sent."), result);
145+
}
146+
147+
fn get_file(name: &str) -> Vec<u8> {
148+
let f = File::open(name);
149+
let mut reader = BufReader::new(f.unwrap());
150+
let mut buffer = Vec::new();
151+
152+
reader.read_to_end(&mut buffer).unwrap();
153+
154+
return buffer;
155+
}
156+
157+
fn get_s3_event() -> S3ObjectLambdaEvent {
158+
return S3ObjectLambdaEvent {
159+
x_amz_request_id: ("ID".to_string()),
160+
head_object_context: (Some(HeadObjectContext::default())),
161+
list_objects_context: (Some(ListObjectsContext::default())),
162+
get_object_context: (Some(GetObjectContext {
163+
input_s3_url: ("S3_URL".to_string()),
164+
output_route: ("O_ROUTE".to_string()),
165+
output_token: ("O_TOKEN".to_string()),
166+
})),
167+
list_objects_v2_context: (Some(ListObjectsV2Context::default())),
168+
protocol_version: ("VERSION".to_string()),
169+
user_identity: (UserIdentity::default()),
170+
user_request: (UserRequest::default()),
171+
configuration: (Configuration {
172+
access_point_arn: ("APRN".to_string()),
173+
supporting_access_point_arn: ("SAPRN".to_string()),
174+
payload: (json!(null)),
175+
}),
176+
};
177+
}
178+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use async_trait::async_trait;
2+
use aws_sdk_s3::{operation::write_get_object_response::WriteGetObjectResponseError, Client as S3Client};
3+
use aws_smithy_http::{byte_stream::ByteStream, result::SdkError};
4+
use std::{error, io::Read};
5+
6+
pub trait GetFile {
7+
fn get_file(&self, url: String) -> Result<Vec<u8>, Box<dyn error::Error>>;
8+
}
9+
10+
#[async_trait]
11+
pub trait SendFile {
12+
async fn send_file(&self, route: String, token: String, vec: Vec<u8>) -> Result<String, Box<dyn error::Error>>;
13+
}
14+
15+
impl GetFile for S3Client {
16+
fn get_file(&self, url: String) -> Result<Vec<u8>, Box<dyn error::Error>> {
17+
tracing::info!("get file url {}", url);
18+
19+
let resp = ureq::get(&url).call()?;
20+
let len: usize = resp.header("Content-Length").unwrap().parse()?;
21+
22+
let mut bytes: Vec<u8> = Vec::with_capacity(len);
23+
24+
std::io::Read::take(resp.into_reader(), 10_000_000).read_to_end(&mut bytes)?;
25+
26+
tracing::info!("got {} bytes", bytes.len());
27+
28+
Ok(bytes)
29+
}
30+
}
31+
32+
#[async_trait]
33+
impl SendFile for S3Client {
34+
async fn send_file(&self, route: String, token: String, vec: Vec<u8>) -> Result<String, Box<dyn error::Error>> {
35+
tracing::info!("send file route {}, token {}, length {}", route, token, vec.len());
36+
37+
let bytes = ByteStream::from(vec);
38+
39+
let write = self
40+
.write_get_object_response()
41+
.request_route(route)
42+
.request_token(token)
43+
.status_code(200)
44+
.body(bytes)
45+
.send()
46+
.await;
47+
48+
if write.is_err() {
49+
let sdk_error = write.err().unwrap();
50+
check_error(sdk_error);
51+
Err("WriteGetObjectResponse creation error".into())
52+
} else {
53+
Ok("File sent.".to_string())
54+
}
55+
}
56+
}
57+
58+
fn check_error(error: SdkError<WriteGetObjectResponseError>) {
59+
match error {
60+
SdkError::ConstructionFailure(_err) => {
61+
tracing::info!("ConstructionFailure");
62+
}
63+
SdkError::DispatchFailure(err) => {
64+
tracing::info!("DispatchFailure");
65+
if err.is_io() {
66+
tracing::info!("IO error");
67+
};
68+
if err.is_timeout() {
69+
tracing::info!("Timeout error");
70+
};
71+
if err.is_user() {
72+
tracing::info!("User error");
73+
};
74+
if err.is_other().is_some() {
75+
tracing::info!("Other error");
76+
};
77+
}
78+
SdkError::ResponseError(_err) => tracing::info!("ResponseError"),
79+
SdkError::TimeoutError(_err) => tracing::info!("TimeoutError"),
80+
SdkError::ServiceError(err) => {
81+
tracing::info!("ServiceError");
82+
let wgore = err.into_err();
83+
let meta = wgore.meta();
84+
let code = meta.code().unwrap_or_default();
85+
let msg = meta.message().unwrap_or_default();
86+
tracing::info!("code: {}, message: {}, meta: {}", code, msg, meta);
87+
}
88+
_ => tracing::info!("other error"),
89+
}
90+
}
Loading
Loading

examples/basic-s3-thumbnail/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,4 @@ async-trait = "0.1.66"
3030
[dev-dependencies]
3131
mockall = "0.11.3"
3232
tokio-test = "0.4.2"
33+
chrono = "0.4"

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ mod tests {
138138

139139
use super::*;
140140
use async_trait::async_trait;
141-
use aws_lambda_events::chrono::DateTime;
141+
//use aws_lambda_events::chrono::DateTime;
142142
use aws_lambda_events::s3::S3Bucket;
143143
use aws_lambda_events::s3::S3Entity;
144144
use aws_lambda_events::s3::S3Object;
@@ -214,7 +214,7 @@ mod tests {
214214
configuration_id: (Some(String::default())),
215215
bucket: (S3Bucket {
216216
name: (Some(bucket_name.to_string())),
217-
owner_identity: (S3UserIdentity {
217+
owner_identity: Some(S3UserIdentity {
218218
principal_id: (Some(String::default())),
219219
}),
220220
arn: (Some(String::default())),
@@ -233,7 +233,7 @@ mod tests {
233233
event_version: (Some(String::default())),
234234
event_source: (Some(String::default())),
235235
aws_region: (Some(String::default())),
236-
event_time: (DateTime::default()),
236+
event_time: (chrono::DateTime::default()),
237237
event_name: (Some(event_name.to_string())),
238238
principal_id: (S3UserIdentity {
239239
principal_id: (Some("X".to_string())),

0 commit comments

Comments
 (0)