mirror of
https://github.com/protomaps/PMTiles.git
synced 2026-02-04 10:51:07 +00:00
tile_path and error handling for lambda
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
import { Readable } from "stream";
|
||||
import { Context, APIGatewayProxyResult, APIGatewayEvent } from "aws-lambda";
|
||||
import {
|
||||
Context,
|
||||
APIGatewayProxyResult,
|
||||
APIGatewayProxyEventV2,
|
||||
} from "aws-lambda";
|
||||
import {
|
||||
PMTiles,
|
||||
ResolvedValueCache,
|
||||
RangeResponse,
|
||||
Source,
|
||||
Compression,
|
||||
} from "../../../js";
|
||||
|
||||
// @ts-ignore
|
||||
import https from "https";
|
||||
|
||||
// @ts-ignore
|
||||
import s3client from "/var/runtime/node_modules/aws-sdk/clients/s3.js";
|
||||
|
||||
@@ -21,6 +27,47 @@ const s3 = new s3client({
|
||||
// TODO: figure out how much memory to allocate
|
||||
const CACHE = new ResolvedValueCache();
|
||||
|
||||
export const pmtiles_path = (name: string, setting?: string): string => {
|
||||
if (setting) {
|
||||
return setting.replace("{name}", name);
|
||||
}
|
||||
return name + ".pmtiles";
|
||||
};
|
||||
|
||||
const TILE =
|
||||
/^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/;
|
||||
|
||||
export const tile_path = (
|
||||
path: string,
|
||||
setting?: string
|
||||
): {
|
||||
ok: boolean;
|
||||
name: string;
|
||||
tile: [number, number, number];
|
||||
ext: string;
|
||||
} => {
|
||||
let pattern = TILE;
|
||||
if (setting) {
|
||||
// escape regex
|
||||
setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&");
|
||||
setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)");
|
||||
setting = setting.replace("{z}", "(?<Z>\\d+)");
|
||||
setting = setting.replace("{x}", "(?<X>\\d+)");
|
||||
setting = setting.replace("{y}", "(?<Y>\\d+)");
|
||||
setting = setting.replace("{ext}", "(?<EXT>[a-z]+)");
|
||||
pattern = new RegExp(setting);
|
||||
}
|
||||
|
||||
let match = path.match(pattern);
|
||||
|
||||
if (match) {
|
||||
const g = match.groups!;
|
||||
return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT };
|
||||
}
|
||||
return { ok: false, name: "", tile: [0, 0, 0], ext: "" };
|
||||
};
|
||||
|
||||
|
||||
class S3Source implements Source {
|
||||
archive_name: string;
|
||||
|
||||
@@ -36,7 +83,7 @@ class S3Source implements Source {
|
||||
const resp = await s3
|
||||
.getObject({
|
||||
Bucket: process.env.BUCKET!,
|
||||
Key: this.archive_name + ".pmtiles",
|
||||
Key: pmtiles_path(this.archive_name, process.env.PMTILES_PATH),
|
||||
Range: "bytes=" + offset + "-" + (offset + length - 1),
|
||||
})
|
||||
.promise();
|
||||
@@ -45,37 +92,86 @@ class S3Source implements Source {
|
||||
}
|
||||
}
|
||||
|
||||
interface Headers {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const apiResp = (
|
||||
statusCode: number,
|
||||
body: string,
|
||||
isBase64Encoded = false,
|
||||
headers: Headers = {}
|
||||
): APIGatewayProxyResult => {
|
||||
return {
|
||||
statusCode: statusCode,
|
||||
body: body,
|
||||
headers: headers,
|
||||
isBase64Encoded: isBase64Encoded,
|
||||
};
|
||||
};
|
||||
|
||||
// Assumes event is a API Gateway V2 or Lambda Function URL formatted dict
|
||||
// and returns API Gateway V2 / Lambda Function dict responses
|
||||
// Does not work with CloudFront events/Lambda@Edge; see README
|
||||
export const handler = async (
|
||||
event: APIGatewayEvent,
|
||||
event: APIGatewayProxyEventV2,
|
||||
context: Context
|
||||
): Promise<APIGatewayProxyResult> => {
|
||||
try {
|
||||
var path;
|
||||
var is_api_gateway;
|
||||
if (event.pathParameters) {
|
||||
is_api_gateway = true;
|
||||
if (event.pathParameters.proxy) {
|
||||
path = "/" + event.pathParameters.proxy;
|
||||
} else {
|
||||
return apiResp(500, "Proxy integration missing tile_path parameter");
|
||||
}
|
||||
} else {
|
||||
path = event.rawPath;
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
return apiResp(500, "Invalid event configuration");
|
||||
}
|
||||
|
||||
const { ok, name, tile } = tile_path(path, process.env.TILE_PATH);
|
||||
|
||||
if (!ok) {
|
||||
return apiResp(400, "Invalid tile URL");
|
||||
}
|
||||
|
||||
var headers: Headers = {};
|
||||
if (process.env.CORS) {
|
||||
headers["Access-Control-Allow-Origin"] = process.env.CORS;
|
||||
}
|
||||
// TODO: extension enforcement and MIME types
|
||||
|
||||
const source = new S3Source("stamen_toner_z3");
|
||||
const p = new PMTiles(source, CACHE);
|
||||
try {
|
||||
const header = await p.getHeader();
|
||||
// TODO optimize by checking min/max zoom, return 404
|
||||
|
||||
headers["Content-Type"] = "application/vnd.vector-tile";
|
||||
|
||||
const tile = await p.getZxy(0, 0, 0);
|
||||
if (tile) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: Buffer.from(tile.data).toString("base64"),
|
||||
};
|
||||
// return suncompressed response
|
||||
// TODO: may need to special case API gateway to return compressed response with gzip content-encoding header
|
||||
return apiResp(
|
||||
200,
|
||||
Buffer.from(tile.data).toString("base64"),
|
||||
true,
|
||||
headers
|
||||
);
|
||||
} else {
|
||||
return {
|
||||
statusCode: 204,
|
||||
body: "",
|
||||
};
|
||||
return apiResp(204, "", false, headers);
|
||||
}
|
||||
} catch (e) {
|
||||
if ((e as Error).name === "AccessDenied") {
|
||||
return {
|
||||
statusCode: 403,
|
||||
body: "Bucket access failed: Unauthorized",
|
||||
};
|
||||
return apiResp(403, "Bucket access unauthorized");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return {
|
||||
statusCode: 404,
|
||||
body: "Invalid URL",
|
||||
};
|
||||
return apiResp(404, "Invalid URL");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user