118 lines
4.5 KiB
JavaScript
118 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
import { randomUUID } from 'node:crypto';
|
|
import { constants } from 'node:fs';
|
|
import { mkdir, open, realpath, rename, rm } from 'node:fs/promises';
|
|
import { dirname, resolve, sep } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import sharp from 'sharp';
|
|
|
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
const permittedPrivateRoot = resolve(repositoryRoot, 'content/private');
|
|
|
|
const fail = () => {
|
|
throw new Error('SOCIAL-PREVIEW-PATH-001');
|
|
};
|
|
|
|
const canonicalizeForWrite = async (path) => {
|
|
const absolute = resolve(path);
|
|
let cursor = dirname(absolute);
|
|
const suffix = [];
|
|
while (true) {
|
|
try {
|
|
return resolve(await realpath(cursor), ...suffix, absolute.slice(dirname(absolute).length + 1));
|
|
} catch (error) {
|
|
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) fail();
|
|
const parent = dirname(cursor);
|
|
if (parent === cursor) fail();
|
|
suffix.unshift(cursor.slice(parent.length + 1));
|
|
cursor = parent;
|
|
}
|
|
}
|
|
};
|
|
|
|
const isInside = (path, root) => path === root || path.startsWith(`${root}${sep}`);
|
|
|
|
const writeRegularFileWithoutFollowingLinks = async (output, bytes) => {
|
|
if (typeof constants.O_NOFOLLOW !== 'number') fail();
|
|
const destinationFlags = constants.O_WRONLY | constants.O_CREAT | constants.O_NOFOLLOW
|
|
| (constants.O_NONBLOCK ?? 0);
|
|
let destination;
|
|
try {
|
|
destination = await open(output, destinationFlags, 0o600);
|
|
const stats = await destination.stat();
|
|
if (!stats.isFile() || stats.nlink !== 1) fail();
|
|
} catch {
|
|
fail();
|
|
} finally {
|
|
await destination?.close();
|
|
}
|
|
|
|
const temporaryPath = `${output}.${randomUUID()}.tmp`;
|
|
let temporary;
|
|
let installed = false;
|
|
try {
|
|
temporary = await open(
|
|
temporaryPath,
|
|
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
0o600,
|
|
);
|
|
const stats = await temporary.stat();
|
|
if (!stats.isFile() || stats.nlink !== 1) fail();
|
|
await temporary.writeFile(bytes);
|
|
await temporary.sync();
|
|
await temporary.close();
|
|
temporary = undefined;
|
|
await rename(temporaryPath, output);
|
|
installed = true;
|
|
} catch {
|
|
fail();
|
|
} finally {
|
|
await temporary?.close();
|
|
if (!installed) await rm(temporaryPath, { force: true });
|
|
}
|
|
};
|
|
|
|
export const generateSocialPreview = async (outputPath) => {
|
|
if (typeof outputPath !== 'string' || outputPath.trim() === '') fail();
|
|
const output = await canonicalizeForWrite(outputPath);
|
|
const privateRoot = await canonicalizeForWrite(permittedPrivateRoot);
|
|
if (!isInside(output, privateRoot) && !isInside(output, '/tmp')) fail();
|
|
|
|
const svg = Buffer.from(`
|
|
<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
|
|
<rect width="1200" height="630" fill="#070910"/>
|
|
<rect y="356" width="1200" height="274" fill="#0b1019"/>
|
|
<path d="M0 356H1200" stroke="#1b3246" stroke-width="2"/>
|
|
<circle cx="880" cy="174" r="78" fill="#152638"/>
|
|
<path d="M474 630L586 356H642L780 630Z" fill="#101b28"/>
|
|
<path d="M544 630L606 356" stroke="#35c8d0" stroke-width="7" opacity="0.9"/>
|
|
<path d="M701 630L623 356" stroke="#e3a44a" stroke-width="7" opacity="0.88"/>
|
|
<path d="M602 566L613 522M614 490L619 458M620 430L623 405" stroke="#dfe9ef" stroke-width="5" stroke-linecap="round" opacity="0.72"/>
|
|
<text x="92" y="183" font-family="DejaVu Sans, sans-serif" font-size="66" font-weight="700" fill="#f1f5f7">Luca Pîslaru</text>
|
|
<text x="96" y="245" font-family="DejaVu Sans, sans-serif" font-size="29" font-weight="500" letter-spacing="0.6" fill="#9fc8d1">System Administrator & Full-Stack Developer</text>
|
|
<path d="M96 286H342" stroke="#35c8d0" stroke-width="4"/>
|
|
<path d="M350 286H430" stroke="#e3a44a" stroke-width="4"/>
|
|
</svg>
|
|
`);
|
|
|
|
const bytes = await sharp(svg, { density: 72 })
|
|
.flatten({ background: '#070910' })
|
|
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
.toBuffer();
|
|
await mkdir(dirname(output), { recursive: true });
|
|
await writeRegularFileWithoutFollowingLinks(output, bytes);
|
|
return output;
|
|
};
|
|
|
|
const parseOutput = (argv) => {
|
|
if (argv.length !== 2 || argv[0] !== '--output') fail();
|
|
return argv[1];
|
|
};
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
generateSocialPreview(parseOutput(process.argv.slice(2))).catch((error) => {
|
|
process.stderr.write(`${error instanceof Error ? error.message : 'SOCIAL-PREVIEW-001'}\n`);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|