mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-03 12:17:03 +00:00
* fix(studio-server): serve projects whose names contain @ % # & or ? Five routes cut the decoded project id out of Hono's c.req.path, which leaves %40 %25 %23 %26 %3F encoded, so the cut missed and files, preview assets, sub-compositions, thumbnails and waveforms 404ed or 403ed for those names. One helper now takes the sub-path by segment from the raw URL. * fix(studio-server): thumbnails and waveforms stay inside the project for any name The thumbnail route built its preview URL from the raw project id and path, so # ? and % broke it, and neither route checked that the decoded path stays inside the project. Thumbnails now use the composition guard and an encoded URL; waveforms use the read-only asset guard. * fix(cli): play and present serve assets whose names contain @ % # & or ? Both /composition/* routes cut a prefix out of Hono's c.req.path with no decode. They now share studio-server's requestSubPath (renamed from projectSubPath and generalised to any route) with the five studio-server routes, so one helper owns how a request path becomes a file path. * fix(studio-server): a thumbnail request for the project folder answers 404 An empty thumbnail path resolved to the project directory and crashed reading it (500). * fix(studio-server): read the thumbnail's composition with one open Checking the path with stat and then reading it let the file change in between (CodeQL js/file-system-race). One open now serves the type check, the mtime and the read; a missing file still thumbnails from the preview as before, anything that is not a file answers 404.
57 lines
2.0 KiB
TypeScript
57 lines
2.0 KiB
TypeScript
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
import type { Hono } from "hono";
|
|
import type { StudioApiAdapter } from "../types.js";
|
|
import {
|
|
decodeAudioPeaks,
|
|
buildWaveformCacheKey,
|
|
writeWaveformCache,
|
|
isWaveformCacheDirectory,
|
|
} from "../helpers/waveform.js";
|
|
import { requestSubPath } from "../helpers/requestSubPath.js";
|
|
|
|
export function registerWaveformRoutes(api: Hono, adapter: StudioApiAdapter): void {
|
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
|
|
const assetPath = requestSubPath(c.req.url, "projects/:id/waveform");
|
|
const audioPath = resolve(project.dir, assetPath);
|
|
if (!isWithinProjectRoot(project.dir, audioPath)) {
|
|
return c.json({ error: "file not found" }, 404);
|
|
}
|
|
const stats = statSync(audioPath, { throwIfNoEntry: false });
|
|
if (!stats) return c.json({ error: "file not found" }, 404);
|
|
|
|
const cacheDir = join(project.dir, ".waveform-cache");
|
|
// Keyed on the file's size and mtime as well as its name, so re-encoding an
|
|
// asset in place invalidates its peaks instead of drawing the old ones.
|
|
const cachePath = join(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
|
|
try {
|
|
if (isWaveformCacheDirectory(cacheDir) && existsSync(cachePath)) {
|
|
const peaks = JSON.parse(readFileSync(cachePath, "utf-8")) as number[];
|
|
return c.json({ peaks });
|
|
}
|
|
} catch {
|
|
// corrupt or inaccessible cache — regenerate
|
|
}
|
|
|
|
let peaks: number[];
|
|
try {
|
|
peaks = await decodeAudioPeaks(audioPath);
|
|
} catch {
|
|
return c.json({ error: "failed to decode audio" }, 500);
|
|
}
|
|
|
|
try {
|
|
writeWaveformCache(cachePath, peaks);
|
|
} catch {
|
|
// cache write failure is non-fatal
|
|
}
|
|
|
|
return c.json({ peaks });
|
|
});
|
|
}
|