Files
2026-10-16 23:33:13 -07:00

57 lines
3.1 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../artifacts/recroom-network-mirror');
const origin = 'https://recroom.network';
const allowed = new Set(['recroom.network', 'cdn.recroom.network']);
const queue = ['/', '/rooms', '/events', '/news', '/creator', '/shop', '/download'].map(p => origin + p);
const seen = new Set();
const manifest = [];
await fs.mkdir(root, { recursive: true });
function localPath(url) {
return (url.hostname === 'cdn.recroom.network' ? '/mirror-cdn' : '') + url.pathname;
}
function rewrite(text) {
return text.replaceAll('https://cdn.recroom.network', '/mirror-cdn')
.replaceAll('https://recroom.network', '');
}
function discover(text, base) {
const add = raw => {
try {
const value = raw.replaceAll('&', '&').replaceAll('\\/', '/');
const url = new URL(value, base);
if (!allowed.has(url.hostname) || url.protocol !== 'https:') return;
if (!/\.(?:js|css|woff2?|ttf|png|jpe?g|webp|svg|ico|avif)(?:$|\?)/i.test(url.pathname + url.search)) return;
url.hash = ''; if (!seen.has(url.href)) queue.push(url.href);
} catch {}
};
for (const m of text.matchAll(/(?:src|href|poster)=["']([^"']+)["']/g)) add(m[1]);
for (const m of text.matchAll(/url\(["']?([^\s)'";]+)["']?\)/g)) add(m[1]);
for (const m of text.matchAll(/["'`](https:\/\/[^"'`\s<>]+|\/(?:_next|static|logos|images)\/[^"'`\s<>]+)["'`]/g)) add(m[1]);
for (const m of text.matchAll(/["'](static\/(?:chunks|media)\/[^"']+)["']/g)) add('/_next/' + m[1]);
}
while (queue.length && seen.size < 350) {
const source = queue.shift(); if (seen.has(source)) continue; seen.add(source);
try {
const response = await fetch(source, { signal: AbortSignal.timeout(25000) });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const type = response.headers.get('content-type') || '';
const url = new URL(source);
let output = localPath(url);
if (type.includes('text/html')) output = output.replace(/\/$/, '') + '/index.html';
const filename = path.resolve(root, '.' + output);
if (!filename.startsWith(root + path.sep)) throw new Error('Invalid output path');
const bytes = Buffer.from(await response.arrayBuffer());
await fs.mkdir(path.dirname(filename), { recursive: true });
if (/javascript|text\/css|text\/html/.test(type)) {
const sourceText = bytes.toString('utf8'); discover(sourceText, source);
await fs.writeFile(filename, rewrite(sourceText));
} else await fs.writeFile(filename, bytes);
manifest.push({ source, file: output, type, bytes: bytes.length });
console.log(`${manifest.length}: ${output} (${bytes.length})`);
} catch (error) { manifest.push({ source, error: error.message }); console.log(`FAILED ${source}: ${error.message}`); }
}
await fs.writeFile(path.join(root, 'manifest.json'), JSON.stringify({ capturedAt: new Date().toISOString(), source: origin, files: manifest }, null, 2));
console.log(`Saved ${manifest.filter(x => !x.error).length} files; ${manifest.filter(x => x.error).length} failed; ${queue.length} queued. ${root}`);