feat: finalize mindspace service extraction phase a
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user