Fix: decode base64-corrupted source files (html/css/js + backend)
This commit is contained in:
1 parent
327b37babb
commit
458c66a2c0
6 files changed
+2699
-5
No files matched your search
+111
-1
@@ -1 +1,111 @@
|
||||
aW1wb3J0IHsgc3Bhd24gfSBmcm9tICdub2RlOmNoaWxkX3Byb2Nlc3MnOwppbXBvcnQgeyBleGlzdHNTeW5jLCBzdGF0U3luYyB9IGZyb20gJ25vZGU6ZnMnOwppbXBvcnQgeyBiYXNlbmFtZSwgZXh0bmFtZSwgam9pbiB9IGZyb20gJ25vZGU6cGF0aCc7CgovKgogKiBnaG9zdC1tZWRpYS5qcyDigJQgc2VydmVyLXNpZGUgZ2hvc3Qgc3ByaXRlIGNvbnZlcnNpb24uCiAqCiAqIFR1cm5zIGFuIHVwbG9hZGVkIGJsYWNrLWJhY2tncm91bmQgZ2hvc3QgTVA0IGludG8gYSB0cmFuc3BhcmVudCBhbmltYXRlZAogKiBXZWJNIChWUDkgKyBhbHBoYSkgcGx1cyBhbiBhbmltYXRlZCBXZWJQIGZhbGxiYWNrLCB1c2luZyBsdW1hIGtleWluZyBzbyB0aGUKICogYmxhY2sgYmVjb21lcyB0cmFuc3BhcmVudCBhbmQgdGhlIGdsb3cga2VlcHMgc29mdCBzZW1pLXRyYW5zcGFyZW50IGVkZ2VzLgogKgogKiBTaGVsbHMgb3V0IHRvIHRoZSBzeXN0ZW0gYGZmbXBlZ2AgYmluYXJ5IChubyBucG0gZGVwZW5kZW5jeSkuIElmIGZmbXBlZyBpcwogKiBub3QgaW5zdGFsbGVkLCBjb252ZXJzaW9uIGZhaWxzIGdyYWNlZnVsbHkgYW5kIHRoZSBjYWxsZXIga2VlcHMgdGhlIG9yaWdpbmFsLgogKgogKiBUaGlzIG1pcnJvcnMgdGhlIHN0YW5kYWxvbmUgZ2hvc3RpZnkuc2ggcGlwZWxpbmUgc28gbG9jYWwgYW5kIHNlcnZlciBvdXRwdXQKICogbWF0Y2guCiAqLwoKY29uc3QgRkZNUEVHID0gcHJvY2Vzcy5lbnYuRkZNUEVHX1BBVEggfHwgJ2ZmbXBlZyc7CgovLyBUdW5hYmxlcyAoZW52LW92ZXJyaWRhYmxlKSDigJQga2VwdCBpbiBzeW5jIHdpdGggZ2hvc3RpZnkuc2ggZGVmYXVsdHMuCmNvbnN0IFRIUkVTSCA9IHByb2Nlc3MuZW52LkdIT1NUX0tFWV9USFJFU0hPTEQgfHwgJzAuMDYnOwpjb25zdCBUT0wgPSBwcm9jZXNzLmVudi5HSE9TVF9LRVlfVE9MRVJBTkNFIHx8ICcwLjEwJzsKY29uc3QgU09GVCA9IHByb2Nlc3MuZW52LkdIT1NUX0tFWV9TT0ZUTkVTUyB8fCAnMC4xNSc7CmNvbnN0IEZQUyA9IHByb2Nlc3MuZW52LkdIT1NUX0ZQUyB8fCAnMTUnOwpjb25zdCBNQVhXID0gcHJvY2Vzcy5lbnYuR0hPU1RfTUFYVyB8fCAnNTEyJzsKY29uc3QgV0VCUF9RID0gcHJvY2Vzcy5lbnYuR0hPU1RfV0VCUF9RVUFMSVRZIHx8ICc4MCc7CmNvbnN0IFZQOV9DUkYgPSBwcm9jZXNzLmVudi5HSE9TVF9WUDlfQ1JGIHx8ICczMCc7CgovLyBUaGUgc2hhcmVkIGx1bWEta2V5ICsgZG93bnNjYWxlIGZpbHRlciBjaGFpbi4KZnVuY3Rpb24ga2V5Q2hhaW4oKSB7CiAgY29uc3Qga2V5ID0gYGZvcm1hdD1yZ2JhLGx1bWFrZXk9dGhyZXNob2xkPSR7VEhSRVNIfTp0b2xlcmFuY2U9JHtUT0x9OnNvZnRuZXNzPSR7U09GVH1gOwogIGNvbnN0IHNjYWxlID0gYHNjYWxlPSdtaW4oaXcsJHtNQVhXfSknOi0xOmZsYWdzPWxhbmN6b3NgOwogIHJldHVybiBgJHtrZXl9LGZwcz0ke0ZQU30sJHtzY2FsZX1gOwp9CgpmdW5jdGlvbiBydW4oYXJncykgewogIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7CiAgICBjb25zdCBwcm9jID0gc3Bhd24oRkZNUEVHLCBhcmdzLCB7IHN0ZGlvOiBbJ2lnbm9yZScsICdpZ25vcmUnLCAncGlwZSddIH0pOwogICAgbGV0IHN0ZGVyciA9ICcnOwogICAgcHJvYy5zdGRlcnIub24oJ2RhdGEnLCAoZCkgPT4geyBzdGRlcnIgKz0gZC50b1N0cmluZygpOyB9KTsKICAgIHByb2Mub24oJ2Vycm9yJywgcmVqZWN0KTsgLy8gZS5nLiBmZm1wZWcgbm90IGZvdW5kIChFTk9FTlQpCiAgICBwcm9jLm9uKCdjbG9zZScsIChjb2RlKSA9PiB7CiAgICAgIGlmIChjb2RlID09PSAwKSByZXNvbHZlKCk7CiAgICAgIGVsc2UgcmVqZWN0KG5ldyBFcnJvcihgZmZtcGVnIGV4aXRlZCAke2NvZGV9OiAke3N0ZGVyci5zbGljZSgtNTAwKX1gKSk7CiAgICB9KTsKICB9KTsKfQoKLyoqIFRydWUgaWYgYSB1c2FibGUgZmZtcGVnIGJpbmFyeSBpcyBvbiBQQVRILiAqLwpleHBvcnQgZnVuY3Rpb24gZmZtcGVnQXZhaWxhYmxlKCkgewogIHJldHVybiBuZXcgUHJvbWlzZSgocmVzb2x2ZSkgPT4gewogICAgY29uc3QgcHJvYyA9IHNwYXduKEZGTVBFRywgWyctdmVyc2lvbiddLCB7IHN0ZGlvOiAnaWdub3JlJyB9KTsKICAgIHByb2Mub24oJ2Vycm9yJywgKCkgPT4gcmVzb2x2ZShmYWxzZSkpOwogICAgcHJvYy5vbignY2xvc2UnLCAoY29kZSkgPT4gcmVzb2x2ZShjb2RlID09PSAwKSk7CiAgfSk7Cn0KCmZ1bmN0aW9uIG91dE5hbWUoc3JjRmlsZW5hbWUsIGV4dCkgewogIC8vIHdyYWl0aC0xNzE4Lm1wNCAtPiB3cmFpdGgtMTcxOC53ZWJtIC8gLndlYnAsIHdyaXR0ZW4gYWxvbmdzaWRlIHRoZSBzb3VyY2UuCiAgY29uc3Qgc3RlbSA9IGJhc2VuYW1lKHNyY0ZpbGVuYW1lLCBleHRuYW1lKHNyY0ZpbGVuYW1lKSk7CiAgcmV0dXJuIGAke3N0ZW19LiR7ZXh0fWA7Cn0KCi8qKgogKiBDb252ZXJ0IGEgc291cmNlIE1QNCAoYWxyZWFkeSBzYXZlZCBpbiB1cGxvYWREaXIpIGludG8gdHJhbnNwYXJlbnQgd2VibSt3ZWJwLgogKgogKiBAcGFyYW0ge3N0cmluZ30gdXBsb2FkRGlyICBhYnNvbHV0ZSB1cGxvYWRzIGRpcmVjdG9yeQogKiBAcGFyYW0ge3N0cmluZ30gc3JjRmlsZW5hbWUgdGhlIG11bHRlciBmaWxlbmFtZSBvZiB0aGUgdXBsb2FkZWQgbXA0CiAqIEByZXR1cm5zIHtQcm9taXNlPHsgd2VibTogc3RyaW5nfG51bGwsIHdlYnA6IHN0cmluZ3xudWxsIH0+fSBuZXcgZmlsZW5hbWVzCiAqICAgICAgICAgIChub3QgZnVsbCBwYXRocykuIEVpdGhlciBtYXkgYmUgbnVsbCBpZiB0aGF0IGVuY29kZSBmYWlsZWQuCiAqLwpleHBvcnQgYXN5bmMgZnVuY3Rpb24gY29udmVydEdob3N0TXA0KHVwbG9hZERpciwgc3JjRmlsZW5hbWUpIHsKICBjb25zdCBzcmNQYXRoID0gam9pbih1cGxvYWREaXIsIHNyY0ZpbGVuYW1lKTsKICBpZiAoIWV4aXN0c1N5bmMoc3JjUGF0aCkpIHRocm93IG5ldyBFcnJvcihgc291cmNlIG5vdCBmb3VuZDogJHtzcmNQYXRofWApOwoKICBjb25zdCB2ZiA9IGtleUNoYWluKCk7CiAgY29uc3QgcmVzdWx0ID0geyB3ZWJtOiBudWxsLCB3ZWJwOiBudWxsIH07CgogIC8vIC0tLSBXZWJNIChWUDkgKyBhbHBoYSk6IHRoZSBwcmltYXJ5LCBicm93c2VyLWRlY29kZWQgc3ByaXRlIC0tLQogIGNvbnN0IHdlYm1OYW1lID0gb3V0TmFtZShzcmNGaWxlbmFtZSwgJ3dlYm0nKTsKICB0cnkgewogICAgYXdhaXQgcnVuKFsKICAgICAgJy1oaWRlX2Jhbm5lcicsICctbG9nbGV2ZWwnLCAnZXJyb3InLCAnLXknLCAnLWknLCBzcmNQYXRoLAogICAgICAnLXZmJywgdmYsICctYW4nLAogICAgICAnLWM6dicsICdsaWJ2cHgtdnA5JywgJy1waXhfZm10JywgJ3l1dmE0MjBwJywKICAgICAgJy1iOnYnLCAnMCcsICctY3JmJywgVlA5X0NSRiwgJy1hdXRvLWFsdC1yZWYnLCAnMCcsCiAgICAgIGpvaW4odXBsb2FkRGlyLCB3ZWJtTmFtZSksCiAgICBdKTsKICAgIGlmIChzdGF0U3luYyhqb2luKHVwbG9hZERpciwgd2VibU5hbWUpKS5zaXplID4gMCkgcmVzdWx0LndlYm0gPSB3ZWJtTmFtZTsKICB9IGNhdGNoIChlKSB7CiAgICBjb25zb2xlLmVycm9yKCdbZ2hvc3QtbWVkaWFdIHdlYm0gZW5jb2RlIGZhaWxlZDonLCBlLm1lc3NhZ2UpOwogIH0KCiAgLy8gLS0tIFdlYlAgZmFsbGJhY2s6IGZvciBpT1MgU2FmYXJpIC8gbm8tVlA5LWFscGhhIGJyb3dzZXJzIC0tLQogIGNvbnN0IHdlYnBOYW1lID0gb3V0TmFtZShzcmNGaWxlbmFtZSwgJ3dlYnAnKTsKICB0cnkgewogICAgYXdhaXQgcnVuKFsKICAgICAgJy1oaWRlX2Jhbm5Line truncated
|
||||
import { spawn } from 'node:child_process';
|
||||
import { existsSync, statSync } from 'node:fs';
|
||||
import { basename, extname, join } from 'node:path';
|
||||
|
||||
/*
|
||||
* ghost-media.js — server-side ghost sprite conversion.
|
||||
*
|
||||
* Turns an uploaded black-background ghost MP4 into a transparent animated
|
||||
* WebM (VP9 + alpha) plus an animated WebP fallback, using luma keying so the
|
||||
* black becomes transparent and the glow keeps soft semi-transparent edges.
|
||||
*
|
||||
* Shells out to the system `ffmpeg` binary (no npm dependency). If ffmpeg is
|
||||
* not installed, conversion fails gracefully and the caller keeps the original.
|
||||
*
|
||||
* This mirrors the standalone ghostify.sh pipeline so local and server output
|
||||
* match.
|
||||
*/
|
||||
|
||||
const FFMPEG = process.env.FFMPEG_PATH || 'ffmpeg';
|
||||
|
||||
// Tunables (env-overridable) — kept in sync with ghostify.sh defaults.
|
||||
const THRESH = process.env.GHOST_KEY_THRESHOLD || '0.06';
|
||||
const TOL = process.env.GHOST_KEY_TOLERANCE || '0.10';
|
||||
const SOFT = process.env.GHOST_KEY_SOFTNESS || '0.15';
|
||||
const FPS = process.env.GHOST_FPS || '15';
|
||||
const MAXW = process.env.GHOST_MAXW || '512';
|
||||
const WEBP_Q = process.env.GHOST_WEBP_QUALITY || '80';
|
||||
const VP9_CRF = process.env.GHOST_VP9_CRF || '30';
|
||||
|
||||
// The shared luma-key + downscale filter chain.
|
||||
function keyChain() {
|
||||
const key = `format=rgba,lumakey=threshold=${THRESH}:tolerance=${TOL}:softness=${SOFT}`;
|
||||
const scale = `scale='min(iw,${MAXW})':-1:flags=lanczos`;
|
||||
return `${key},fps=${FPS},${scale}`;
|
||||
}
|
||||
|
||||
function run(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(FFMPEG, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
let stderr = '';
|
||||
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
||||
proc.on('error', reject); // e.g. ffmpeg not found (ENOENT)
|
||||
proc.on('close', (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`ffmpeg exited ${code}: ${stderr.slice(-500)}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** True if a usable ffmpeg binary is on PATH. */
|
||||
export function ffmpegAvailable() {
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn(FFMPEG, ['-version'], { stdio: 'ignore' });
|
||||
proc.on('error', () => resolve(false));
|
||||
proc.on('close', (code) => resolve(code === 0));
|
||||
});
|
||||
}
|
||||
|
||||
function outName(srcFilename, ext) {
|
||||
// wraith-1718.mp4 -> wraith-1718.webm / .webp, written alongside the source.
|
||||
const stem = basename(srcFilename, extname(srcFilename));
|
||||
return `${stem}.${ext}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a source MP4 (already saved in uploadDir) into transparent webm+webp.
|
||||
*
|
||||
* @param {string} uploadDir absolute uploads directory
|
||||
* @param {string} srcFilename the multer filename of the uploaded mp4
|
||||
* @returns {Promise<{ webm: string|null, webp: string|null }>} new filenames
|
||||
* (not full paths). Either may be null if that encode failed.
|
||||
*/
|
||||
export async function convertGhostMp4(uploadDir, srcFilename) {
|
||||
const srcPath = join(uploadDir, srcFilename);
|
||||
if (!existsSync(srcPath)) throw new Error(`source not found: ${srcPath}`);
|
||||
|
||||
const vf = keyChain();
|
||||
const result = { webm: null, webp: null };
|
||||
|
||||
// --- WebM (VP9 + alpha): the primary, browser-decoded sprite ---
|
||||
const webmName = outName(srcFilename, 'webm');
|
||||
try {
|
||||
await run([
|
||||
'-hide_banner', '-loglevel', 'error', '-y', '-i', srcPath,
|
||||
'-vf', vf, '-an',
|
||||
'-c:v', 'libvpx-vp9', '-pix_fmt', 'yuva420p',
|
||||
'-b:v', '0', '-crf', VP9_CRF, '-auto-alt-ref', '0',
|
||||
join(uploadDir, webmName),
|
||||
]);
|
||||
if (statSync(join(uploadDir, webmName)).size > 0) result.webm = webmName;
|
||||
} catch (e) {
|
||||
console.error('[ghost-media] webm encode failed:', e.message);
|
||||
}
|
||||
|
||||
// --- WebP fallback: for iOS Safari / no-VP9-alpha browsers ---
|
||||
const webpName = outName(srcFilename, 'webp');
|
||||
try {
|
||||
await run([
|
||||
'-hide_banner', '-loglevel', 'error', '-y', '-i', srcPath,
|
||||
'-vf', vf, '-loop', '0', '-an',
|
||||
'-c:v', 'libwebp_anim', '-lossless', '0', '-q:v', WEBP_Q,
|
||||
'-preset', 'drawing', '-compression_level', '6',
|
||||
join(uploadDir, webpName),
|
||||
]);
|
||||
if (statSync(join(uploadDir, webpName)).size > 0) result.webp = webpName;
|
||||
} catch (e) {
|
||||
console.error('[ghost-media] webp encode failed:', e.message);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in new issue
Block a user