mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
* feat(media-use): core infrastructure — manifest, cache, adopt, probe Foundation for media-use — the media resolution layer for HyperFrames. - manifest.mjs: JSONL read/write/find for .media/manifest.jsonl - index-gen.mjs: regenerate agent-readable index.md from manifest - cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel) - freeze.mjs: download URL or copy local file to .media/ - probe.mjs: extract duration/dimensions via ffprobe - adopt.mjs: scan assets/ directory, register existing files with metadata - 19 passing tests (manifest round-trip, cache, promote, index generation) * fix(media-use): oxfmt formatting + cap freeze download size Format adopt/cache/probe/manifest.test (CI oxfmt --check gate). Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill the disk (addresses CodeQL #670: network data written to file). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
3.2 KiB
JavaScript
115 lines
3.2 KiB
JavaScript
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
|
|
import { join, basename } from "node:path";
|
|
import { createHash } from "node:crypto";
|
|
import { homedir } from "node:os";
|
|
import { readManifest, appendRecord } from "./manifest.mjs";
|
|
|
|
const SCHEMA_PREFIX = "mu-v1-";
|
|
const KEY_HEX_CHARS = 16;
|
|
const COMPLETE_SENTINEL = ".hf-complete";
|
|
|
|
export function globalMediaDir() {
|
|
return join(homedir(), ".media");
|
|
}
|
|
|
|
export function contentHash(filePath) {
|
|
const bytes = readFileSync(filePath);
|
|
return createHash("sha256").update(bytes).digest("hex");
|
|
}
|
|
|
|
function cacheEntryDir(rootDir, sha) {
|
|
return join(rootDir, SCHEMA_PREFIX + sha.slice(0, KEY_HEX_CHARS));
|
|
}
|
|
|
|
function isComplete(entryDir) {
|
|
return existsSync(join(entryDir, COMPLETE_SENTINEL));
|
|
}
|
|
|
|
function markComplete(entryDir) {
|
|
writeFileSync(join(entryDir, COMPLETE_SENTINEL), "", "utf8");
|
|
}
|
|
|
|
function readGlobalManifest() {
|
|
return readManifest(globalMediaDir());
|
|
}
|
|
|
|
function validateCacheHit(match) {
|
|
if (!match?.sha) return null;
|
|
return isComplete(cacheEntryDir(globalMediaDir(), match.sha)) ? match : null;
|
|
}
|
|
|
|
export function cacheGet(prompt, type) {
|
|
return validateCacheHit(
|
|
readGlobalManifest().find(
|
|
(r) => r.reusable && r.provenance?.prompt === prompt && (type == null || r.type === type),
|
|
),
|
|
);
|
|
}
|
|
|
|
export function cacheGetByEntity(entity) {
|
|
const lower = entity.toLowerCase();
|
|
return validateCacheHit(
|
|
readGlobalManifest().find((r) => r.reusable && r.entity && r.entity.toLowerCase() === lower),
|
|
);
|
|
}
|
|
|
|
export function cachePut(filePath, record) {
|
|
const sha = contentHash(filePath);
|
|
const dir = globalMediaDir();
|
|
const entryDir = cacheEntryDir(dir, sha);
|
|
mkdirSync(entryDir, { recursive: true });
|
|
|
|
const dest = join(entryDir, basename(filePath));
|
|
copyFileSync(filePath, dest);
|
|
markComplete(entryDir);
|
|
|
|
const globalRecord = {
|
|
...record,
|
|
sha,
|
|
reusable: true,
|
|
cached_path: dest,
|
|
};
|
|
appendRecord(globalMediaDir(), globalRecord);
|
|
return { sha, cached_path: dest };
|
|
}
|
|
|
|
export function importFromCache(cacheRecord, projectDir, localId, localPath) {
|
|
const sha = cacheRecord.sha;
|
|
const entryDir = cacheEntryDir(globalMediaDir(), sha);
|
|
if (!isComplete(entryDir)) return null;
|
|
|
|
const cachedFile = cacheRecord.cached_path;
|
|
if (!cachedFile || !existsSync(cachedFile)) return null;
|
|
|
|
mkdirSync(join(projectDir, ".media"), { recursive: true });
|
|
const fullDest = join(projectDir, localPath);
|
|
mkdirSync(join(fullDest, ".."), { recursive: true });
|
|
copyFileSync(cachedFile, fullDest);
|
|
|
|
const projectRecord = {
|
|
...cacheRecord,
|
|
id: localId,
|
|
path: localPath,
|
|
provenance: {
|
|
...cacheRecord.provenance,
|
|
imported_from: sha,
|
|
},
|
|
};
|
|
delete projectRecord.sha;
|
|
delete projectRecord.reusable;
|
|
delete projectRecord.cached_path;
|
|
|
|
return projectRecord;
|
|
}
|
|
|
|
export function promote(projectDir, id) {
|
|
const records = readManifest(projectDir);
|
|
const record = records.find((r) => r.id === id);
|
|
if (!record) throw new Error(`asset not found in project manifest: ${id}`);
|
|
|
|
const filePath = join(projectDir, record.path);
|
|
if (!existsSync(filePath)) throw new Error(`asset file not found: ${filePath}`);
|
|
|
|
return cachePut(filePath, record);
|
|
}
|