Add readFile function
This commit is contained in:
parent
3fedd5bb77
commit
1f693003ed
2 changed files with 164 additions and 152 deletions
|
@ -7,7 +7,7 @@ import clone from 'clone';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
import { validateStyleMin } from '@maplibre/maplibre-gl-style-spec';
|
||||||
|
|
||||||
import { fixUrl, allowedOptions } from './utils.js';
|
import { fixUrl, allowedOptions, readFile } from './utils.js';
|
||||||
|
|
||||||
const httpTester = /^https?:\/\//i;
|
const httpTester = /^https?:\/\//i;
|
||||||
const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
const allowedSpriteFormats = allowedOptions(['png', 'json']);
|
||||||
|
@ -101,94 +101,98 @@ export const serve_style = {
|
||||||
* @param {string} req.params.format - Format of the sprite file, 'png' or 'json'.
|
* @param {string} req.params.format - Format of the sprite file, 'png' or 'json'.
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
app.get(`/:id/sprite{/:spriteID}{@:scale}{.:format}`, (req, res, next) => {
|
app.get(
|
||||||
const { spriteID = 'default', id, format, scale } = req.params;
|
`/:id/sprite{/:spriteID}{@:scale}{.:format}`,
|
||||||
const sanitizedId = String(id).replace(/\n|\r/g, '');
|
async (req, res, next) => {
|
||||||
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : '';
|
const { spriteID = 'default', id, format, scale } = req.params;
|
||||||
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
|
const sanitizedId = String(id).replace(/\n|\r/g, '');
|
||||||
const sanitizedFormat = format
|
const sanitizedScale = scale ? String(scale).replace(/\n|\r/g, '') : '';
|
||||||
? '.' + String(format).replace(/\n|\r/g, '')
|
const sanitizedSpriteID = String(spriteID).replace(/\n|\r/g, '');
|
||||||
: '';
|
const sanitizedFormat = format
|
||||||
if (verbose) {
|
? '.' + String(format).replace(/\n|\r/g, '')
|
||||||
console.log(
|
: '';
|
||||||
`Handling sprite request for: /styles/%s/sprite/%s%s%s`,
|
if (verbose) {
|
||||||
sanitizedId,
|
console.log(
|
||||||
sanitizedSpriteID,
|
`Handling sprite request for: /styles/%s/sprite/%s%s%s`,
|
||||||
sanitizedScale,
|
|
||||||
sanitizedFormat,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const item = repo[id];
|
|
||||||
const validatedFormat = allowedSpriteFormats(format);
|
|
||||||
if (!item || !validatedFormat) {
|
|
||||||
if (verbose)
|
|
||||||
console.error(
|
|
||||||
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
|
|
||||||
sanitizedId,
|
sanitizedId,
|
||||||
sanitizedSpriteID,
|
sanitizedSpriteID,
|
||||||
sanitizedScale,
|
sanitizedScale,
|
||||||
sanitizedFormat,
|
sanitizedFormat,
|
||||||
);
|
);
|
||||||
return res.sendStatus(404);
|
|
||||||
}
|
|
||||||
const sprite = item.spritePaths.find((sprite) => sprite.id === spriteID);
|
|
||||||
const spriteScale = allowedSpriteScales(scale);
|
|
||||||
if (!sprite || spriteScale === null) {
|
|
||||||
if (verbose)
|
|
||||||
console.error(
|
|
||||||
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
|
|
||||||
sanitizedId,
|
|
||||||
sanitizedSpriteID,
|
|
||||||
sanitizedScale,
|
|
||||||
sanitizedFormat,
|
|
||||||
);
|
|
||||||
return res.status(400).send('Bad Sprite ID or Scale');
|
|
||||||
}
|
|
||||||
|
|
||||||
const modifiedSince = req.get('if-modified-since');
|
|
||||||
const cc = req.get('cache-control');
|
|
||||||
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
|
||||||
if (
|
|
||||||
new Date(item.lastModified).getTime() ===
|
|
||||||
new Date(modifiedSince).getTime()
|
|
||||||
) {
|
|
||||||
return res.sendStatus(304);
|
|
||||||
}
|
}
|
||||||
}
|
const item = repo[id];
|
||||||
|
const validatedFormat = allowedSpriteFormats(format);
|
||||||
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
|
if (!item || !validatedFormat) {
|
||||||
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
|
|
||||||
if (verbose) console.log(`Loading sprite from: %s`, filename);
|
|
||||||
|
|
||||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
||||||
fs.readFile(filename, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
if (verbose)
|
if (verbose)
|
||||||
|
console.error(
|
||||||
|
`Sprite item or format not found for: /styles/%s/sprite/%s%s%s`,
|
||||||
|
sanitizedId,
|
||||||
|
sanitizedSpriteID,
|
||||||
|
sanitizedScale,
|
||||||
|
sanitizedFormat,
|
||||||
|
);
|
||||||
|
return res.sendStatus(404);
|
||||||
|
}
|
||||||
|
const sprite = item.spritePaths.find(
|
||||||
|
(sprite) => sprite.id === spriteID,
|
||||||
|
);
|
||||||
|
const spriteScale = allowedSpriteScales(scale);
|
||||||
|
if (!sprite || spriteScale === null) {
|
||||||
|
if (verbose)
|
||||||
|
console.error(
|
||||||
|
`Bad Sprite ID or Scale for: /styles/%s/sprite/%s%s%s`,
|
||||||
|
sanitizedId,
|
||||||
|
sanitizedSpriteID,
|
||||||
|
sanitizedScale,
|
||||||
|
sanitizedFormat,
|
||||||
|
);
|
||||||
|
return res.status(400).send('Bad Sprite ID or Scale');
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedSince = req.get('if-modified-since');
|
||||||
|
const cc = req.get('cache-control');
|
||||||
|
if (modifiedSince && (!cc || cc.indexOf('no-cache') === -1)) {
|
||||||
|
if (
|
||||||
|
new Date(item.lastModified).getTime() ===
|
||||||
|
new Date(modifiedSince).getTime()
|
||||||
|
) {
|
||||||
|
return res.sendStatus(304);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedSpritePath = sprite.path.replace(/^(\.\.\/)+/, '');
|
||||||
|
const filename = `${sanitizedSpritePath}${spriteScale}.${validatedFormat}`;
|
||||||
|
if (verbose) console.log(`Loading sprite from: %s`, filename);
|
||||||
|
try {
|
||||||
|
const data = await readFile(filename);
|
||||||
|
|
||||||
|
if (validatedFormat === 'json') {
|
||||||
|
res.header('Content-type', 'application/json');
|
||||||
|
} else if (validatedFormat === 'png') {
|
||||||
|
res.header('Content-type', 'image/png');
|
||||||
|
}
|
||||||
|
if (verbose)
|
||||||
|
console.log(
|
||||||
|
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
|
||||||
|
sanitizedId,
|
||||||
|
sanitizedSpriteID,
|
||||||
|
sanitizedScale,
|
||||||
|
sanitizedFormat,
|
||||||
|
);
|
||||||
|
res.set({ 'Last-Modified': item.lastModified });
|
||||||
|
return res.send(data);
|
||||||
|
} catch (err) {
|
||||||
|
if (verbose) {
|
||||||
console.error(
|
console.error(
|
||||||
'Sprite load error: %s, Error: %s',
|
'Sprite load error: %s, Error: %s',
|
||||||
filename,
|
filename,
|
||||||
String(err),
|
String(err),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return res.sendStatus(404);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
if (validatedFormat === 'json') {
|
);
|
||||||
res.header('Content-type', 'application/json');
|
|
||||||
} else if (validatedFormat === 'png') {
|
|
||||||
res.header('Content-type', 'image/png');
|
|
||||||
}
|
|
||||||
if (verbose)
|
|
||||||
console.log(
|
|
||||||
`Responding with sprite data for /styles/%s/sprite/%s%s%s`,
|
|
||||||
sanitizedId,
|
|
||||||
sanitizedSpriteID,
|
|
||||||
sanitizedScale,
|
|
||||||
sanitizedFormat,
|
|
||||||
);
|
|
||||||
res.set({ 'Last-Modified': item.lastModified });
|
|
||||||
return res.send(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
},
|
},
|
||||||
|
|
162
src/utils.js
162
src/utils.js
|
@ -185,6 +185,24 @@ export function fixTileJSONCenter(tileJSON) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a file and returns a Promise with the file data.
|
||||||
|
* @param {string} filename - Path to the file to read.
|
||||||
|
* @returns {Promise<Buffer>} - A Promise that resolves with the file data as a Buffer or rejects with an error.
|
||||||
|
*/
|
||||||
|
export function readFile(filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
||||||
|
fs.readFile(filename, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves font data for a given font and range.
|
* Retrieves font data for a given font and range.
|
||||||
* @param {object} allowedFonts - An object of allowed fonts.
|
* @param {object} allowedFonts - An object of allowed fonts.
|
||||||
|
@ -194,85 +212,75 @@ export function fixTileJSONCenter(tileJSON) {
|
||||||
* @param {object} [fallbacks] - Optional fallback font list.
|
* @param {object} [fallbacks] - Optional fallback font list.
|
||||||
* @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
|
* @returns {Promise<Buffer>} A promise that resolves with the font data Buffer or rejects with an error.
|
||||||
*/
|
*/
|
||||||
function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
async function getFontPbf(allowedFonts, fontPath, name, range, fallbacks) {
|
||||||
return new Promise((resolve, reject) => {
|
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
||||||
if (!allowedFonts || (allowedFonts[name] && fallbacks)) {
|
const fontMatch = name?.match(/^[\w\s-]+$/);
|
||||||
const fontMatch = name?.match(/^[\w\s-]+$/);
|
const sanitizedName = fontMatch?.[0] || 'invalid';
|
||||||
const sanitizedName = fontMatch?.[0] || 'invalid';
|
if (!name || typeof name !== 'string' || name.trim() === '' || !fontMatch) {
|
||||||
if (
|
console.error(
|
||||||
!name ||
|
'ERROR: Invalid font name: %s',
|
||||||
typeof name !== 'string' ||
|
sanitizedName.replace(/\n|\r/g, ''),
|
||||||
name.trim() === '' ||
|
|
||||||
!fontMatch
|
|
||||||
) {
|
|
||||||
console.error(
|
|
||||||
'ERROR: Invalid font name: %s',
|
|
||||||
sanitizedName.replace(/\n|\r/g, ''),
|
|
||||||
);
|
|
||||||
return reject('Invalid font name');
|
|
||||||
}
|
|
||||||
|
|
||||||
const rangeMatch = range?.match(/^[\d-]+$/);
|
|
||||||
const sanitizedRange = rangeMatch?.[0] || 'invalid';
|
|
||||||
if (!/^\d+-\d+$/.test(range)) {
|
|
||||||
console.error(
|
|
||||||
'ERROR: Invalid range: %s',
|
|
||||||
sanitizedRange.replace(/\n|\r/g, ''),
|
|
||||||
);
|
|
||||||
return reject('Invalid range');
|
|
||||||
}
|
|
||||||
const filename = path.join(
|
|
||||||
fontPath,
|
|
||||||
sanitizedName,
|
|
||||||
`${sanitizedRange}.pbf`,
|
|
||||||
);
|
);
|
||||||
if (!fallbacks) {
|
throw new Error('Invalid font name');
|
||||||
fallbacks = clone(allowedFonts || {});
|
|
||||||
}
|
|
||||||
delete fallbacks[name];
|
|
||||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
||||||
fs.readFile(filename, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
console.error(
|
|
||||||
'ERROR: Font not found: %s, Error: %s',
|
|
||||||
filename.replace(/\n|\r/g, ''),
|
|
||||||
String(err),
|
|
||||||
);
|
|
||||||
if (fallbacks && Object.keys(fallbacks).length) {
|
|
||||||
let fallbackName;
|
|
||||||
|
|
||||||
let fontStyle = name.split(' ').pop();
|
|
||||||
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
|
|
||||||
fontStyle = 'Regular';
|
|
||||||
}
|
|
||||||
fallbackName = `Noto Sans ${fontStyle}`;
|
|
||||||
if (!fallbacks[fallbackName]) {
|
|
||||||
fallbackName = `Open Sans ${fontStyle}`;
|
|
||||||
if (!fallbacks[fallbackName]) {
|
|
||||||
fallbackName = Object.keys(fallbacks)[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.error(
|
|
||||||
`ERROR: Trying to use %s as a fallback for: %s`,
|
|
||||||
fallbackName,
|
|
||||||
sanitizedName,
|
|
||||||
);
|
|
||||||
delete fallbacks[fallbackName];
|
|
||||||
getFontPbf(null, fontPath, fallbackName, range, fallbacks).then(
|
|
||||||
resolve,
|
|
||||||
reject,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
reject('Font load error');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject('Font not allowed');
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const rangeMatch = range?.match(/^[\d-]+$/);
|
||||||
|
const sanitizedRange = rangeMatch?.[0] || 'invalid';
|
||||||
|
if (!/^\d+-\d+$/.test(range)) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: Invalid range: %s',
|
||||||
|
sanitizedRange.replace(/\n|\r/g, ''),
|
||||||
|
);
|
||||||
|
throw new Error('Invalid range');
|
||||||
|
}
|
||||||
|
const filename = path.join(
|
||||||
|
fontPath,
|
||||||
|
sanitizedName,
|
||||||
|
`${sanitizedRange}.pbf`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!fallbacks) {
|
||||||
|
fallbacks = clone(allowedFonts || {});
|
||||||
|
}
|
||||||
|
delete fallbacks[name];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await readFile(filename);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
'ERROR: Font not found: %s, Error: %s',
|
||||||
|
filename.replace(/\n|\r/g, ''),
|
||||||
|
String(err),
|
||||||
|
);
|
||||||
|
if (fallbacks && Object.keys(fallbacks).length) {
|
||||||
|
let fallbackName;
|
||||||
|
|
||||||
|
let fontStyle = name.split(' ').pop();
|
||||||
|
if (['Regular', 'Bold', 'Italic'].indexOf(fontStyle) < 0) {
|
||||||
|
fontStyle = 'Regular';
|
||||||
|
}
|
||||||
|
fallbackName = `Noto Sans ${fontStyle}`;
|
||||||
|
if (!fallbacks[fallbackName]) {
|
||||||
|
fallbackName = `Open Sans ${fontStyle}`;
|
||||||
|
if (!fallbacks[fallbackName]) {
|
||||||
|
fallbackName = Object.keys(fallbacks)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.error(
|
||||||
|
`ERROR: Trying to use %s as a fallback for: %s`,
|
||||||
|
fallbackName,
|
||||||
|
sanitizedName,
|
||||||
|
);
|
||||||
|
delete fallbacks[fallbackName];
|
||||||
|
return getFontPbf(null, fontPath, fallbackName, range, fallbacks);
|
||||||
|
} else {
|
||||||
|
throw new Error('Font load error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Font not allowed');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Combines multiple font pbf buffers into one.
|
* Combines multiple font pbf buffers into one.
|
||||||
|
|
Loading…
Reference in a new issue