702871a98c
Serve missing *.thumbnail.png requests when the SVG sidecar exists, and schedule async PNG rasterization after public HTML is materialized so WeChat og:image URLs stop 404ing. Co-authored-by: Cursor <cursoragent@cursor.com>
236 lines
6.9 KiB
JavaScript
236 lines
6.9 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import {
|
|
buildMindSpacePublicRoutePath,
|
|
resolveMindSpaceUserPublishDir,
|
|
} from './mindspace-runtime-config.mjs';
|
|
import { PUBLIC_ZONE_DIR, PUBLISH_KEY_UUID } from './user-publish.mjs';
|
|
|
|
const USERNAME_SLUG = /^[a-z0-9_]{2,32}$/;
|
|
const MISPLACED_PUBLIC_HTML_NAME = /^[a-z0-9][a-z0-9._-]{0,127}\.html$/i;
|
|
|
|
export async function resolveMindSpacePublishOwnerKey(segment, resolveUsernameToUserId) {
|
|
const lower = String(segment ?? '').trim().toLowerCase();
|
|
if (!lower) return null;
|
|
if (PUBLISH_KEY_UUID.test(lower)) return lower;
|
|
if (!USERNAME_SLUG.test(lower)) return null;
|
|
const resolved = await resolveUsernameToUserId?.(lower);
|
|
return resolved ? String(resolved).toLowerCase() : lower;
|
|
}
|
|
|
|
export function decodeMindSpaceRouteSegment(segment) {
|
|
try {
|
|
return decodeURIComponent(segment);
|
|
} catch {
|
|
return segment;
|
|
}
|
|
}
|
|
|
|
export function resolveMindSpacePublicRouteTarget({ h5Root, ownerKey, parts = [] }) {
|
|
const targetDir = resolveMindSpaceUserPublishDir(h5Root, { id: ownerKey });
|
|
const resolvedRoot = path.resolve(targetDir);
|
|
const resolvedPath = path.resolve(path.join(targetDir, ...parts));
|
|
return {
|
|
targetDir,
|
|
resolvedRoot,
|
|
resolvedPath,
|
|
insideRoot:
|
|
resolvedPath === resolvedRoot || resolvedPath.startsWith(`${resolvedRoot}${path.sep}`),
|
|
};
|
|
}
|
|
|
|
export function buildMindSpaceCanonicalRedirect(ownerKey, parts = []) {
|
|
return buildMindSpacePublicRoutePath(ownerKey, parts);
|
|
}
|
|
|
|
export function resolveMindSpaceHtmlFallbackRedirect({ ownerKey, targetDir, resolvedRoot, rest = [] }) {
|
|
if (rest.length === 1 && String(rest[0]).toLowerCase().endsWith('.html')) {
|
|
const publicFallback = path.resolve(targetDir, PUBLIC_ZONE_DIR, rest[0]);
|
|
if (
|
|
publicFallback.startsWith(`${resolvedRoot}${path.sep}`) &&
|
|
fs.existsSync(publicFallback) &&
|
|
fs.statSync(publicFallback).isFile()
|
|
) {
|
|
return buildMindSpacePublicRoutePath(ownerKey, [PUBLIC_ZONE_DIR, rest[0]]);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolveMindSpaceSimilarHtmlRedirect({ ownerKey, relativePath }) {
|
|
if (!relativePath) return null;
|
|
return buildMindSpacePublicRoutePath(ownerKey, String(relativePath).split('/'));
|
|
}
|
|
|
|
export async function recoverMisplacedMindSpacePublicHtml({
|
|
h5Root,
|
|
targetDir,
|
|
resolvedRoot,
|
|
rest = [],
|
|
ensureThumbnail,
|
|
logger = console,
|
|
} = {}) {
|
|
if (rest.length !== 2 || rest[0] !== PUBLIC_ZONE_DIR) return null;
|
|
const filename = rest[1];
|
|
if (!MISPLACED_PUBLIC_HTML_NAME.test(filename)) return null;
|
|
|
|
const destination = path.resolve(targetDir, PUBLIC_ZONE_DIR, filename);
|
|
if (!destination.startsWith(`${resolvedRoot}${path.sep}`)) return null;
|
|
const candidates = [
|
|
path.resolve(h5Root, filename),
|
|
path.resolve(h5Root, PUBLIC_ZONE_DIR, filename),
|
|
];
|
|
for (const candidate of candidates) {
|
|
if (candidate === destination) continue;
|
|
if (!candidate.startsWith(`${path.resolve(h5Root)}${path.sep}`)) continue;
|
|
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isFile()) continue;
|
|
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
fs.copyFileSync(candidate, destination);
|
|
await ensureThumbnail?.(targetDir, `${PUBLIC_ZONE_DIR}/${filename}`);
|
|
logger?.warn?.(
|
|
`[MindSpace] recovered misplaced public HTML ${path.relative(h5Root, candidate)} -> ${path.relative(h5Root, destination)}`,
|
|
);
|
|
return destination;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function resolveMindSpacePublicRequest({
|
|
h5Root,
|
|
requestPath = '',
|
|
resolveUsernameToUserId,
|
|
resolveClosestHtmlRelativePath,
|
|
ensureThumbnail,
|
|
logger = console,
|
|
} = {}) {
|
|
const parts = String(requestPath || '')
|
|
.split('/')
|
|
.filter(Boolean);
|
|
if (parts.length < 1) {
|
|
return { action: 'not_found', reason: 'missing_path' };
|
|
}
|
|
|
|
const ownerKey = await resolveMindSpacePublishOwnerKey(parts[0], resolveUsernameToUserId);
|
|
if (!ownerKey) {
|
|
return { action: 'not_found', reason: 'missing_owner' };
|
|
}
|
|
|
|
if (parts[0].toLowerCase() !== ownerKey && parts[0].trim()) {
|
|
return {
|
|
action: 'redirect',
|
|
status: 301,
|
|
location: buildMindSpaceCanonicalRedirect(ownerKey, parts.slice(1)),
|
|
ownerKey,
|
|
};
|
|
}
|
|
|
|
const rest = parts.slice(1).map(decodeMindSpaceRouteSegment);
|
|
const routeTarget = resolveMindSpacePublicRouteTarget({
|
|
h5Root,
|
|
ownerKey,
|
|
parts: rest,
|
|
});
|
|
const { targetDir, resolvedRoot, resolvedPath } = routeTarget;
|
|
|
|
if (!routeTarget.insideRoot) {
|
|
return { action: 'forbidden', reason: 'outside_publish_root', ownerKey };
|
|
}
|
|
|
|
if (!fs.existsSync(targetDir)) {
|
|
return { action: 'not_found', reason: 'missing_owner_dir', ownerKey };
|
|
}
|
|
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
const publicFallbackRedirect = resolveMindSpaceHtmlFallbackRedirect({
|
|
ownerKey,
|
|
targetDir,
|
|
resolvedRoot,
|
|
rest,
|
|
});
|
|
if (publicFallbackRedirect) {
|
|
return {
|
|
action: 'redirect',
|
|
status: 301,
|
|
location: publicFallbackRedirect,
|
|
ownerKey,
|
|
};
|
|
}
|
|
|
|
if (rest.length === 2 && rest[0] === PUBLIC_ZONE_DIR && rest[1].toLowerCase().endsWith('.html')) {
|
|
const similarRelativePath = await resolveClosestHtmlRelativePath?.(
|
|
targetDir,
|
|
`${PUBLIC_ZONE_DIR}/${rest[1]}`,
|
|
);
|
|
if (similarRelativePath) {
|
|
return {
|
|
action: 'redirect',
|
|
status: 301,
|
|
location: resolveMindSpaceSimilarHtmlRedirect({
|
|
ownerKey,
|
|
relativePath: similarRelativePath,
|
|
}),
|
|
ownerKey,
|
|
};
|
|
}
|
|
}
|
|
|
|
const recoveredPublicHtml = await recoverMisplacedMindSpacePublicHtml({
|
|
h5Root,
|
|
targetDir,
|
|
resolvedRoot,
|
|
rest,
|
|
ensureThumbnail,
|
|
logger,
|
|
});
|
|
if (recoveredPublicHtml) {
|
|
return {
|
|
action: 'serve',
|
|
filePath: recoveredPublicHtml,
|
|
ownerKey,
|
|
targetDir,
|
|
};
|
|
}
|
|
|
|
// og:image points at <base>.thumbnail.png while only the SVG sidecar may exist on disk.
|
|
// Serve resolves lazily via ensureThumbnailPng in the MindSpace public file handler.
|
|
if (/\.thumbnail\.png$/i.test(resolvedPath)) {
|
|
const svgSibling = resolvedPath.replace(/\.png$/i, '.svg');
|
|
if (fs.existsSync(svgSibling)) {
|
|
return {
|
|
action: 'serve',
|
|
filePath: resolvedPath,
|
|
ownerKey,
|
|
targetDir,
|
|
};
|
|
}
|
|
}
|
|
|
|
return { action: 'not_found', reason: 'missing_file', ownerKey };
|
|
}
|
|
|
|
if (fs.statSync(resolvedPath).isDirectory()) {
|
|
const indexPath = path.join(resolvedPath, 'index.html');
|
|
if (fs.existsSync(indexPath)) {
|
|
return {
|
|
action: 'serve',
|
|
filePath: indexPath,
|
|
ownerKey,
|
|
targetDir,
|
|
};
|
|
}
|
|
return { action: 'not_found', reason: 'missing_directory_index', ownerKey };
|
|
}
|
|
|
|
return {
|
|
action: 'serve',
|
|
filePath: resolvedPath,
|
|
ownerKey,
|
|
targetDir,
|
|
};
|
|
}
|
|
|
|
export const mindspacePublicRouteInternals = {
|
|
USERNAME_SLUG,
|
|
MISPLACED_PUBLIC_HTML_NAME,
|
|
};
|