From 8a34c6ef31b91677882fda204154b450e0343828 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Tue, 12 Jul 2022 21:19:03 +0800 Subject: [PATCH] add Lambda python implementation --- serverless/aws/.gitignore | 1 + serverless/aws/config.py | 2 + serverless/aws/create_lambda_function.py | 20 ++++++ serverless/aws/lambda_function.py | 79 +++++++++++++++++++++++ serverless/aws/lambda_function.zip | Bin 0 -> 2218 bytes serverless/aws/test_lambda_function.py | 26 ++++++++ 6 files changed, 128 insertions(+) create mode 100644 serverless/aws/.gitignore create mode 100644 serverless/aws/config.py create mode 100644 serverless/aws/create_lambda_function.py create mode 100644 serverless/aws/lambda_function.py create mode 100644 serverless/aws/lambda_function.zip create mode 100644 serverless/aws/test_lambda_function.py diff --git a/serverless/aws/.gitignore b/serverless/aws/.gitignore new file mode 100644 index 0000000..d026543 --- /dev/null +++ b/serverless/aws/.gitignore @@ -0,0 +1 @@ +lambda_function.zip diff --git a/serverless/aws/config.py b/serverless/aws/config.py new file mode 100644 index 0000000..e475043 --- /dev/null +++ b/serverless/aws/config.py @@ -0,0 +1,2 @@ +BUCKET = "example-bucket-name" +REGION = "us-east-1" diff --git a/serverless/aws/create_lambda_function.py b/serverless/aws/create_lambda_function.py new file mode 100644 index 0000000..14f06af --- /dev/null +++ b/serverless/aws/create_lambda_function.py @@ -0,0 +1,20 @@ +import argparse +import os +import zipfile +import sys + +parser = argparse.ArgumentParser( + description="Create a deployment-ready PMTiles Lambda zip." +) +parser.add_argument("region", help="AWS Region of the S3 bucket.") +parser.add_argument("bucket", help="S3 Bucket Name.") +args = parser.parse_args() + +with zipfile.ZipFile("lambda_function.zip", "w", zipfile.ZIP_DEFLATED) as z: + z.write("lambda_function.py") + z.write("../../python/pmtiles/reader.py", "pmtiles.py") + info = zipfile.ZipInfo("config.py") + info.external_attr = 0o777 << 16 + z.writestr(info, f'REGION="{args.region}"\nBUCKET="{args.bucket}"') + +print(f"created lambda_function.zip with REGION {args.region} and BUCKET {args.bucket}") diff --git a/serverless/aws/lambda_function.py b/serverless/aws/lambda_function.py new file mode 100644 index 0000000..4076480 --- /dev/null +++ b/serverless/aws/lambda_function.py @@ -0,0 +1,79 @@ +import json +import pmtiles +import re +import boto3 +import base64 +import collections +from config import BUCKET, REGION + +Zxy = collections.namedtuple("Zxy", ["z", "x", "y"]) + +s3 = boto3.client("s3") + +rootCache = {} + + +def parse_tile_uri(str): + m = re.match("^(?:/([0-9a-zA-Z/!\-_\.\*'\(\)]+))?/(\d+)/(\d+)/(\d+).pbf$", str) + if not m: + return None, None + return (m.group(1), Zxy(int(m.group(2)), int(m.group(3)), int(m.group(4)))) + + +def cloudfrontResponse(status_code, body, body_b64=False, headers={}): + headers = {key: [{"value": value}] for key, value in headers.items()} + resp = {"status": status_code, "body": body, "headers": headers} + if body_b64: + resp["bodyEncoding"] = "base64" + return resp + + +def apiGatewayResponse(status_code, body, body_b64=False, headers={}): + resp = {"status": status_code, "body": body, "headers": headers} + if body_b64: + resp["isBase64Encoded"] = True + return resp + + +def lambda_handler(event, context): + if "Records" in event: + uri = event["Records"][0]["cf"]["request"]["uri"] # CloudFront Origin Request + lambdaResponse = cloudfrontResponse + else: + uri = event["rawPath"] # API Gateway and Lambda Function URLs + lambdaResponse = apiGatewayResponse + tileset, tile = parse_tile_uri(uri) + + if not tile: + return lambdaResponse(400, "Invalid tile URL") + + def get_bytes(offset, length): + global rootCache + if offset == 0 and length == 512000 and tileset in rootCache: + return rootCache[tileset] + + end = offset + length - 1 + result = ( + s3.get_object( + Bucket=BUCKET, + Key=tileset + ".pmtiles", + Range=f"bytes={offset}-{end}", + ) + .get("Body") + .read() + ) + if offset == 0 and length == 512000: + rootCache[tileset] = result + return result + + reader = pmtiles.Reader(get_bytes) + tile_data = reader.get(tile.z, tile.x, tile.y) + if not tile_data: + return lambdaResponse(404, "Tile not found") + + headers = { + "Content-Encoding": "gzip", + "Content-Type": "application/protobuf", + "Access-Control-Allow-Origin": "*", + } + return lambdaResponse(200, base64.b64encode(tile_data), True, headers) diff --git a/serverless/aws/lambda_function.zip b/serverless/aws/lambda_function.zip new file mode 100644 index 0000000000000000000000000000000000000000..faaf4e36b71d842226e4a96fce20e304e10d5512 GIT binary patch literal 2218 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 Mo6L;!%p?Ht2fGmELjV8( literal 0 HcmV?d00001 diff --git a/serverless/aws/test_lambda_function.py b/serverless/aws/test_lambda_function.py new file mode 100644 index 0000000..a3cf5c2 --- /dev/null +++ b/serverless/aws/test_lambda_function.py @@ -0,0 +1,26 @@ +import unittest +from lambda_function import parse_tile_uri, cloudfrontResponse, apiGatewayResponse + + +class TestLambda(unittest.TestCase): + def test_parse_regex(self): + tileset, tile = parse_tile_uri("/0/0/0.pbf") + self.assertEqual(tileset, None) + self.assertEqual(tile.x, 0) + self.assertEqual(tile.y, 0) + self.assertEqual(tile.z, 0) + + tileset, tile = parse_tile_uri("abcd") + self.assertEqual(tile, None) + + def test_cloudfront_response(self): + resp = cloudfrontResponse(200, "ok", False, {"a": "b"}) + self.assertEqual(resp["headers"]["a"], [{"value": "b"}]) + + def test_api_gateway_response(self): + resp = apiGatewayResponse(200, "ok", False, {"a": "b"}) + self.assertEqual(resp["headers"]["a"], "b") + + +if __name__ == "__main__": + unittest.main()