mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
fix(producer): serve symlinked render assets (#486)
This commit is contained in:
@@ -235,6 +235,14 @@ function discoverTestSuites(
|
||||
return suites;
|
||||
}
|
||||
|
||||
function copyFixtureSupportFiles(suite: TestSuite, tempRoot: string): void {
|
||||
const excluded = new Set(["src", "output", "meta.json", "failures"]);
|
||||
for (const entry of readdirSync(suite.dir)) {
|
||||
if (excluded.has(entry)) continue;
|
||||
cpSync(join(suite.dir, entry), join(tempRoot, entry), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// ── FFmpeg Utilities ─────────────────────────────────────────────────────────
|
||||
|
||||
function runFfmpeg(args: string[], label: string): { stdout: Buffer; stderr: string } {
|
||||
@@ -582,6 +590,7 @@ async function runTestSuite(
|
||||
logPretty("Rendering video...", "🎬");
|
||||
|
||||
const tempSrcDir = join(tempRoot, "src");
|
||||
copyFixtureSupportFiles(suite, tempRoot);
|
||||
cpSync(suite.srcDir, tempSrcDir, { recursive: true });
|
||||
|
||||
const job = createRenderJob({
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import path, { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import {
|
||||
createFileServer,
|
||||
HF_BRIDGE_SCRIPT,
|
||||
HF_EARLY_STUB,
|
||||
injectScriptsAtHeadStart,
|
||||
@@ -151,6 +152,45 @@ describe("isPathInside", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFileServer", () => {
|
||||
it("serves asset files through project-root symlinked directories", async () => {
|
||||
const workspaceDir = mkdtempSync(join(tmpdir(), "hf-file-server-symlink-assets-"));
|
||||
const adsDir = join(workspaceDir, "Ads");
|
||||
const projectDir = join(adsDir, "annual-upsell-2");
|
||||
const sharedDir = join(adsDir, "shared");
|
||||
|
||||
try {
|
||||
mkdirSync(projectDir, { recursive: true });
|
||||
mkdirSync(sharedDir, { recursive: true });
|
||||
writeFileSync(join(projectDir, "index.html"), "<!doctype html><html></html>");
|
||||
writeFileSync(
|
||||
join(sharedDir, "brand.css"),
|
||||
".aisplus-glass { backdrop-filter: blur(28px); }",
|
||||
);
|
||||
symlinkSync("../shared", join(projectDir, "shared"));
|
||||
|
||||
const server = await createFileServer({
|
||||
projectDir,
|
||||
preHeadScripts: [],
|
||||
headScripts: [],
|
||||
bodyScripts: [],
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`${server.url}/shared/brand.css`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain("text/css");
|
||||
expect(await response.text()).toContain(".aisplus-glass");
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
} finally {
|
||||
rmSync(workspaceDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("HF_EARLY_STUB + HF_BRIDGE_SCRIPT integration", () => {
|
||||
/**
|
||||
* Simulates the real injection order in a Puppeteer page:
|
||||
|
||||
@@ -31,8 +31,8 @@ type IsPathInsideOptions = {
|
||||
|
||||
/**
|
||||
* Returns true iff `child` is the same as, or nested inside, `parent` after
|
||||
* symlink-free path normalization. Used to reject path-traversal attempts
|
||||
* (e.g. GET `/../etc/passwd`) before opening any file.
|
||||
* path normalization. Used to reject path-traversal attempts (e.g.
|
||||
* GET `/../etc/passwd`) before opening any file.
|
||||
*
|
||||
* `path.join(root, "..")` normalizes traversal segments and can escape `root`
|
||||
* entirely, so the join return value alone is not a safe guard. Callers must
|
||||
@@ -537,13 +537,14 @@ export function createFileServer(options: FileServerOptions): Promise<FileServer
|
||||
// Each candidate is rejected if `..` segments push it outside the
|
||||
// intended root: `path.join` normalizes traversal but does not enforce
|
||||
// containment, so a request like `GET /../etc/passwd` would otherwise
|
||||
// be served straight off the filesystem.
|
||||
// be served straight off the filesystem. Keep this lexical so project
|
||||
// symlinks to sibling asset directories behave like preview mode.
|
||||
let filePath: string | null = null;
|
||||
if (compiledDir) {
|
||||
const candidate = join(compiledDir, relativePath);
|
||||
if (
|
||||
existsSync(candidate) &&
|
||||
isPathInside(candidate, compiledDir, { resolveSymlinks: true }) &&
|
||||
isPathInside(candidate, compiledDir) &&
|
||||
statSync(candidate).isFile()
|
||||
) {
|
||||
filePath = candidate;
|
||||
@@ -553,7 +554,7 @@ export function createFileServer(options: FileServerOptions): Promise<FileServer
|
||||
const candidate = join(projectDir, relativePath);
|
||||
if (
|
||||
existsSync(candidate) &&
|
||||
isPathInside(candidate, projectDir, { resolveSymlinks: true }) &&
|
||||
isPathInside(candidate, projectDir) &&
|
||||
statSync(candidate).isFile()
|
||||
) {
|
||||
filePath = candidate;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Render Symlinked Assets",
|
||||
"description": "Regression test for render-mode FileServer parity with preview when a project asset directory is a symlink to a sibling shared folder.",
|
||||
"tags": ["regression"],
|
||||
"minPsnr": 30,
|
||||
"maxFrameFailures": 0,
|
||||
"minAudioCorrelation": 0,
|
||||
"maxAudioLagWindows": 1,
|
||||
"renderConfig": {
|
||||
"fps": 24,
|
||||
"workers": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="shared/brand.css">
|
||||
</head>
|
||||
<body style="margin: 0">
|
||||
<div data-composition-id="render-symlinked-assets" data-width="320" data-height="180" data-duration="5" style="position: relative; width: 320px; height: 180px; overflow: hidden">
|
||||
<div class="stage clip" data-start="0" data-duration="5">
|
||||
<div class="card">SYMLINK CSS</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d73f03b86c6c60f8900627df3a71771b47386add8bcbb50ed7b162b81abea976
|
||||
size 10433
|
||||
@@ -0,0 +1,18 @@
|
||||
.stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: #0b1220;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 260px;
|
||||
height: 120px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 4px solid #86efac;
|
||||
background: #22c55e;
|
||||
color: #04130a;
|
||||
font: 700 34px system-ui, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="shared/brand.css" />
|
||||
</head>
|
||||
<body style="margin: 0">
|
||||
<div
|
||||
data-composition-id="render-symlinked-assets"
|
||||
data-width="320"
|
||||
data-height="180"
|
||||
data-duration="5"
|
||||
style="position: relative; width: 320px; height: 180px; overflow: hidden"
|
||||
>
|
||||
<div class="stage clip" data-start="0" data-duration="5">
|
||||
<div class="card">SYMLINK CSS</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
../shared
|
||||
Reference in New Issue
Block a user