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 0000000..faaf4e3 Binary files /dev/null and b/serverless/aws/lambda_function.zip differ 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()