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;
|
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 ─────────────────────────────────────────────────────────
|
// ── FFmpeg Utilities ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function runFfmpeg(args: string[], label: string): { stdout: Buffer; stderr: string } {
|
function runFfmpeg(args: string[], label: string): { stdout: Buffer; stderr: string } {
|
||||||
@@ -582,6 +590,7 @@ async function runTestSuite(
|
|||||||
logPretty("Rendering video...", "🎬");
|
logPretty("Rendering video...", "🎬");
|
||||||
|
|
||||||
const tempSrcDir = join(tempRoot, "src");
|
const tempSrcDir = join(tempRoot, "src");
|
||||||
|
copyFixtureSupportFiles(suite, tempRoot);
|
||||||
cpSync(suite.srcDir, tempSrcDir, { recursive: true });
|
cpSync(suite.srcDir, tempSrcDir, { recursive: true });
|
||||||
|
|
||||||
const job = createRenderJob({
|
const job = createRenderJob({
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
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 path, { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import {
|
import {
|
||||||
|
createFileServer,
|
||||||
HF_BRIDGE_SCRIPT,
|
HF_BRIDGE_SCRIPT,
|
||||||
HF_EARLY_STUB,
|
HF_EARLY_STUB,
|
||||||
injectScriptsAtHeadStart,
|
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", () => {
|
describe("HF_EARLY_STUB + HF_BRIDGE_SCRIPT integration", () => {
|
||||||
/**
|
/**
|
||||||
* Simulates the real injection order in a Puppeteer page:
|
* 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
|
* Returns true iff `child` is the same as, or nested inside, `parent` after
|
||||||
* symlink-free path normalization. Used to reject path-traversal attempts
|
* path normalization. Used to reject path-traversal attempts (e.g.
|
||||||
* (e.g. GET `/../etc/passwd`) before opening any file.
|
* GET `/../etc/passwd`) before opening any file.
|
||||||
*
|
*
|
||||||
* `path.join(root, "..")` normalizes traversal segments and can escape `root`
|
* `path.join(root, "..")` normalizes traversal segments and can escape `root`
|
||||||
* entirely, so the join return value alone is not a safe guard. Callers must
|
* 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
|
// Each candidate is rejected if `..` segments push it outside the
|
||||||
// intended root: `path.join` normalizes traversal but does not enforce
|
// intended root: `path.join` normalizes traversal but does not enforce
|
||||||
// containment, so a request like `GET /../etc/passwd` would otherwise
|
// 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;
|
let filePath: string | null = null;
|
||||||
if (compiledDir) {
|
if (compiledDir) {
|
||||||
const candidate = join(compiledDir, relativePath);
|
const candidate = join(compiledDir, relativePath);
|
||||||
if (
|
if (
|
||||||
existsSync(candidate) &&
|
existsSync(candidate) &&
|
||||||
isPathInside(candidate, compiledDir, { resolveSymlinks: true }) &&
|
isPathInside(candidate, compiledDir) &&
|
||||||
statSync(candidate).isFile()
|
statSync(candidate).isFile()
|
||||||
) {
|
) {
|
||||||
filePath = candidate;
|
filePath = candidate;
|
||||||
@@ -553,7 +554,7 @@ export function createFileServer(options: FileServerOptions): Promise<FileServer
|
|||||||
const candidate = join(projectDir, relativePath);
|
const candidate = join(projectDir, relativePath);
|
||||||
if (
|
if (
|
||||||
existsSync(candidate) &&
|
existsSync(candidate) &&
|
||||||
isPathInside(candidate, projectDir, { resolveSymlinks: true }) &&
|
isPathInside(candidate, projectDir) &&
|
||||||
statSync(candidate).isFile()
|
statSync(candidate).isFile()
|
||||||
) {
|
) {
|
||||||
filePath = candidate;
|
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