Skip to content

Commit 34a35dd

Browse files
committed
add first draft version of the app
1 parent cdb4990 commit 34a35dd

File tree

10 files changed

+318
-7
lines changed

10 files changed

+318
-7
lines changed

.gitignore

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,6 @@
66
.coverage.*
77
htmlcov
88

9-
.cache
10-
.filesystem
11-
/infra/
12-
localstack/infra/
13-
149
/node_modules/
1510
package-lock.json
1611
/nosetests.xml
@@ -57,8 +52,10 @@ requirements.copy.txt
5752
venv
5853
api_states
5954

60-
/integration/lambdas/golang/handler.zip
61-
/tests/integration/lambdas/golang/handler.zip
6255
tmp/
6356

6457
volume/
58+
59+
# lambda packages
60+
lambdas/*/package/
61+
lambdas/*/lambda.zip

lambdas/presign/handler.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import json
2+
import typing
3+
4+
import boto3
5+
from botocore.exceptions import ClientError
6+
7+
if typing.TYPE_CHECKING:
8+
from mypy_boto3_s3 import S3Client
9+
from mypy_boto3_ssm import SSMClient
10+
11+
s3: "S3Client" = boto3.client("s3")
12+
ssm: "SSMClient" = boto3.client("ssm")
13+
14+
15+
def get_bucket_name() -> str:
16+
parameter = ssm.get_parameter(Name="/localstack-thumbnail-app/buckets/images")
17+
return parameter["Parameter"]["Value"]
18+
19+
20+
def handler(event, context):
21+
bucket = get_bucket_name()
22+
23+
key = event["rawPath"].lstrip("/")
24+
if not key:
25+
raise ValueError("no key given")
26+
27+
# make sure the bucket exists
28+
s3.create_bucket(Bucket=bucket)
29+
30+
# make sure the object does not exist
31+
try:
32+
s3.head_object(Bucket=bucket, Key=key)
33+
return {"statusCode": 409, "body": f"{bucket}/{key} already exists"}
34+
except ClientError as e:
35+
if e.response["ResponseMetadata"]["HTTPStatusCode"] != 404:
36+
raise
37+
38+
# generate the pre-signed POST url
39+
url = s3.generate_presigned_post(Bucket=bucket, Key=key)
40+
41+
# return it!
42+
return {"statusCode": 200, "body": json.dumps(url)}
43+
44+
45+
if __name__ == "__main__":
46+
print(handler(None, None))

lambdas/resize/handler.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# adapted from https://docs.aws.amazon.com/lambda/latest/dg/with-s3-tutorial.html
2+
import typing
3+
import uuid
4+
from urllib.parse import unquote_plus
5+
6+
import boto3
7+
from PIL import Image
8+
9+
if typing.TYPE_CHECKING:
10+
from mypy_boto3_s3 import S3Client
11+
from mypy_boto3_ssm import SSMClient
12+
13+
s3: "S3Client" = boto3.client("s3")
14+
ssm: "SSMClient" = boto3.client("ssm")
15+
16+
17+
def get_bucket_name() -> str:
18+
parameter = ssm.get_parameter(Name="/localstack-thumbnail-app/buckets/resized")
19+
return parameter["Parameter"]["Value"]
20+
21+
22+
def resize_image(image_path, resized_path):
23+
with Image.open(image_path) as image:
24+
image.thumbnail(tuple(int(x / 2) for x in image.size))
25+
image.save(resized_path)
26+
27+
28+
def download_and_resize(bucket, key) -> str:
29+
tmpkey = key.replace("/", "")
30+
download_path = f"/tmp/{uuid.uuid4()}{tmpkey}"
31+
upload_path = f"/tmp/resized-{tmpkey}"
32+
s3.download_file(bucket, key, download_path)
33+
resize_image(download_path, upload_path)
34+
return upload_path
35+
36+
37+
def handler(event, context):
38+
target_bucket = get_bucket_name()
39+
40+
for record in event["Records"]:
41+
source_bucket = record['s3']['bucket']['name']
42+
key = unquote_plus(record["s3"]["object"]["key"])
43+
print(source_bucket, key)
44+
45+
resized_path = download_and_resize(source_bucket, key)
46+
s3.upload_file(resized_path, target_bucket, key)

