mirror of
https://github.com/protomaps/PMTiles.git
synced 2026-02-04 10:51:07 +00:00
serverless js: make formatting consistent
This commit is contained in:
@@ -1,16 +1,16 @@
|
|||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import {
|
import {
|
||||||
Context,
|
Context,
|
||||||
APIGatewayProxyResult,
|
APIGatewayProxyResult,
|
||||||
APIGatewayProxyEventV2,
|
APIGatewayProxyEventV2,
|
||||||
} from "aws-lambda";
|
} from "aws-lambda";
|
||||||
import {
|
import {
|
||||||
PMTiles,
|
PMTiles,
|
||||||
ResolvedValueCache,
|
ResolvedValueCache,
|
||||||
RangeResponse,
|
RangeResponse,
|
||||||
Source,
|
Source,
|
||||||
Compression,
|
Compression,
|
||||||
TileType,
|
TileType,
|
||||||
} from "../../../js/index";
|
} from "../../../js/index";
|
||||||
|
|
||||||
import https from "https";
|
import https from "https";
|
||||||
@@ -21,23 +21,23 @@ import { NodeHttpHandler } from "@aws-sdk/node-http-handler";
|
|||||||
|
|
||||||
// the region should default to the same one as the function
|
// the region should default to the same one as the function
|
||||||
const s3client = new S3Client({
|
const s3client = new S3Client({
|
||||||
requestHandler: new NodeHttpHandler({
|
requestHandler: new NodeHttpHandler({
|
||||||
connectionTimeout: 500,
|
connectionTimeout: 500,
|
||||||
socketTimeout: 500,
|
socketTimeout: 500,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
async function nativeDecompress(
|
async function nativeDecompress(
|
||||||
buf: ArrayBuffer,
|
buf: ArrayBuffer,
|
||||||
compression: Compression
|
compression: Compression
|
||||||
): Promise<ArrayBuffer> {
|
): Promise<ArrayBuffer> {
|
||||||
if (compression === Compression.None || compression === Compression.Unknown) {
|
if (compression === Compression.None || compression === Compression.Unknown) {
|
||||||
return buf;
|
return buf;
|
||||||
} else if (compression === Compression.Gzip) {
|
} else if (compression === Compression.Gzip) {
|
||||||
return zlib.gunzipSync(buf);
|
return zlib.gunzipSync(buf);
|
||||||
} else {
|
} else {
|
||||||
throw Error("Compression method not supported");
|
throw Error("Compression method not supported");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lambda needs to run with 512MB, empty function takes about 70
|
// Lambda needs to run with 512MB, empty function takes about 70
|
||||||
@@ -45,219 +45,219 @@ const CACHE = new ResolvedValueCache(undefined, undefined, nativeDecompress);
|
|||||||
|
|
||||||
// duplicated code below
|
// duplicated code below
|
||||||
export const pmtiles_path = (name: string, setting?: string): string => {
|
export const pmtiles_path = (name: string, setting?: string): string => {
|
||||||
if (setting) {
|
if (setting) {
|
||||||
return setting.replaceAll("{name}", name);
|
return setting.replaceAll("{name}", name);
|
||||||
}
|
}
|
||||||
return name + ".pmtiles";
|
return name + ".pmtiles";
|
||||||
};
|
};
|
||||||
|
|
||||||
const TILE =
|
const TILE =
|
||||||
/^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/;
|
/^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/;
|
||||||
|
|
||||||
export const tile_path = (
|
export const tile_path = (
|
||||||
path: string,
|
path: string,
|
||||||
setting?: string
|
setting?: string
|
||||||
): {
|
): {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
tile: [number, number, number];
|
tile: [number, number, number];
|
||||||
ext: string;
|
ext: string;
|
||||||
} => {
|
} => {
|
||||||
let pattern = TILE;
|
let pattern = TILE;
|
||||||
if (setting) {
|
if (setting) {
|
||||||
// escape regex
|
// escape regex
|
||||||
setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&");
|
setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&");
|
||||||
setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)");
|
setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)");
|
||||||
setting = setting.replace("{z}", "(?<Z>\\d+)");
|
setting = setting.replace("{z}", "(?<Z>\\d+)");
|
||||||
setting = setting.replace("{x}", "(?<X>\\d+)");
|
setting = setting.replace("{x}", "(?<X>\\d+)");
|
||||||
setting = setting.replace("{y}", "(?<Y>\\d+)");
|
setting = setting.replace("{y}", "(?<Y>\\d+)");
|
||||||
setting = setting.replace("{ext}", "(?<EXT>[a-z]+)");
|
setting = setting.replace("{ext}", "(?<EXT>[a-z]+)");
|
||||||
pattern = new RegExp(setting);
|
pattern = new RegExp(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
let match = path.match(pattern);
|
let match = path.match(pattern);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const g = match.groups!;
|
const g = match.groups!;
|
||||||
return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT };
|
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: "" };
|
return { ok: false, name: "", tile: [0, 0, 0], ext: "" };
|
||||||
};
|
};
|
||||||
|
|
||||||
class S3Source implements Source {
|
class S3Source implements Source {
|
||||||
archive_name: string;
|
archive_name: string;
|
||||||
|
|
||||||
constructor(archive_name: string) {
|
constructor(archive_name: string) {
|
||||||
this.archive_name = archive_name;
|
this.archive_name = archive_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
getKey() {
|
getKey() {
|
||||||
return this.archive_name;
|
return this.archive_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBytes(offset: number, length: number): Promise<RangeResponse> {
|
async getBytes(offset: number, length: number): Promise<RangeResponse> {
|
||||||
const resp = await s3client.send(
|
const resp = await s3client.send(
|
||||||
new GetObjectCommand({
|
new GetObjectCommand({
|
||||||
Bucket: process.env.BUCKET!,
|
Bucket: process.env.BUCKET!,
|
||||||
Key: pmtiles_path(this.archive_name, process.env.PMTILES_PATH),
|
Key: pmtiles_path(this.archive_name, process.env.PMTILES_PATH),
|
||||||
Range: "bytes=" + offset + "-" + (offset + length - 1),
|
Range: "bytes=" + offset + "-" + (offset + length - 1),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const arr = await resp.Body!.transformToByteArray();
|
const arr = await resp.Body!.transformToByteArray();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: arr.buffer,
|
data: arr.buffer,
|
||||||
etag: resp.ETag,
|
etag: resp.ETag,
|
||||||
expires: resp.Expires?.toISOString(),
|
expires: resp.Expires?.toISOString(),
|
||||||
cacheControl: resp.CacheControl,
|
cacheControl: resp.CacheControl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Headers {
|
interface Headers {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiResp = (
|
const apiResp = (
|
||||||
statusCode: number,
|
statusCode: number,
|
||||||
body: string,
|
body: string,
|
||||||
isBase64Encoded = false,
|
isBase64Encoded = false,
|
||||||
headers: Headers = {}
|
headers: Headers = {}
|
||||||
): APIGatewayProxyResult => {
|
): APIGatewayProxyResult => {
|
||||||
return {
|
return {
|
||||||
statusCode: statusCode,
|
statusCode: statusCode,
|
||||||
body: body,
|
body: body,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
isBase64Encoded: isBase64Encoded,
|
isBase64Encoded: isBase64Encoded,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Assumes event is a API Gateway V2 or Lambda Function URL formatted dict
|
// Assumes event is a API Gateway V2 or Lambda Function URL formatted dict
|
||||||
// and returns API Gateway V2 / Lambda Function dict responses
|
// and returns API Gateway V2 / Lambda Function dict responses
|
||||||
// Does not work with CloudFront events/Lambda@Edge; see README
|
// Does not work with CloudFront events/Lambda@Edge; see README
|
||||||
export const handlerRaw = async (
|
export const handlerRaw = async (
|
||||||
event: APIGatewayProxyEventV2,
|
event: APIGatewayProxyEventV2,
|
||||||
context: Context,
|
context: Context,
|
||||||
tilePostprocess?: (a: ArrayBuffer, t: TileType) => ArrayBuffer
|
tilePostprocess?: (a: ArrayBuffer, t: TileType) => ArrayBuffer
|
||||||
): Promise<APIGatewayProxyResult> => {
|
): Promise<APIGatewayProxyResult> => {
|
||||||
var path;
|
var path;
|
||||||
var is_api_gateway;
|
var is_api_gateway;
|
||||||
if (event.pathParameters) {
|
if (event.pathParameters) {
|
||||||
is_api_gateway = true;
|
is_api_gateway = true;
|
||||||
if (event.pathParameters.proxy) {
|
if (event.pathParameters.proxy) {
|
||||||
path = "/" + event.pathParameters.proxy;
|
path = "/" + event.pathParameters.proxy;
|
||||||
} else {
|
} else {
|
||||||
return apiResp(500, "Proxy integration missing tile_path parameter");
|
return apiResp(500, "Proxy integration missing tile_path parameter");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
path = event.rawPath;
|
path = event.rawPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return apiResp(500, "Invalid event configuration");
|
return apiResp(500, "Invalid event configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
var headers: Headers = {};
|
var headers: Headers = {};
|
||||||
// TODO: metadata and TileJSON
|
// TODO: metadata and TileJSON
|
||||||
|
|
||||||
if (process.env.CORS) {
|
if (process.env.CORS) {
|
||||||
headers["Access-Control-Allow-Origin"] = process.env.CORS;
|
headers["Access-Control-Allow-Origin"] = process.env.CORS;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ok, name, tile, ext } = tile_path(path, process.env.TILE_PATH);
|
const { ok, name, tile, ext } = tile_path(path, process.env.TILE_PATH);
|
||||||
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return apiResp(400, "Invalid tile URL", false, headers);
|
return apiResp(400, "Invalid tile URL", false, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = new S3Source(name);
|
const source = new S3Source(name);
|
||||||
const p = new PMTiles(source, CACHE, nativeDecompress);
|
const p = new PMTiles(source, CACHE, nativeDecompress);
|
||||||
try {
|
try {
|
||||||
const header = await p.getHeader();
|
const header = await p.getHeader();
|
||||||
if (tile[0] < header.minZoom || tile[0] > header.maxZoom) {
|
if (tile[0] < header.minZoom || tile[0] > header.maxZoom) {
|
||||||
return apiResp(404, "", false, headers);
|
return apiResp(404, "", false, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pair of [
|
for (const pair of [
|
||||||
[TileType.Mvt, "mvt"],
|
[TileType.Mvt, "mvt"],
|
||||||
[TileType.Png, "png"],
|
[TileType.Png, "png"],
|
||||||
[TileType.Jpeg, "jpg"],
|
[TileType.Jpeg, "jpg"],
|
||||||
[TileType.Webp, "webp"],
|
[TileType.Webp, "webp"],
|
||||||
]) {
|
]) {
|
||||||
if (header.tileType === pair[0] && ext !== pair[1]) {
|
if (header.tileType === pair[0] && ext !== pair[1]) {
|
||||||
if (header.tileType == TileType.Mvt && ext === "pbf") {
|
if (header.tileType == TileType.Mvt && ext === "pbf") {
|
||||||
// allow this for now. Eventually we will delete this in favor of .mvt
|
// allow this for now. Eventually we will delete this in favor of .mvt
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return apiResp(
|
return apiResp(
|
||||||
400,
|
400,
|
||||||
"Bad request: archive has type ." + pair[1],
|
"Bad request: archive has type ." + pair[1],
|
||||||
false,
|
false,
|
||||||
headers
|
headers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tile_result = await p.getZxy(tile[0], tile[1], tile[2]);
|
const tile_result = await p.getZxy(tile[0], tile[1], tile[2]);
|
||||||
if (tile_result) {
|
if (tile_result) {
|
||||||
switch (header.tileType) {
|
switch (header.tileType) {
|
||||||
case TileType.Mvt:
|
case TileType.Mvt:
|
||||||
// part of the list of Cloudfront compressible types.
|
// part of the list of Cloudfront compressible types.
|
||||||
headers["Content-Type"] = "application/vnd.mapbox-vector-tile";
|
headers["Content-Type"] = "application/vnd.mapbox-vector-tile";
|
||||||
break;
|
break;
|
||||||
case TileType.Png:
|
case TileType.Png:
|
||||||
headers["Content-Type"] = "image/png";
|
headers["Content-Type"] = "image/png";
|
||||||
break;
|
break;
|
||||||
case TileType.Jpeg:
|
case TileType.Jpeg:
|
||||||
headers["Content-Type"] = "image/jpeg";
|
headers["Content-Type"] = "image/jpeg";
|
||||||
break;
|
break;
|
||||||
case TileType.Webp:
|
case TileType.Webp:
|
||||||
headers["Content-Type"] = "image/webp";
|
headers["Content-Type"] = "image/webp";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = tile_result.data;
|
let data = tile_result.data;
|
||||||
|
|
||||||
if (tilePostprocess) {
|
if (tilePostprocess) {
|
||||||
data = tilePostprocess(data, header.tileType);
|
data = tilePostprocess(data, header.tileType);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_api_gateway) {
|
if (is_api_gateway) {
|
||||||
// this is wasted work, but we need to force API Gateway to interpret the Lambda response as binary
|
// this is wasted work, but we need to force API Gateway to interpret the Lambda response as binary
|
||||||
// without depending on clients sending matching Accept: headers in the request.
|
// without depending on clients sending matching Accept: headers in the request.
|
||||||
const recompressed_data = zlib.gzipSync(data);
|
const recompressed_data = zlib.gzipSync(data);
|
||||||
headers["Content-Encoding"] = "gzip";
|
headers["Content-Encoding"] = "gzip";
|
||||||
return apiResp(
|
return apiResp(
|
||||||
200,
|
200,
|
||||||
Buffer.from(recompressed_data).toString("base64"),
|
Buffer.from(recompressed_data).toString("base64"),
|
||||||
true,
|
true,
|
||||||
headers
|
headers
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// returns uncompressed response
|
// returns uncompressed response
|
||||||
return apiResp(
|
return apiResp(
|
||||||
200,
|
200,
|
||||||
Buffer.from(data).toString("base64"),
|
Buffer.from(data).toString("base64"),
|
||||||
true,
|
true,
|
||||||
headers
|
headers
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return apiResp(204, "", false, headers);
|
return apiResp(204, "", false, headers);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if ((e as Error).name === "AccessDenied") {
|
if ((e as Error).name === "AccessDenied") {
|
||||||
return apiResp(403, "Bucket access unauthorized", false, headers);
|
return apiResp(403, "Bucket access unauthorized", false, headers);
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
return apiResp(404, "Invalid URL", false, headers);
|
return apiResp(404, "Invalid URL", false, headers);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handler = async (
|
export const handler = async (
|
||||||
event: APIGatewayProxyEventV2,
|
event: APIGatewayProxyEventV2,
|
||||||
context: Context
|
context: Context
|
||||||
): Promise<APIGatewayProxyResult> => {
|
): Promise<APIGatewayProxyResult> => {
|
||||||
return handlerRaw(event, context);
|
return handlerRaw(event, context);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,240 +5,240 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PMTiles,
|
PMTiles,
|
||||||
Source,
|
Source,
|
||||||
RangeResponse,
|
RangeResponse,
|
||||||
ResolvedValueCache,
|
ResolvedValueCache,
|
||||||
TileType,
|
TileType,
|
||||||
Compression,
|
Compression,
|
||||||
} from "../../../js/index";
|
} from "../../../js/index";
|
||||||
|
|
||||||
interface Env {
|
interface Env {
|
||||||
BUCKET: R2Bucket;
|
BUCKET: R2Bucket;
|
||||||
ALLOWED_ORIGINS?: string;
|
ALLOWED_ORIGINS?: string;
|
||||||
PMTILES_PATH?: string;
|
PMTILES_PATH?: string;
|
||||||
TILE_PATH?: string;
|
TILE_PATH?: string;
|
||||||
CACHE_MAX_AGE?: number;
|
CACHE_MAX_AGE?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeyNotFoundError extends Error {
|
class KeyNotFoundError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pmtiles_path = (name: string, setting?: string): string => {
|
export const pmtiles_path = (name: string, setting?: string): string => {
|
||||||
if (setting) {
|
if (setting) {
|
||||||
return setting.replaceAll("{name}", name);
|
return setting.replaceAll("{name}", name);
|
||||||
}
|
}
|
||||||
return name + ".pmtiles";
|
return name + ".pmtiles";
|
||||||
};
|
};
|
||||||
|
|
||||||
const TILE =
|
const TILE =
|
||||||
/^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/;
|
/^\/(?<NAME>[0-9a-zA-Z\/!\-_\.\*\'\(\)]+)\/(?<Z>\d+)\/(?<X>\d+)\/(?<Y>\d+).(?<EXT>[a-z]+)$/;
|
||||||
|
|
||||||
export const tile_path = (
|
export const tile_path = (
|
||||||
path: string,
|
path: string,
|
||||||
setting?: string
|
setting?: string
|
||||||
): {
|
): {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
tile: [number, number, number];
|
tile: [number, number, number];
|
||||||
ext: string;
|
ext: string;
|
||||||
} => {
|
} => {
|
||||||
let pattern = TILE;
|
let pattern = TILE;
|
||||||
if (setting) {
|
if (setting) {
|
||||||
// escape regex
|
// escape regex
|
||||||
setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&");
|
setting = setting.replace(/[.*+?^$()|[\]\\]/g, "\\$&");
|
||||||
setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)");
|
setting = setting.replace("{name}", "(?<NAME>[0-9a-zA-Z/!-_.*'()]+)");
|
||||||
setting = setting.replace("{z}", "(?<Z>\\d+)");
|
setting = setting.replace("{z}", "(?<Z>\\d+)");
|
||||||
setting = setting.replace("{x}", "(?<X>\\d+)");
|
setting = setting.replace("{x}", "(?<X>\\d+)");
|
||||||
setting = setting.replace("{y}", "(?<Y>\\d+)");
|
setting = setting.replace("{y}", "(?<Y>\\d+)");
|
||||||
setting = setting.replace("{ext}", "(?<EXT>[a-z]+)");
|
setting = setting.replace("{ext}", "(?<EXT>[a-z]+)");
|
||||||
pattern = new RegExp(setting);
|
pattern = new RegExp(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
let match = path.match(pattern);
|
let match = path.match(pattern);
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const g = match.groups!;
|
const g = match.groups!;
|
||||||
return { ok: true, name: g.NAME, tile: [+g.Z, +g.X, +g.Y], ext: g.EXT };
|
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: "" };
|
return { ok: false, name: "", tile: [0, 0, 0], ext: "" };
|
||||||
};
|
};
|
||||||
|
|
||||||
async function nativeDecompress(
|
async function nativeDecompress(
|
||||||
buf: ArrayBuffer,
|
buf: ArrayBuffer,
|
||||||
compression: Compression
|
compression: Compression
|
||||||
): Promise<ArrayBuffer> {
|
): Promise<ArrayBuffer> {
|
||||||
if (compression === Compression.None || compression === Compression.Unknown) {
|
if (compression === Compression.None || compression === Compression.Unknown) {
|
||||||
return buf;
|
return buf;
|
||||||
} else if (compression === Compression.Gzip) {
|
} else if (compression === Compression.Gzip) {
|
||||||
let stream = new Response(buf).body!;
|
let stream = new Response(buf).body!;
|
||||||
let result = stream.pipeThrough(new DecompressionStream("gzip"));
|
let result = stream.pipeThrough(new DecompressionStream("gzip"));
|
||||||
return new Response(result).arrayBuffer();
|
return new Response(result).arrayBuffer();
|
||||||
} else {
|
} else {
|
||||||
throw Error("Compression method not supported");
|
throw Error("Compression method not supported");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CACHE = new ResolvedValueCache(25, undefined, nativeDecompress);
|
const CACHE = new ResolvedValueCache(25, undefined, nativeDecompress);
|
||||||
|
|
||||||
class R2Source implements Source {
|
class R2Source implements Source {
|
||||||
env: Env;
|
env: Env;
|
||||||
archive_name: string;
|
archive_name: string;
|
||||||
|
|
||||||
constructor(env: Env, archive_name: string) {
|
constructor(env: Env, archive_name: string) {
|
||||||
this.env = env;
|
this.env = env;
|
||||||
this.archive_name = archive_name;
|
this.archive_name = archive_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
getKey() {
|
getKey() {
|
||||||
return this.archive_name;
|
return this.archive_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBytes(offset: number, length: number): Promise<RangeResponse> {
|
async getBytes(offset: number, length: number): Promise<RangeResponse> {
|
||||||
const resp = await this.env.BUCKET.get(
|
const resp = await this.env.BUCKET.get(
|
||||||
pmtiles_path(this.archive_name, this.env.PMTILES_PATH),
|
pmtiles_path(this.archive_name, this.env.PMTILES_PATH),
|
||||||
{
|
{
|
||||||
range: { offset: offset, length: length },
|
range: { offset: offset, length: length },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!resp) {
|
if (!resp) {
|
||||||
throw new KeyNotFoundError("Archive not found");
|
throw new KeyNotFoundError("Archive not found");
|
||||||
}
|
}
|
||||||
const o = resp as R2ObjectBody;
|
const o = resp as R2ObjectBody;
|
||||||
const a = await o.arrayBuffer();
|
const a = await o.arrayBuffer();
|
||||||
return {
|
return {
|
||||||
data: a,
|
data: a,
|
||||||
etag: o.etag,
|
etag: o.etag,
|
||||||
cacheControl: o.httpMetadata?.cacheControl,
|
cacheControl: o.httpMetadata?.cacheControl,
|
||||||
expires: o.httpMetadata?.cacheExpiry?.toISOString(),
|
expires: o.httpMetadata?.cacheExpiry?.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(
|
async fetch(
|
||||||
request: Request,
|
request: Request,
|
||||||
env: Env,
|
env: Env,
|
||||||
ctx: ExecutionContext
|
ctx: ExecutionContext
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
if (request.method.toUpperCase() === "POST")
|
if (request.method.toUpperCase() === "POST")
|
||||||
return new Response(undefined, { status: 405 });
|
return new Response(undefined, { status: 405 });
|
||||||
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const { ok, name, tile, ext } = tile_path(url.pathname, env.TILE_PATH);
|
const { ok, name, tile, ext } = tile_path(url.pathname, env.TILE_PATH);
|
||||||
|
|
||||||
const cache = caches.default;
|
const cache = caches.default;
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
let allowed_origin = "";
|
let allowed_origin = "";
|
||||||
if (typeof env.ALLOWED_ORIGINS !== "undefined") {
|
if (typeof env.ALLOWED_ORIGINS !== "undefined") {
|
||||||
for (let o of env.ALLOWED_ORIGINS.split(",")) {
|
for (let o of env.ALLOWED_ORIGINS.split(",")) {
|
||||||
if (o === request.headers.get("Origin") || o === "*") {
|
if (o === request.headers.get("Origin") || o === "*") {
|
||||||
allowed_origin = o;
|
allowed_origin = o;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cached = await cache.match(request.url);
|
let cached = await cache.match(request.url);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
let resp_headers = new Headers(cached.headers);
|
let resp_headers = new Headers(cached.headers);
|
||||||
if (allowed_origin)
|
if (allowed_origin)
|
||||||
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
|
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
|
||||||
resp_headers.set("Vary", "Origin");
|
resp_headers.set("Vary", "Origin");
|
||||||
|
|
||||||
return new Response(cached.body, {
|
return new Response(cached.body, {
|
||||||
headers: resp_headers,
|
headers: resp_headers,
|
||||||
status: cached.status,
|
status: cached.status,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheableResponse = (
|
const cacheableResponse = (
|
||||||
body: ArrayBuffer | string | undefined,
|
body: ArrayBuffer | string | undefined,
|
||||||
cacheable_headers: Headers,
|
cacheable_headers: Headers,
|
||||||
status: number
|
status: number
|
||||||
) => {
|
) => {
|
||||||
cacheable_headers.set(
|
cacheable_headers.set(
|
||||||
"Cache-Control",
|
"Cache-Control",
|
||||||
"max-age=" + (env.CACHE_MAX_AGE | 86400)
|
"max-age=" + (env.CACHE_MAX_AGE | 86400)
|
||||||
);
|
);
|
||||||
let cacheable = new Response(body, {
|
let cacheable = new Response(body, {
|
||||||
headers: cacheable_headers,
|
headers: cacheable_headers,
|
||||||
status: status,
|
status: status,
|
||||||
});
|
});
|
||||||
|
|
||||||
// normalize HEAD requests
|
// normalize HEAD requests
|
||||||
ctx.waitUntil(cache.put(request.url, cacheable));
|
ctx.waitUntil(cache.put(request.url, cacheable));
|
||||||
|
|
||||||
let resp_headers = new Headers(cacheable_headers);
|
let resp_headers = new Headers(cacheable_headers);
|
||||||
if (allowed_origin)
|
if (allowed_origin)
|
||||||
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
|
resp_headers.set("Access-Control-Allow-Origin", allowed_origin);
|
||||||
resp_headers.set("Vary", "Origin");
|
resp_headers.set("Vary", "Origin");
|
||||||
return new Response(body, { headers: resp_headers, status: status });
|
return new Response(body, { headers: resp_headers, status: status });
|
||||||
};
|
};
|
||||||
|
|
||||||
const cacheable_headers = new Headers();
|
const cacheable_headers = new Headers();
|
||||||
const source = new R2Source(env, name);
|
const source = new R2Source(env, name);
|
||||||
const p = new PMTiles(source, CACHE, nativeDecompress);
|
const p = new PMTiles(source, CACHE, nativeDecompress);
|
||||||
try {
|
try {
|
||||||
const p_header = await p.getHeader();
|
const p_header = await p.getHeader();
|
||||||
if (tile[0] < p_header.minZoom || tile[0] > p_header.maxZoom) {
|
if (tile[0] < p_header.minZoom || tile[0] > p_header.maxZoom) {
|
||||||
return cacheableResponse(undefined, cacheable_headers, 404);
|
return cacheableResponse(undefined, cacheable_headers, 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const pair of [
|
for (const pair of [
|
||||||
[TileType.Mvt, "mvt"],
|
[TileType.Mvt, "mvt"],
|
||||||
[TileType.Png, "png"],
|
[TileType.Png, "png"],
|
||||||
[TileType.Jpeg, "jpg"],
|
[TileType.Jpeg, "jpg"],
|
||||||
[TileType.Webp, "webp"],
|
[TileType.Webp, "webp"],
|
||||||
]) {
|
]) {
|
||||||
if (p_header.tileType === pair[0] && ext !== pair[1]) {
|
if (p_header.tileType === pair[0] && ext !== pair[1]) {
|
||||||
if (p_header.tileType == TileType.Mvt && ext === "pbf") {
|
if (p_header.tileType == TileType.Mvt && ext === "pbf") {
|
||||||
// allow this for now. Eventually we will delete this in favor of .mvt
|
// allow this for now. Eventually we will delete this in favor of .mvt
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return cacheableResponse(
|
return cacheableResponse(
|
||||||
"Bad request: archive has type ." + pair[1],
|
"Bad request: archive has type ." + pair[1],
|
||||||
cacheable_headers,
|
cacheable_headers,
|
||||||
400
|
400
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tiledata = await p.getZxy(tile[0], tile[1], tile[2]);
|
const tiledata = await p.getZxy(tile[0], tile[1], tile[2]);
|
||||||
|
|
||||||
switch (p_header.tileType) {
|
switch (p_header.tileType) {
|
||||||
case TileType.Mvt:
|
case TileType.Mvt:
|
||||||
cacheable_headers.set("Content-Type", "application/x-protobuf");
|
cacheable_headers.set("Content-Type", "application/x-protobuf");
|
||||||
break;
|
break;
|
||||||
case TileType.Png:
|
case TileType.Png:
|
||||||
cacheable_headers.set("Content-Type", "image/png");
|
cacheable_headers.set("Content-Type", "image/png");
|
||||||
break;
|
break;
|
||||||
case TileType.Jpeg:
|
case TileType.Jpeg:
|
||||||
cacheable_headers.set("Content-Type", "image/jpeg");
|
cacheable_headers.set("Content-Type", "image/jpeg");
|
||||||
break;
|
break;
|
||||||
case TileType.Webp:
|
case TileType.Webp:
|
||||||
cacheable_headers.set("Content-Type", "image/webp");
|
cacheable_headers.set("Content-Type", "image/webp");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tiledata) {
|
if (tiledata) {
|
||||||
return cacheableResponse(tiledata.data, cacheable_headers, 200);
|
return cacheableResponse(tiledata.data, cacheable_headers, 200);
|
||||||
} else {
|
} else {
|
||||||
return cacheableResponse(undefined, cacheable_headers, 204);
|
return cacheableResponse(undefined, cacheable_headers, 204);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof KeyNotFoundError) {
|
if (e instanceof KeyNotFoundError) {
|
||||||
return cacheableResponse("Archive not found", cacheable_headers, 404);
|
return cacheableResponse("Archive not found", cacheable_headers, 404);
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: metadata responses, tileJSON
|
// TODO: metadata responses, tileJSON
|
||||||
return new Response("Invalid URL", { status: 404 });
|
return new Response("Invalid URL", { status: 404 });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user