mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 15:20:13 +00:00
feat(media-use): core infrastructure — manifest, cache, adopt, probe (#1682)
* 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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
eee376cb5e
commit
73f6d3e5be
@@ -0,0 +1,114 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user