diff --git a/scripts/mirror.js b/scripts/mirror.js index 0e1e696..2541fd9 100644 --- a/scripts/mirror.js +++ b/scripts/mirror.js @@ -42,23 +42,47 @@ const CHAT_OVERLAY = ` `; -function urlToSafePath(u) { +function mimeExt(ct) { + ct = (ct || '').split(';')[0].trim().toLowerCase(); + if (ct.includes('css')) return '.css'; + if (ct.includes('javascript') || ct.includes('ecmascript')) return '.js'; + if (ct === 'image/png') return '.png'; + if (ct === 'image/jpeg' || ct === 'image/jpg') return '.jpg'; + if (ct === 'image/gif') return '.gif'; + if (ct === 'image/svg+xml') return '.svg'; + if (ct === 'image/webp') return '.webp'; + if (ct === 'image/x-icon' || ct === 'image/vnd.microsoft.icon') return '.ico'; + if (ct === 'font/woff2' || ct === 'application/font-woff2') return '.woff2'; + if (ct === 'font/woff' || ct === 'application/font-woff') return '.woff'; + if (ct === 'font/ttf' || ct === 'application/x-font-ttf') return '.ttf'; + if (ct === 'font/otf') return '.otf'; + if (ct.startsWith('text/html')) return '.html'; + if (ct.startsWith('application/json')) return '.json'; + return ''; +} + +function urlToSafePath(u, contentType) { const parsed = new URL(u); let p = parsed.pathname; if (parsed.search) { let safe = parsed.search.slice(1).replace(/[^a-zA-Z0-9._-]/g, '_'); - // Hash overly long query strings to avoid ENAMETOOLONG if (safe.length > 80) { safe = crypto.createHash('md5').update(parsed.search).digest('hex').slice(0, 12); } p += '_q_' + safe; } - // also hash if path itself is way too long - const basename = path.basename(p); - if (basename.length > 200) { + // Append correct extension if missing — so express.static sends proper Content-Type + const baseName = path.basename(p); + if (!/\.[a-z0-9]{2,5}$/i.test(baseName) && contentType) { + const ext = mimeExt(contentType); + if (ext) p += ext; + } + // Hash if too long + const baseName2 = path.basename(p); + if (baseName2.length > 200) { const ext = path.extname(p); const dir = path.dirname(p); - p = dir + '/' + crypto.createHash('md5').update(basename).digest('hex').slice(0, 16) + ext; + p = dir + '/' + crypto.createHash('md5').update(baseName2).digest('hex').slice(0, 16) + ext; } return p.replace(/^\/+/, ''); } @@ -99,7 +123,8 @@ async function main() { if (!rurl.startsWith('http')) return; try { const buf = await resp.body(); - if (buf && buf.length > 0) resources.set(rurl, buf); + const ct = resp.headers()['content-type'] || ''; + if (buf && buf.length > 0) resources.set(rurl, { buf, ct }); } catch (e) { /* redirect/204/etc */ } }); @@ -115,10 +140,10 @@ async function main() { // Write resources fs.mkdirSync(MIRROR, { recursive: true }); const mapping = new Map(); // absUrl → /q/mirror/ - for (const [rurl, buf] of resources) { + for (const [rurl, { buf, ct }] of resources) { const u = new URL(rurl); if (u.hostname !== HOST) continue; // skip third-party (cdnjs etc) — let browser load them - const safe = urlToSafePath(rurl); + const safe = urlToSafePath(rurl, ct); const local = path.join(MIRROR, safe); fs.mkdirSync(path.dirname(local), { recursive: true }); fs.writeFileSync(local, buf);