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 { Readable } from "stream";
|
||||||
import { Context, APIGatewayProxyResult, APIGatewayEvent } from "aws-lambda";
|
import {
|
||||||
|
Context,
|
||||||
|
APIGatewayProxyResult,
|
||||||
|
APIGatewayProxyEventV2,
|
||||||
|
} from "aws-lambda";
|
||||||
import {
|
import {
|
||||||
PMTiles,
|
PMTiles,
|
||||||
ResolvedValueCache,
|
ResolvedValueCache,
|
||||||
RangeResponse,
|
RangeResponse,
|
||||||
Source,
|
Source,
|
||||||
|
Compression,
|
||||||
} from "../../../js";
|
} from "../../../js";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import https from "https";
|
import https from "https";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import s3client from "/var/runtime/node_modules/aws-sdk/clients/s3.js";
|
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
|
// TODO: figure out how much memory to allocate
|
||||||
const CACHE = new ResolvedValueCache();
|
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 {
|
class S3Source implements Source {
|
||||||
archive_name: string;
|
archive_name: string;
|
||||||
|
|
||||||
@@ -36,7 +83,7 @@ class S3Source implements Source {
|
|||||||
const resp = await s3
|
const resp = await s3
|
||||||
.getObject({
|
.getObject({
|
||||||
Bucket: process.env.BUCKET!,
|
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),
|
Range: "bytes=" + offset + "-" + (offset + length - 1),
|
||||||
})
|
})
|
||||||
.promise();
|
.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 (
|
export const handler = async (
|
||||||
event: APIGatewayEvent,
|
event: APIGatewayProxyEventV2,
|
||||||
context: Context
|
context: Context
|
||||||
): Promise<APIGatewayProxyResult> => {
|
): 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 source = new S3Source("stamen_toner_z3");
|
||||||
const p = new PMTiles(source, CACHE);
|
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);
|
const tile = await p.getZxy(0, 0, 0);
|
||||||
if (tile) {
|
if (tile) {
|
||||||
return {
|
// return suncompressed response
|
||||||
statusCode: 200,
|
// TODO: may need to special case API gateway to return compressed response with gzip content-encoding header
|
||||||
body: Buffer.from(tile.data).toString("base64"),
|
return apiResp(
|
||||||
};
|
200,
|
||||||
|
Buffer.from(tile.data).toString("base64"),
|
||||||
|
true,
|
||||||
|
headers
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return {
|
return apiResp(204, "", false, headers);
|
||||||
statusCode: 204,
|
|
||||||
body: "",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ((e as Error).name === "AccessDenied") {
|
if ((e as Error).name === "AccessDenied") {
|
||||||
return {
|
return apiResp(403, "Bucket access unauthorized");
|
||||||
statusCode: 403,
|
|
||||||
body: "Bucket access failed: Unauthorized",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
return {
|
return apiResp(404, "Invalid URL");
|
||||||
statusCode: 404,
|
|
||||||
body: "Invalid URL",
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user