lambdas/resize/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Pillow==9.2.0

requirements-dev.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
boto3
2+
requests>=2.20
3+
Pillow==9.2.0
4+
mypy_boto3_ssm
5+
mypy_boto3_sns
6+
mypy_boto3_s3
7+
black
8+
pytest

tests/nyan-cat.png

12 KB
Loading

tests/some-file.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
not an image!
2+
when processing this file, the lambda will fail.

tests/test_integration.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import os
2+
import time
3+
import typing
4+
import uuid
5+
6+
import boto3
7+
import requests
8+
9+
if typing.TYPE_CHECKING:
10+
from mypy_boto3_s3 import S3Client
11+
from mypy_boto3_ssm import SSMClient
12+
13+
s3: "S3Client" = boto3.client(
14+
"s3", endpoint_url="http://localhost.localstack.cloud:4566"
15+
)
16+
ssm: "SSMClient" = boto3.client(
17+
"ssm", endpoint_url="http://localhost.localstack.cloud:4566"
18+
)
19+
20+
21+
def test_s3_resize_integration():
22+
file = "nyan-cat.png"
23+
key = os.path.basename(file)
24+
25+
parameter = ssm.get_parameter(Name="/localstack-thumbnail-app/buckets/images")
26+
source_bucket = parameter["Parameter"]["Value"]
27+
28+
parameter = ssm.get_parameter(Name="/localstack-thumbnail-app/buckets/resized")
29+
target_bucket = parameter["Parameter"]["Value"]
30+
31+
s3.upload_file(file, Bucket=source_bucket, Key=key)
32+
33+
# try for 15 seconds to wait for the resized image
34+
for i in range(15):
35+
try:
36+
s3.head_object(Bucket=target_bucket, Key=key)
37+
break
38+
except:
39+
pass
40+
time.sleep(1)
41+
42+
s3.head_object(Bucket=target_bucket, Key=key)
43+
s3.download_file(
44+
Bucket=target_bucket, Key=key, Filename="/tmp/nyan-cat-resized.png"
45+
)
46+
47+
assert (
48+
os.stat("/tmp/nyan-cat-resized.png").st_size < os.stat(key).st_size
49+
)
50+
51+
s3.delete_object(Bucket=source_bucket, Key=key)
52+
s3.delete_object(Bucket=target_bucket, Key=key)
53+
54+
55+
def test_failure_sns_to_ses_integration():
56+
file = "some-file.txt"
57+
key = f"{uuid.uuid4()}-{os.path.basename(file)}"
58+
59+
parameter = ssm.get_parameter(Name="/localstack-thumbnail-app/buckets/images")
60+
source_bucket = parameter["Parameter"]["Value"]
61+
62+
s3.upload_file(file, Bucket=source_bucket, Key=key)
63+
64+
def _check_message():
65+
response = requests.get("http://localhost:4566/_localstack/ses")
66+
messages = response.json()['messages']
67+
assert key in messages[-1]['Body']['text_part']
68+
69+
# retry to check for the message
70+
for i in range(9):
71+
try:
72+
_check_message()
73+
except:
74+
time.sleep(1)
75+
_check_message()
76+
77+
# clean up resources
78+
s3.delete_object(Bucket=source_bucket, Key=key)

