Add preview-only Decap CMS pilot
This commit is contained in:
+44
-5
@@ -1,7 +1,7 @@
|
||||
import Ajv2020 from 'ajv/dist/2020.js';
|
||||
import addFormats from 'ajv-formats';
|
||||
import { readdir, readFile, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { extname, join, relative, sep } from 'node:path';
|
||||
|
||||
const readJson = async (path) => JSON.parse(await readFile(path, 'utf8'));
|
||||
const ajv = new Ajv2020({ allErrors: true, strict: true });
|
||||
@@ -23,8 +23,47 @@ for (const key of Object.keys(sites.sites)) {
|
||||
if (!siteCheck(data)) throw new Error(`${key} site data rejected: ${ajv.errorsText(siteCheck.errors)}`);
|
||||
}
|
||||
|
||||
for (const file of await readdir('public/images')) {
|
||||
const info = await stat(join('public/images', file));
|
||||
if (info.size > 500 * 1024) throw new Error(`Image exceeds the 500 KiB pilot limit: ${file}`);
|
||||
async function filesUnder(directory) {
|
||||
const entries = await readdir(directory, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) files.push(...await filesUnder(path));
|
||||
else if (entry.isFile()) files.push(path);
|
||||
else throw new Error(`Unsupported filesystem entry in public images: ${path}`);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
console.log('Intake fixtures, deployment manifest, site data, and image limits are valid.');
|
||||
|
||||
const uploadRoot = join('public', 'images', 'uploads');
|
||||
const allowedUploadExtensions = new Set(['.jpg', '.jpeg', '.png', '.webp', '.avif']);
|
||||
const ascii = (value) => Buffer.from(value, 'ascii');
|
||||
const startsWith = (buffer, signature) => buffer.subarray(0, signature.length).equals(signature);
|
||||
const isRasterForExtension = (buffer, extension) => {
|
||||
if (extension === '.jpg' || extension === '.jpeg') return startsWith(buffer, Buffer.from([0xff, 0xd8, 0xff]));
|
||||
if (extension === '.png') return startsWith(buffer, Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
if (extension === '.webp') return buffer.length >= 12 && buffer.subarray(0, 4).equals(ascii('RIFF')) && buffer.subarray(8, 12).equals(ascii('WEBP'));
|
||||
if (extension === '.avif' && buffer.length >= 16 && buffer.subarray(4, 8).equals(ascii('ftyp'))) {
|
||||
for (let offset = 8; offset + 4 <= Math.min(buffer.length, 64); offset += 4) {
|
||||
const brand = buffer.subarray(offset, offset + 4).toString('ascii');
|
||||
if (brand === 'avif' || brand === 'avis') return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
for (const path of await filesUnder('public/images')) {
|
||||
const imagePath = relative('public/images', path).split(sep).join('/');
|
||||
const info = await stat(path);
|
||||
if (info.size > 500 * 1024) throw new Error(`Image exceeds the 500 KiB pilot limit: ${imagePath}`);
|
||||
|
||||
const uploadPath = relative(uploadRoot, path);
|
||||
const isUpload = uploadPath !== '..' && !uploadPath.startsWith(`..${sep}`);
|
||||
if (!isUpload) continue;
|
||||
if (uploadPath.includes(sep)) throw new Error(`Nested CMS media paths are not allowed: ${imagePath}`);
|
||||
const extension = extname(uploadPath).toLowerCase();
|
||||
if (!allowedUploadExtensions.has(extension)) throw new Error(`CMS media is not an allowed raster extension: ${imagePath}`);
|
||||
const buffer = await readFile(path);
|
||||
if (!isRasterForExtension(buffer, extension)) throw new Error(`CMS media does not match its raster extension: ${imagePath}`);
|
||||
}
|
||||
console.log('Intake fixtures, deployment manifest, site data, and bounded raster images are valid.');
|
||||
|
||||
Reference in New Issue
Block a user