website/app.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
(function ($) {
2+
$("#uploadForm").submit(function (event) {
3+
$("#uploadForm button").addClass('disabled');
4+
5+
if (event.preventDefault)
6+
event.preventDefault();
7+
else
8+
event.returnValue = false;
9+
10+
event.preventDefault();
11+
12+
let fileName = $("#customFile").val().replace(/C:\\fakepath\\/i, '');
13+
let presignerUrl = $("#presignerUrl").val();
14+
15+
// modify the original form
16+
console.log(fileName, presignerUrl);
17+
18+
let urlToCall = presignerUrl + "/" + fileName
19+
console.log(urlToCall);
20+
21+
let form = this;
22+
23+
$.ajax({
24+
url: urlToCall,
25+
success: function (data) {
26+
console.log("got pre-signed POST URL", data);
27+
28+
// set form fields to make it easier to serialize
29+
let fields = data['fields'];
30+
$(form).attr("action", data['url']);
31+
for (let key in fields) {
32+
$("#" + key).val(fields[key]);
33+
}
34+
35+
$.ajax({
36+
type: 'POST',
37+
url: data['url'],
38+
data: new FormData($("#uploadForm")[0]),
39+
processData: false,
40+
contentType: false,
41+
success: function () {
42+
alert("success!");
43+
},
44+
complete: function (event) {
45+
console.log("done", event);
46+
alert("error uploading. check the logs!")
47+
$("#uploadForm button").removeClass('disabled');
48+
}
49+
});
50+
},
51+
error: function (e) {
52+
console.log("error", e);
53+
alert("error getting pre-signed URL. check the logs!");
54+
$("#uploadForm button").removeClass('disabled');
55+
}
56+
});
57+
});
58+
})(jQuery);

website/index.html

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>Serverless image resizer</title>
6+
7+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
8+
rel="stylesheet"
9+
integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
10+
crossorigin="anonymous">
11+
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.2/font/bootstrap-icons.css"
12+
rel="stylesheet">
13+
</head>
14+
<body>
15+
16+
<div class="col-lg-8 mx-auto p-4 py-md-5">
17+
<header class="d-flex align-items-center pb-3 mb-5 border-bottom">
18+
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
19+
<span class="fs-5"><i class="bi bi-images fs-4"></i> Serverless thumbnail generator</span>
20+
</a>
21+
</header>
22+
23+
<main>
24+
<h1 class="h4">Upload your file <i class="bi bi-cloud-upload"></i></h1>
25+
<hr>
26+
<div class="card">
27+
<div class="card-header">
28+
Input
29+
</div>
30+
<div class="card-body">
31+
<h5 class="card-title">Form</h5>
32+
<p class="card-text">This form requests from a Lambda a S3 pre-signed POST URL, and then forwards the
33+
upload there.</p>
34+
<form id="uploadForm" action="#" , method="post">
35+
<div class="mb-3">
36+
<label class="form-label" for="presignerUrl">Function URL of the pre-sign Lambda</label>
37+
<input type="text" class="form-control" id="presignerUrl" required/>
38+
</div>
39+
<div class="mb-3">
40+
<label class="form-label" for="customFile">Select your file to upload</label>
41+
<input type="file" class="form-control" id="customFile" name="file" required/>
42+
</div>
43+
<div class="mb-3">
44+
<button type="submit" class="btn btn-primary mb-3">Upload <i
45+
class="bi bi-cloud-upload-fill"></i></button>
46+
</div>
47+
48+
<input type="hidden" name="key" id="key">
49+
<input type="hidden" name="AWSAccessKeyId" id="AWSAccessKeyId">
50+
<input type="hidden" name="policy" id="policy">
51+
<input type="hidden" name="signature" id="signature">
52+
</form>
53+
</div>
54+
</div>
55+
56+
</main>
57+
58+
<footer class="pt-5 my-5 text-muted border-top">
59+
Created by the LocalStack team - &copy; 2022
60+
</footer>
61+
</div>
62+
63+
<script src="https://code.jquery.com/jquery-3.6.1.min.js"
64+
integrity="sha256-o88AwQnZB+VDvE9tvIXrMQaPlFFSUTR+nldQm1LuPXQ="
65+
crossorigin="anonymous"></script>
66+
67+
<!-- JavaScript Bundle with Popper -->
68+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
69+
integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
70+
crossorigin="anonymous"></script>
71+
72+
<!-- client app -->
73+
<script src="app.js"></script>
74+
</body>
75+
</html>

0 commit comments

Comments
 (0)