@hyperframes/studio-server 0.8.80 → 0.8.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-YGANSFUU.js → chunk-FTDTO3QP.js} +3 -3
- package/dist/{chunk-56JREE33.js → chunk-G6IIFKXY.js} +7 -14
- package/dist/{chunk-56JREE33.js.map → chunk-G6IIFKXY.js.map} +1 -1
- package/dist/{chunk-KOB22VPG.js → chunk-TJQ76IEA.js} +71 -5
- package/dist/chunk-TJQ76IEA.js.map +1 -0
- package/dist/helpers/mediaCodecMap.js +1 -1
- package/dist/helpers/mediaProxyPreview.d.ts +1 -1
- package/dist/helpers/mediaProxyPreview.js +3 -3
- package/dist/helpers/proxyTranscoder.js +2 -2
- package/dist/index.d.ts +7 -3
- package/dist/index.js +214 -230
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-B7ckRI-l.d.ts → mediaProxyPreview-BOp3Wa9v.d.ts} +4 -3
- package/package.json +3 -3
- package/dist/chunk-KOB22VPG.js.map +0 -1
- /package/dist/{chunk-YGANSFUU.js.map → chunk-FTDTO3QP.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -22,21 +22,28 @@ import {
|
|
|
22
22
|
isAutoProxyEnabled,
|
|
23
23
|
proxyEtagSalt,
|
|
24
24
|
resolvePreviewMediaCodecProbeCache
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-FTDTO3QP.js";
|
|
26
26
|
import {
|
|
27
27
|
ProxyCapacityError,
|
|
28
28
|
ProxyTranscodeError,
|
|
29
29
|
resolveProxy
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-G6IIFKXY.js";
|
|
31
31
|
import {
|
|
32
32
|
PROXY_VARIANT_CONFIG,
|
|
33
33
|
decideMediaProxyEligibility,
|
|
34
|
+
isInHiddenOrVendorDir,
|
|
34
35
|
isProxyVariantRequest,
|
|
36
|
+
isSafePath,
|
|
37
|
+
pinWithinProject,
|
|
35
38
|
probeAssetCodec,
|
|
36
39
|
probeMediaMetadata,
|
|
40
|
+
realFilePath,
|
|
41
|
+
realpath,
|
|
37
42
|
recordProxyRequest,
|
|
38
|
-
resolveProxyVariantRequest
|
|
39
|
-
|
|
43
|
+
resolveProxyVariantRequest,
|
|
44
|
+
resolveWithinProject,
|
|
45
|
+
walkDir
|
|
46
|
+
} from "./chunk-TJQ76IEA.js";
|
|
40
47
|
import {
|
|
41
48
|
STUDIO_MANUAL_EDITS_PATH,
|
|
42
49
|
createStudioManualEditsRenderBodyScript,
|
|
@@ -52,64 +59,7 @@ import { Hono } from "hono";
|
|
|
52
59
|
|
|
53
60
|
// src/routes/projects.ts
|
|
54
61
|
import { readFile } from "fs/promises";
|
|
55
|
-
import { join as
|
|
56
|
-
|
|
57
|
-
// src/helpers/safePath.ts
|
|
58
|
-
import { basename, dirname, isAbsolute, join, relative, sep } from "path";
|
|
59
|
-
import { readdirSync, realpathSync } from "fs";
|
|
60
|
-
import { resolveWithinProject } from "@hyperframes/core";
|
|
61
|
-
import { isSafePath, resolveWithinProject as resolveWithinProject2 } from "@hyperframes/core";
|
|
62
|
-
function realFilePath(filePath) {
|
|
63
|
-
try {
|
|
64
|
-
return realpathSync(filePath);
|
|
65
|
-
} catch {
|
|
66
|
-
const dir = dirname(filePath);
|
|
67
|
-
return dir === filePath ? filePath : join(realFilePath(dir), basename(filePath));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function pinWithinProject(base, relativePath) {
|
|
71
|
-
const checked = resolveWithinProject(base, relativePath);
|
|
72
|
-
if (!checked) return null;
|
|
73
|
-
const inside = relative(realFilePath(base), realFilePath(checked));
|
|
74
|
-
const escapes = inside === ".." || inside.startsWith(`..${sep}`) || isAbsolute(inside);
|
|
75
|
-
return escapes ? null : join(base, inside);
|
|
76
|
-
}
|
|
77
|
-
var IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
78
|
-
".thumbnails",
|
|
79
|
-
".transcode-cache",
|
|
80
|
-
".waveform-cache",
|
|
81
|
-
"node_modules",
|
|
82
|
-
".git"
|
|
83
|
-
]);
|
|
84
|
-
function shouldIgnoreDir(rel) {
|
|
85
|
-
return rel === ".hyperframes/backup";
|
|
86
|
-
}
|
|
87
|
-
function isInHiddenOrVendorDir(relPath) {
|
|
88
|
-
const segments = relPath.split("/");
|
|
89
|
-
return segments.slice(0, -1).some((seg) => seg.startsWith(".") || seg === "node_modules");
|
|
90
|
-
}
|
|
91
|
-
var UNREADABLE_DIR_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "ENOENT", "ENOTDIR"]);
|
|
92
|
-
function walkDir(dir, prefix = "") {
|
|
93
|
-
let entries;
|
|
94
|
-
try {
|
|
95
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
96
|
-
} catch (err) {
|
|
97
|
-
const code = err.code;
|
|
98
|
-
if (!prefix || !code || !UNREADABLE_DIR_CODES.has(code)) throw err;
|
|
99
|
-
return [];
|
|
100
|
-
}
|
|
101
|
-
const files = [];
|
|
102
|
-
for (const entry of entries) {
|
|
103
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
104
|
-
if (IGNORE_DIRS.has(entry.name) || shouldIgnoreDir(rel)) continue;
|
|
105
|
-
if (entry.isDirectory()) {
|
|
106
|
-
files.push(...walkDir(join(dir, entry.name), rel));
|
|
107
|
-
} else {
|
|
108
|
-
files.push(rel);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return files;
|
|
112
|
-
}
|
|
62
|
+
import { join as join2 } from "path";
|
|
113
63
|
|
|
114
64
|
// src/helpers/hfIdPersist.ts
|
|
115
65
|
import { ensureHfIds } from "@hyperframes/parsers/hf-ids";
|
|
@@ -123,7 +73,7 @@ import {
|
|
|
123
73
|
readFileSync,
|
|
124
74
|
writeFileSync
|
|
125
75
|
} from "fs";
|
|
126
|
-
import { dirname
|
|
76
|
+
import { dirname, join } from "path";
|
|
127
77
|
|
|
128
78
|
// src/helpers/atomicFile.ts
|
|
129
79
|
import * as fs from "fs";
|
|
@@ -145,10 +95,10 @@ function replaceFileAtomically(filePath, content, mode, operations = fs) {
|
|
|
145
95
|
|
|
146
96
|
// src/helpers/hfIdPersist.ts
|
|
147
97
|
var isCompositionSource = (html) => /data-composition-id\s*=/.test(html);
|
|
148
|
-
var STAMP_RECORD =
|
|
98
|
+
var STAMP_RECORD = join(".hyperframes", "hf-ids-stamped.json");
|
|
149
99
|
var contentHash = (text2) => createHash("sha1").update(text2).digest("base64url");
|
|
150
100
|
function stampProjectHfIds(projectDir) {
|
|
151
|
-
const recordPath =
|
|
101
|
+
const recordPath = join(projectDir, STAMP_RECORD);
|
|
152
102
|
let last = {};
|
|
153
103
|
try {
|
|
154
104
|
last = JSON.parse(readFileSync(recordPath, "utf-8"));
|
|
@@ -157,7 +107,7 @@ function stampProjectHfIds(projectDir) {
|
|
|
157
107
|
const next = {};
|
|
158
108
|
for (const file of walkDir(projectDir)) {
|
|
159
109
|
if (!file.endsWith(".html") || isInHiddenOrVendorDir(file)) continue;
|
|
160
|
-
const absPath =
|
|
110
|
+
const absPath = join(projectDir, file);
|
|
161
111
|
let html;
|
|
162
112
|
try {
|
|
163
113
|
html = readFileSync(absPath, "utf-8");
|
|
@@ -173,7 +123,7 @@ function stampProjectHfIds(projectDir) {
|
|
|
173
123
|
next[file] = hash;
|
|
174
124
|
}
|
|
175
125
|
try {
|
|
176
|
-
mkdirSync(
|
|
126
|
+
mkdirSync(dirname(recordPath), { recursive: true });
|
|
177
127
|
writeFileSync(recordPath, JSON.stringify(next));
|
|
178
128
|
} catch {
|
|
179
129
|
}
|
|
@@ -226,8 +176,8 @@ function stampFileHfIds(filePath) {
|
|
|
226
176
|
|
|
227
177
|
// src/helpers/projectSignature.ts
|
|
228
178
|
import { createHash as createHash2 } from "crypto";
|
|
229
|
-
import { lstatSync, readFileSync as readFileSync2, readdirSync
|
|
230
|
-
import { extname, isAbsolute
|
|
179
|
+
import { lstatSync, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
180
|
+
import { extname, isAbsolute, relative, resolve, sep } from "path";
|
|
231
181
|
var SIGNATURE_TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
232
182
|
".cjs",
|
|
233
183
|
".css",
|
|
@@ -262,18 +212,18 @@ var STUDIO_SIGNATURE_MANIFEST_PATHS = [
|
|
|
262
212
|
".hyperframes/studio-motion.json"
|
|
263
213
|
];
|
|
264
214
|
function affectsProjectSignature(projectDir, changedPath) {
|
|
265
|
-
const relativePath =
|
|
266
|
-
if (relativePath === "" || relativePath.startsWith("..") ||
|
|
215
|
+
const relativePath = relative(resolve(projectDir), resolve(changedPath));
|
|
216
|
+
if (relativePath === "" || relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
267
217
|
return false;
|
|
268
218
|
}
|
|
269
|
-
const segments = relativePath.split(
|
|
219
|
+
const segments = relativePath.split(sep);
|
|
270
220
|
if (STUDIO_SIGNATURE_MANIFEST_PATHS.includes(segments.join("/"))) return true;
|
|
271
221
|
return !segments.some((segment) => SIGNATURE_EXCLUDED_DIRS.has(segment));
|
|
272
222
|
}
|
|
273
223
|
var projectSignatureCache = /* @__PURE__ */ new Map();
|
|
274
224
|
function isPathWithin(parentDir, childPath) {
|
|
275
|
-
const childRelativePath =
|
|
276
|
-
return childRelativePath === "" || !childRelativePath.startsWith("..") && !
|
|
225
|
+
const childRelativePath = relative(parentDir, childPath);
|
|
226
|
+
return childRelativePath === "" || !childRelativePath.startsWith("..") && !isAbsolute(childRelativePath);
|
|
277
227
|
}
|
|
278
228
|
function isTextContentEligible(file, size) {
|
|
279
229
|
return SIGNATURE_TEXT_EXTENSIONS.has(extname(file).toLowerCase()) && size <= MAX_SIGNATURE_TEXT_BYTES;
|
|
@@ -281,7 +231,7 @@ function isTextContentEligible(file, size) {
|
|
|
281
231
|
function collectProjectSignatureFiles(projectDir, dir, files) {
|
|
282
232
|
let entries;
|
|
283
233
|
try {
|
|
284
|
-
entries =
|
|
234
|
+
entries = readdirSync(dir).sort();
|
|
285
235
|
} catch {
|
|
286
236
|
return;
|
|
287
237
|
}
|
|
@@ -334,7 +284,7 @@ function collectProjectSignatureManifestFiles(projectDir, files) {
|
|
|
334
284
|
function createProjectFingerprint(projectDir, files) {
|
|
335
285
|
const hash = createHash2("sha256");
|
|
336
286
|
for (const entry of files) {
|
|
337
|
-
hash.update(
|
|
287
|
+
hash.update(relative(projectDir, entry.file));
|
|
338
288
|
hash.update("\0");
|
|
339
289
|
hash.update(String(entry.size));
|
|
340
290
|
hash.update("\0");
|
|
@@ -356,7 +306,7 @@ function collectProjectFiles(normalizedProjectDir) {
|
|
|
356
306
|
function listProjectFiles(projectDir) {
|
|
357
307
|
const normalizedProjectDir = resolve(projectDir);
|
|
358
308
|
return collectProjectFiles(normalizedProjectDir).map((entry) => ({
|
|
359
|
-
path:
|
|
309
|
+
path: relative(normalizedProjectDir, entry.file).split(sep).join("/"),
|
|
360
310
|
size: entry.size,
|
|
361
311
|
mtimeMs: entry.mtimeMs,
|
|
362
312
|
ctimeMs: entry.ctimeMs
|
|
@@ -374,7 +324,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
374
324
|
const normalizedProjectDir = resolve(projectDir);
|
|
375
325
|
const collected = collectProjectFiles(normalizedProjectDir);
|
|
376
326
|
const files = collected.filter(
|
|
377
|
-
(entry) => !excluding.has(
|
|
327
|
+
(entry) => !excluding.has(relative(normalizedProjectDir, entry.file).split(sep).join("/"))
|
|
378
328
|
);
|
|
379
329
|
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
380
330
|
const fingerprint = createProjectFingerprint(normalizedProjectDir, files);
|
|
@@ -383,7 +333,7 @@ function createProjectSignature(projectDir, excluding = /* @__PURE__ */ new Set(
|
|
|
383
333
|
if (cached?.fingerprint === fingerprint) return cached.signature;
|
|
384
334
|
const hash = createHash2("sha256");
|
|
385
335
|
for (const entry of files) {
|
|
386
|
-
const relativePath =
|
|
336
|
+
const relativePath = relative(normalizedProjectDir, entry.file);
|
|
387
337
|
hash.update(relativePath);
|
|
388
338
|
hash.update("\0");
|
|
389
339
|
hash.update(String(entry.size));
|
|
@@ -410,7 +360,7 @@ async function filterCompositionFiles(projectDir, files) {
|
|
|
410
360
|
const checks = await Promise.all(
|
|
411
361
|
htmlFiles.map(async (f) => {
|
|
412
362
|
try {
|
|
413
|
-
const content = await readFile(
|
|
363
|
+
const content = await readFile(join2(projectDir, f), "utf-8");
|
|
414
364
|
return isCompositionSource(content);
|
|
415
365
|
} catch {
|
|
416
366
|
return false;
|
|
@@ -464,9 +414,9 @@ import {
|
|
|
464
414
|
statSync as statSync3,
|
|
465
415
|
fstatSync as fstatSync2,
|
|
466
416
|
renameSync as renameSync2,
|
|
467
|
-
readdirSync as
|
|
417
|
+
readdirSync as readdirSync3
|
|
468
418
|
} from "fs";
|
|
469
|
-
import { resolve as resolve3, dirname as
|
|
419
|
+
import { resolve as resolve3, dirname as dirname4, join as join6 } from "path";
|
|
470
420
|
|
|
471
421
|
// src/helpers/mime.ts
|
|
472
422
|
var MIME_TYPES = {
|
|
@@ -526,7 +476,7 @@ import {
|
|
|
526
476
|
rmSync,
|
|
527
477
|
statSync
|
|
528
478
|
} from "fs";
|
|
529
|
-
import { dirname as
|
|
479
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
530
480
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
531
481
|
var SAMPLE_RATE = 4e3;
|
|
532
482
|
var PEAK_COUNT = 4e3;
|
|
@@ -593,11 +543,11 @@ function decodeAudioPeaks(audioPath) {
|
|
|
593
543
|
});
|
|
594
544
|
}
|
|
595
545
|
async function generateWaveformCache(projectDir, assetPath) {
|
|
596
|
-
const audioPath =
|
|
546
|
+
const audioPath = join3(projectDir, assetPath);
|
|
597
547
|
if (!existsSync(audioPath)) return;
|
|
598
548
|
const stats = statSync(audioPath);
|
|
599
|
-
const cacheDir =
|
|
600
|
-
const cachePath =
|
|
549
|
+
const cacheDir = join3(projectDir, ".waveform-cache");
|
|
550
|
+
const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
601
551
|
if (isWaveformCacheDirectory(cacheDir) && existsSync(cachePath)) return;
|
|
602
552
|
const peaks = await decodeAudioPeaks(audioPath);
|
|
603
553
|
writeWaveformCache(cachePath, peaks);
|
|
@@ -606,7 +556,7 @@ function isWaveformCacheDirectory(cacheDir) {
|
|
|
606
556
|
return lstatSync2(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
607
557
|
}
|
|
608
558
|
function writeWaveformCache(cachePath, peaks) {
|
|
609
|
-
const cacheDir =
|
|
559
|
+
const cacheDir = dirname2(cachePath);
|
|
610
560
|
try {
|
|
611
561
|
mkdirSync2(cacheDir, { mode: 448 });
|
|
612
562
|
} catch (error) {
|
|
@@ -615,9 +565,9 @@ function writeWaveformCache(cachePath, peaks) {
|
|
|
615
565
|
if (!isWaveformCacheDirectory(cacheDir)) {
|
|
616
566
|
throw new Error("Waveform cache must be a directory, not a symlink");
|
|
617
567
|
}
|
|
618
|
-
const stagingDir = mkdtempSync(
|
|
568
|
+
const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
|
|
619
569
|
try {
|
|
620
|
-
const stagingPath =
|
|
570
|
+
const stagingPath = join3(stagingDir, "peaks.json");
|
|
621
571
|
writeFileSync2(stagingPath, JSON.stringify(peaks), { flag: "wx" });
|
|
622
572
|
renameSync(stagingPath, cachePath);
|
|
623
573
|
} finally {
|
|
@@ -632,7 +582,7 @@ function writeWaveformCache(cachePath, peaks) {
|
|
|
632
582
|
import { spawnSync } from "child_process";
|
|
633
583
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
634
584
|
import { tmpdir } from "os";
|
|
635
|
-
import { basename
|
|
585
|
+
import { basename, join as join4 } from "path";
|
|
636
586
|
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
637
587
|
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
638
588
|
function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
@@ -669,8 +619,8 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
|
669
619
|
}
|
|
670
620
|
}
|
|
671
621
|
function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
672
|
-
const tempDir = mkdtempSync2(
|
|
673
|
-
const tempPath =
|
|
622
|
+
const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
|
|
623
|
+
const tempPath = join4(tempDir, basename(fileName));
|
|
674
624
|
try {
|
|
675
625
|
writeFileSync3(tempPath, buffer);
|
|
676
626
|
return validateUploadedMedia(tempPath, runner);
|
|
@@ -680,9 +630,9 @@ function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
|
680
630
|
}
|
|
681
631
|
|
|
682
632
|
// src/helpers/backupJournal.ts
|
|
683
|
-
import { mkdirSync as mkdirSync3, readdirSync as
|
|
633
|
+
import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync4 } from "fs";
|
|
684
634
|
import { Buffer as Buffer2 } from "buffer";
|
|
685
|
-
import { join as
|
|
635
|
+
import { join as join5, relative as relative2 } from "path";
|
|
686
636
|
var DEFAULT_KEEP_PER_FILE = 10;
|
|
687
637
|
function backupKeyForPath(path) {
|
|
688
638
|
return Buffer2.from(path, "utf-8").toString("base64url");
|
|
@@ -692,7 +642,7 @@ function timestampPrefix() {
|
|
|
692
642
|
}
|
|
693
643
|
function backupPathForResponse(projectDir, backupPath) {
|
|
694
644
|
if (!backupPath) return null;
|
|
695
|
-
const rel =
|
|
645
|
+
const rel = relative2(projectDir, backupPath);
|
|
696
646
|
if (!rel || rel.startsWith("..")) return null;
|
|
697
647
|
return rel.split("\\").join("/");
|
|
698
648
|
}
|
|
@@ -700,8 +650,8 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
|
700
650
|
if (!isSafePath(projectDir, absPath)) return { backupPath: null };
|
|
701
651
|
try {
|
|
702
652
|
const content = readFileSync3(absPath);
|
|
703
|
-
const relativePath =
|
|
704
|
-
const backupDir =
|
|
653
|
+
const relativePath = relative2(projectDir, absPath);
|
|
654
|
+
const backupDir = join5(projectDir, ".hyperframes", "backup");
|
|
705
655
|
mkdirSync3(backupDir, { recursive: true });
|
|
706
656
|
const backupKey = backupKeyForPath(relativePath);
|
|
707
657
|
const backupPath = nextBackupPath(backupDir, backupKey);
|
|
@@ -717,7 +667,7 @@ function snapshotBeforeWrite(projectDir, absPath, options = {}) {
|
|
|
717
667
|
}
|
|
718
668
|
function nextBackupPath(backupDir, backupKey) {
|
|
719
669
|
const base = `${timestampPrefix()}-${backupKey}`;
|
|
720
|
-
let candidate =
|
|
670
|
+
let candidate = join5(backupDir, base);
|
|
721
671
|
let counter = 2;
|
|
722
672
|
while (true) {
|
|
723
673
|
try {
|
|
@@ -728,7 +678,7 @@ function nextBackupPath(backupDir, backupKey) {
|
|
|
728
678
|
}
|
|
729
679
|
throw error;
|
|
730
680
|
}
|
|
731
|
-
candidate =
|
|
681
|
+
candidate = join5(backupDir, `${base}-${counter}`);
|
|
732
682
|
counter += 1;
|
|
733
683
|
}
|
|
734
684
|
}
|
|
@@ -736,7 +686,7 @@ function pruneBackups(backupDir, backupKey, keepPerFile) {
|
|
|
736
686
|
const keep = Math.max(1, Math.floor(keepPerFile));
|
|
737
687
|
const suffix = `-${backupKey}`;
|
|
738
688
|
const numberedSuffix = new RegExp(`-${backupKey}-\\d+$`);
|
|
739
|
-
const matches =
|
|
689
|
+
const matches = readdirSync2(backupDir).filter((name) => name.endsWith(suffix) || numberedSuffix.test(name)).map((name) => join5(backupDir, name)).sort((a, b) => {
|
|
740
690
|
return b.localeCompare(a);
|
|
741
691
|
});
|
|
742
692
|
for (const file of matches.slice(keep)) {
|
|
@@ -937,9 +887,9 @@ import { parseHTML as parseHTML2 } from "linkedom";
|
|
|
937
887
|
import { ensureHfIds as ensureHfIds2 } from "@hyperframes/parsers/hf-ids";
|
|
938
888
|
|
|
939
889
|
// src/helpers/compositionInsertion.ts
|
|
940
|
-
import { existsSync as existsSync2, readFileSync as readFileSync5
|
|
890
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
941
891
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
942
|
-
import { dirname as
|
|
892
|
+
import { dirname as dirname3, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
943
893
|
import { parseHTML } from "linkedom";
|
|
944
894
|
var CompositionInsertionError = class extends Error {
|
|
945
895
|
constructor(message, status) {
|
|
@@ -975,8 +925,8 @@ function canonicalProjectPath(projectDir, candidate) {
|
|
|
975
925
|
if (!existsSync2(candidate)) {
|
|
976
926
|
throw new CompositionInsertionError("Composition source was not found", 404);
|
|
977
927
|
}
|
|
978
|
-
const canonical =
|
|
979
|
-
if (!isSafePath(
|
|
928
|
+
const canonical = realpath(candidate);
|
|
929
|
+
if (!isSafePath(realpath(projectDir), canonical)) {
|
|
980
930
|
throw new CompositionInsertionError("Composition source escapes the project", 400);
|
|
981
931
|
}
|
|
982
932
|
return canonical;
|
|
@@ -988,13 +938,13 @@ function validateSourcePath(sourcePath) {
|
|
|
988
938
|
}
|
|
989
939
|
function canonicalProjectFile(projectDir, sourcePath) {
|
|
990
940
|
validateSourcePath(sourcePath);
|
|
991
|
-
return canonicalProjectPath(projectDir,
|
|
941
|
+
return canonicalProjectPath(projectDir, resolveWithinProject(projectDir, sourcePath));
|
|
992
942
|
}
|
|
993
943
|
function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
994
944
|
validateSourcePath(sourcePath);
|
|
995
945
|
return canonicalProjectPath(
|
|
996
946
|
projectDir,
|
|
997
|
-
|
|
947
|
+
resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname3(ownerAbs), sourcePath)))
|
|
998
948
|
);
|
|
999
949
|
}
|
|
1000
950
|
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
@@ -1067,7 +1017,7 @@ function uniqueHostId(root, base) {
|
|
|
1067
1017
|
return `${base}_${suffix}`;
|
|
1068
1018
|
}
|
|
1069
1019
|
function relativeSourcePath(targetAbs, sourceAbs) {
|
|
1070
|
-
return
|
|
1020
|
+
return relative3(dirname3(targetAbs), sourceAbs).split(sep2).join("/");
|
|
1071
1021
|
}
|
|
1072
1022
|
function insertCompositionIntoSource(input) {
|
|
1073
1023
|
const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
|
|
@@ -1199,7 +1149,7 @@ async function loadGsapParser() {
|
|
|
1199
1149
|
return import("@hyperframes/parsers/gsap-parser-recast");
|
|
1200
1150
|
}
|
|
1201
1151
|
function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
1202
|
-
if (!isSafePath(projectDir,
|
|
1152
|
+
if (!isSafePath(projectDir, dirname4(lexicalPath))) return false;
|
|
1203
1153
|
let stats;
|
|
1204
1154
|
try {
|
|
1205
1155
|
stats = lstatSync3(lexicalPath);
|
|
@@ -1207,7 +1157,7 @@ function isDanglingSymlinkInProject(projectDir, lexicalPath) {
|
|
|
1207
1157
|
return false;
|
|
1208
1158
|
}
|
|
1209
1159
|
if (!stats.isSymbolicLink()) return false;
|
|
1210
|
-
const target = resolve3(
|
|
1160
|
+
const target = resolve3(dirname4(lexicalPath), readlinkSync(lexicalPath));
|
|
1211
1161
|
if (!isSafePath(projectDir, target)) return false;
|
|
1212
1162
|
try {
|
|
1213
1163
|
statSync3(lexicalPath);
|
|
@@ -1231,7 +1181,7 @@ async function resolveProjectPath(c, adapter, route, opts) {
|
|
|
1231
1181
|
if (filePath.includes("\0")) {
|
|
1232
1182
|
return { error: c.json({ error: "forbidden", why: "nul" }, 403) };
|
|
1233
1183
|
}
|
|
1234
|
-
const absPath = (opts?.pin ? pinWithinProject :
|
|
1184
|
+
const absPath = (opts?.pin ? pinWithinProject : resolveWithinProject)(project.dir, filePath);
|
|
1235
1185
|
if (!absPath) {
|
|
1236
1186
|
if (isDanglingSymlinkInProject(project.dir, resolve3(project.dir, filePath))) {
|
|
1237
1187
|
return { error: c.json({ error: "not found", why: "dangling_symlink" }, 404) };
|
|
@@ -1430,7 +1380,7 @@ async function parseMutationBody(c) {
|
|
|
1430
1380
|
return { target: body.target, body };
|
|
1431
1381
|
}
|
|
1432
1382
|
function ensureDir(filePath) {
|
|
1433
|
-
const dir =
|
|
1383
|
+
const dir = dirname4(filePath);
|
|
1434
1384
|
if (!existsSync3(dir)) mkdirSync4(dir, { recursive: true });
|
|
1435
1385
|
}
|
|
1436
1386
|
function generateCopyPath(projectDir, originalPath) {
|
|
@@ -1448,8 +1398,8 @@ function generateCopyPath(projectDir, originalPath) {
|
|
|
1448
1398
|
}
|
|
1449
1399
|
function walkFiles(dir, filter) {
|
|
1450
1400
|
const results = [];
|
|
1451
|
-
for (const entry of
|
|
1452
|
-
const full =
|
|
1401
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
1402
|
+
const full = join6(dir, entry.name);
|
|
1453
1403
|
if (entry.isDirectory()) {
|
|
1454
1404
|
if (entry.name === "node_modules" || entry.name === ".thumbnails" || entry.name === "renders" || entry.name === ".transcode-cache")
|
|
1455
1405
|
continue;
|
|
@@ -2612,7 +2562,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2612
2562
|
if (n >= MAX_COPY_INDEX) skipped.push(name);
|
|
2613
2563
|
continue;
|
|
2614
2564
|
}
|
|
2615
|
-
const relativePath = subDir ?
|
|
2565
|
+
const relativePath = subDir ? join6(subDir, finalName) : finalName;
|
|
2616
2566
|
uploaded.push(relativePath);
|
|
2617
2567
|
if (isAudioFile(finalName)) {
|
|
2618
2568
|
generateWaveformCache(projectDir, relativePath).catch(() => {
|
|
@@ -3295,7 +3245,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3295
3245
|
if (!body.newPath || body.newPath.includes("\0")) {
|
|
3296
3246
|
return c.json({ error: "newPath required" }, 400);
|
|
3297
3247
|
}
|
|
3298
|
-
const newAbs =
|
|
3248
|
+
const newAbs = resolveWithinProject(res.project.dir, body.newPath);
|
|
3299
3249
|
if (!newAbs) {
|
|
3300
3250
|
return c.json({ error: "forbidden" }, 403);
|
|
3301
3251
|
}
|
|
@@ -3314,12 +3264,12 @@ function registerFileRoutes(api, adapter) {
|
|
|
3314
3264
|
if (!body.path || body.path.includes("\0")) {
|
|
3315
3265
|
return c.json({ error: "path required" }, 400);
|
|
3316
3266
|
}
|
|
3317
|
-
const srcAbs =
|
|
3267
|
+
const srcAbs = resolveWithinProject(project.dir, body.path);
|
|
3318
3268
|
if (!srcAbs || !existsSync3(srcAbs)) {
|
|
3319
3269
|
return c.json({ error: "not found" }, 404);
|
|
3320
3270
|
}
|
|
3321
3271
|
const copyPath = generateCopyPath(project.dir, body.path);
|
|
3322
|
-
const destAbs =
|
|
3272
|
+
const destAbs = resolveWithinProject(project.dir, copyPath);
|
|
3323
3273
|
if (!destAbs) {
|
|
3324
3274
|
return c.json({ error: "forbidden" }, 403);
|
|
3325
3275
|
}
|
|
@@ -3345,7 +3295,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3345
3295
|
const project = await adapter.resolveProject(c.req.param("id"));
|
|
3346
3296
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
3347
3297
|
const subDir = c.req.query("dir") ?? "";
|
|
3348
|
-
const targetDir = subDir ?
|
|
3298
|
+
const targetDir = subDir ? resolveWithinProject(project.dir, subDir) : project.dir;
|
|
3349
3299
|
if (!targetDir) return c.json({ error: "forbidden" }, 403);
|
|
3350
3300
|
if (subDir && !existsSync3(targetDir)) mkdirSync4(targetDir, { recursive: true });
|
|
3351
3301
|
const formData = await c.req.formData();
|
|
@@ -3429,7 +3379,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
3429
3379
|
// src/routes/preview.ts
|
|
3430
3380
|
import { createReadStream, existsSync as existsSync5, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
3431
3381
|
import { Readable } from "stream";
|
|
3432
|
-
import { join as
|
|
3382
|
+
import { join as join9, resolve as resolve5 } from "path";
|
|
3433
3383
|
import { createHash as createHash4 } from "crypto";
|
|
3434
3384
|
import {
|
|
3435
3385
|
addScenePartsManifest,
|
|
@@ -3441,7 +3391,7 @@ import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
|
3441
3391
|
|
|
3442
3392
|
// src/helpers/subComposition.ts
|
|
3443
3393
|
import { existsSync as existsSync4, readFileSync as readFileSync7 } from "fs";
|
|
3444
|
-
import { join as
|
|
3394
|
+
import { join as join7 } from "path";
|
|
3445
3395
|
import { parseHTML as parseHTML3 } from "linkedom";
|
|
3446
3396
|
import {
|
|
3447
3397
|
rewriteAssetPaths,
|
|
@@ -3451,7 +3401,7 @@ import {
|
|
|
3451
3401
|
import { stripEmbeddedRuntimeScripts } from "@hyperframes/core/compiler";
|
|
3452
3402
|
import { isFullHtmlDocument } from "@hyperframes/core/compiler/html-document";
|
|
3453
3403
|
function rewriteRelativePaths(root, compPath, projectDir) {
|
|
3454
|
-
const assetExists = (path) => existsSync4(
|
|
3404
|
+
const assetExists = (path) => existsSync4(join7(projectDir, path));
|
|
3455
3405
|
rewriteAssetPaths(
|
|
3456
3406
|
root.querySelectorAll("[src], [href]"),
|
|
3457
3407
|
compPath,
|
|
@@ -3548,7 +3498,7 @@ function tagRootCompositionFile(bodyHtml, compPath) {
|
|
|
3548
3498
|
return bodyHtml.slice(0, tagEnd) + ` data-composition-file="${compPath}"` + bodyHtml.slice(tagEnd);
|
|
3549
3499
|
}
|
|
3550
3500
|
function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, rawOverride) {
|
|
3551
|
-
const compFile =
|
|
3501
|
+
const compFile = join7(projectDir, compPath);
|
|
3552
3502
|
if (!existsSync4(compFile)) return null;
|
|
3553
3503
|
const rawComp = rawOverride ?? readFileSync7(compFile, "utf-8");
|
|
3554
3504
|
let compHeadContent = "";
|
|
@@ -3580,7 +3530,7 @@ function buildSubCompositionHtml(projectDir, compPath, runtimeUrl, baseHref, raw
|
|
|
3580
3530
|
}
|
|
3581
3531
|
rewrittenContent = stripEmbeddedRuntimeScripts(rewrittenContent);
|
|
3582
3532
|
rewrittenContent = tagRootCompositionFile(rewrittenContent, compPath);
|
|
3583
|
-
const indexPath =
|
|
3533
|
+
const indexPath = join7(projectDir, "index.html");
|
|
3584
3534
|
let headContent = "";
|
|
3585
3535
|
if (existsSync4(indexPath)) {
|
|
3586
3536
|
const indexHtml = readFileSync7(indexPath, "utf-8");
|
|
@@ -3624,14 +3574,14 @@ import { ensureHfIds as ensureHfIds3 } from "@hyperframes/parsers/hf-ids";
|
|
|
3624
3574
|
|
|
3625
3575
|
// src/helpers/previewReads.ts
|
|
3626
3576
|
import { readlinkSync as readlinkSync2 } from "fs";
|
|
3627
|
-
import { dirname as
|
|
3577
|
+
import { dirname as dirname5, join as join8, parse, relative as relative4, resolve as resolve4 } from "path";
|
|
3628
3578
|
var ALWAYS_AFFECTS = ["hyperframes.json", ...STUDIO_SIGNATURE_MANIFEST_PATHS];
|
|
3629
3579
|
var REFERENCE = /\b(?:src|href|poster|data-composition-src)\s*=\s*(?:"([^"\n]*)"|'([^'\n]*)')|url\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|([^"'\s)]+))/gi;
|
|
3630
3580
|
var pathKey = process.platform === "darwin" || process.platform === "win32" ? (path) => resolve4(path).toLowerCase() : (path) => resolve4(path);
|
|
3631
3581
|
var readsByProject = /* @__PURE__ */ new Map();
|
|
3632
3582
|
var builtProjects = /* @__PURE__ */ new Set();
|
|
3633
3583
|
var projectKey = (projectDir) => pathKey(realFilePath(resolve4(projectDir)));
|
|
3634
|
-
var inRealProject = (projectDir, path) =>
|
|
3584
|
+
var inRealProject = (projectDir, path) => join8(realFilePath(resolve4(projectDir)), relative4(resolve4(projectDir), resolve4(projectDir, path)));
|
|
3635
3585
|
var readLink = (path) => {
|
|
3636
3586
|
try {
|
|
3637
3587
|
return readlinkSync2(path);
|
|
@@ -3647,10 +3597,10 @@ function linksOnTheWay(path) {
|
|
|
3647
3597
|
const part = rest.shift();
|
|
3648
3598
|
if (!part || part === ".") continue;
|
|
3649
3599
|
if (part === "..") {
|
|
3650
|
-
at =
|
|
3600
|
+
at = dirname5(at);
|
|
3651
3601
|
continue;
|
|
3652
3602
|
}
|
|
3653
|
-
const next =
|
|
3603
|
+
const next = join8(at, part);
|
|
3654
3604
|
const target = hops < 40 ? readLink(next) : null;
|
|
3655
3605
|
if (target === null) {
|
|
3656
3606
|
at = next;
|
|
@@ -3671,7 +3621,7 @@ function recordPreviewRead(projectDir, filePath) {
|
|
|
3671
3621
|
if (!reads) readsByProject.set(key, reads = /* @__PURE__ */ new Set());
|
|
3672
3622
|
const read = inRealProject(projectDir, filePath);
|
|
3673
3623
|
for (const found of [read, ...linksOnTheWay(read)]) {
|
|
3674
|
-
for (let path = pathKey(found); !reads.has(path); path =
|
|
3624
|
+
for (let path = pathKey(found); !reads.has(path); path = dirname5(path)) reads.add(path);
|
|
3675
3625
|
}
|
|
3676
3626
|
}
|
|
3677
3627
|
function recordPreviewReferences(projectDir, html) {
|
|
@@ -3740,7 +3690,7 @@ function injectProjectSignature(html, signature) {
|
|
|
3740
3690
|
${html}`;
|
|
3741
3691
|
}
|
|
3742
3692
|
function readStudioMotionManifestContent(projectDir) {
|
|
3743
|
-
const manifestPath =
|
|
3693
|
+
const manifestPath = join9(projectDir, STUDIO_MOTION_PATH);
|
|
3744
3694
|
if (!existsSync5(manifestPath)) return "";
|
|
3745
3695
|
try {
|
|
3746
3696
|
return readFileSync8(manifestPath, "utf-8");
|
|
@@ -3885,8 +3835,8 @@ async function transformPreviewHtml(html, adapter, project, activeCompositionPat
|
|
|
3885
3835
|
}
|
|
3886
3836
|
}
|
|
3887
3837
|
function resolveProjectMainHtml(projectDir, projectId) {
|
|
3888
|
-
const indexPath =
|
|
3889
|
-
const blockHtmlPath =
|
|
3838
|
+
const indexPath = join9(projectDir, "index.html");
|
|
3839
|
+
const blockHtmlPath = join9(projectDir, `${projectId}.html`);
|
|
3890
3840
|
recordPreviewRead(projectDir, indexPath);
|
|
3891
3841
|
recordPreviewRead(projectDir, blockHtmlPath);
|
|
3892
3842
|
if (existsSync5(indexPath)) {
|
|
@@ -4040,7 +3990,7 @@ ${etag}`;
|
|
|
4040
3990
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
4041
3991
|
const previewVariables = vars.values;
|
|
4042
3992
|
const compPath = requestSubPath(c.req.url, "projects/:id/preview/comp");
|
|
4043
|
-
const compFile =
|
|
3993
|
+
const compFile = resolveWithinProject(project.dir, compPath);
|
|
4044
3994
|
if (compFile) recordPreviewRead(project.dir, compFile);
|
|
4045
3995
|
recordPreviewRead(project.dir, "index.html");
|
|
4046
3996
|
if (!compFile || !existsSync5(compFile) || !statSync4(compFile).isFile()) {
|
|
@@ -4175,7 +4125,7 @@ ${etag}`;
|
|
|
4175
4125
|
|
|
4176
4126
|
// src/routes/lint.ts
|
|
4177
4127
|
import { readFileSync as readFileSync9 } from "fs";
|
|
4178
|
-
import { join as
|
|
4128
|
+
import { join as join10 } from "path";
|
|
4179
4129
|
async function collectProjectFindings(adapter, projectDir) {
|
|
4180
4130
|
const findings = [];
|
|
4181
4131
|
const coveredFiles = /* @__PURE__ */ new Set();
|
|
@@ -4193,7 +4143,7 @@ async function lintUncoveredHtml(adapter, projectDir, htmlFiles, coveredFiles) {
|
|
|
4193
4143
|
const findings = [];
|
|
4194
4144
|
for (const file of htmlFiles) {
|
|
4195
4145
|
if (coveredFiles.has(file)) continue;
|
|
4196
|
-
const content = readFileSync9(
|
|
4146
|
+
const content = readFileSync9(join10(projectDir, file), "utf-8");
|
|
4197
4147
|
const result = await adapter.lint(content, { filePath: file, isSubComposition: true });
|
|
4198
4148
|
for (const finding of result?.findings ?? []) {
|
|
4199
4149
|
findings.push({ ...finding, file });
|
|
@@ -4226,8 +4176,8 @@ function registerLintRoutes(api, adapter) {
|
|
|
4226
4176
|
|
|
4227
4177
|
// src/routes/render.ts
|
|
4228
4178
|
import { streamSSE } from "hono/streaming";
|
|
4229
|
-
import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as
|
|
4230
|
-
import { join as
|
|
4179
|
+
import { existsSync as existsSync6, readFileSync as readFileSync10, mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
4180
|
+
import { join as join11 } from "path";
|
|
4231
4181
|
import { VALID_CANVAS_RESOLUTIONS } from "@hyperframes/parsers";
|
|
4232
4182
|
import { formatRenderOutputTimestamp, parseFps } from "@hyperframes/core";
|
|
4233
4183
|
var VALID_RESOLUTIONS = new Set(VALID_CANVAS_RESOLUTIONS);
|
|
@@ -4278,7 +4228,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4278
4228
|
const outputResolution = VALID_RESOLUTIONS.has(body.resolution ?? "") ? body.resolution : void 0;
|
|
4279
4229
|
let composition;
|
|
4280
4230
|
if (typeof body.composition === "string" && body.composition.length > 0) {
|
|
4281
|
-
if (!
|
|
4231
|
+
if (!resolveWithinProject(project.dir, body.composition)) {
|
|
4282
4232
|
return c.json({ error: "composition path must be within the project directory" }, 400);
|
|
4283
4233
|
}
|
|
4284
4234
|
composition = body.composition;
|
|
@@ -4295,7 +4245,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4295
4245
|
const rendersDir = adapter.rendersDir(project);
|
|
4296
4246
|
if (!existsSync6(rendersDir)) mkdirSync5(rendersDir, { recursive: true });
|
|
4297
4247
|
const ext = FORMAT_EXT[format] ?? ".mp4";
|
|
4298
|
-
const outputPath =
|
|
4248
|
+
const outputPath = join11(rendersDir, `${jobId}${ext}`);
|
|
4299
4249
|
const jobState = adapter.startRender({
|
|
4300
4250
|
project,
|
|
4301
4251
|
outputPath,
|
|
@@ -4396,7 +4346,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4396
4346
|
if (state.id === jobId && state.outputPath) {
|
|
4397
4347
|
const dir = state.outputPath.replace(/\/[^/]+$/, "");
|
|
4398
4348
|
for (const ext of [".mp4", ".webm", ".mov", ".meta.json"]) {
|
|
4399
|
-
const fp =
|
|
4349
|
+
const fp = join11(dir, `${jobId}${ext}`);
|
|
4400
4350
|
if (existsSync6(fp)) unlinkSync3(fp);
|
|
4401
4351
|
}
|
|
4402
4352
|
break;
|
|
@@ -4411,7 +4361,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4411
4361
|
const filename = c.req.path.split("/renders/file/")[1];
|
|
4412
4362
|
if (!filename) return c.json({ error: "missing filename" }, 400);
|
|
4413
4363
|
const rendersDir = adapter.rendersDir(project);
|
|
4414
|
-
const fp =
|
|
4364
|
+
const fp = resolveWithinProject(rendersDir, filename);
|
|
4415
4365
|
if (!fp) return c.json({ error: "forbidden" }, 403);
|
|
4416
4366
|
if (!existsSync6(fp)) return c.json({ error: "not found" }, 404);
|
|
4417
4367
|
const contentType = renderContentType(fp);
|
|
@@ -4430,11 +4380,11 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4430
4380
|
if (!project) return c.json({ error: "not found" }, 404);
|
|
4431
4381
|
const rendersDir = adapter.rendersDir(project);
|
|
4432
4382
|
if (!existsSync6(rendersDir)) return c.json({ renders: [] });
|
|
4433
|
-
const files =
|
|
4434
|
-
const fp =
|
|
4383
|
+
const files = readdirSync4(rendersDir).filter((f) => f.endsWith(".mp4") || f.endsWith(".webm") || f.endsWith(".mov")).map((f) => {
|
|
4384
|
+
const fp = join11(rendersDir, f);
|
|
4435
4385
|
const stat3 = statSync5(fp);
|
|
4436
4386
|
const rid = f.replace(/\.(mp4|webm|mov)$/, "");
|
|
4437
|
-
const metaPath =
|
|
4387
|
+
const metaPath = join11(rendersDir, `${rid}.meta.json`);
|
|
4438
4388
|
let status = "complete";
|
|
4439
4389
|
let durationMs;
|
|
4440
4390
|
if (existsSync6(metaPath)) {
|
|
@@ -4460,7 +4410,7 @@ function registerRenderRoutes(api, adapter) {
|
|
|
4460
4410
|
id: file.id,
|
|
4461
4411
|
status: file.status,
|
|
4462
4412
|
progress: 100,
|
|
4463
|
-
outputPath:
|
|
4413
|
+
outputPath: join11(rendersDir, file.filename),
|
|
4464
4414
|
createdAt: file.createdAt
|
|
4465
4415
|
});
|
|
4466
4416
|
}
|
|
@@ -4608,14 +4558,14 @@ async function generateThumbnail(path, signal) {
|
|
|
4608
4558
|
async function resolveSource(adapter, id, url) {
|
|
4609
4559
|
const project = await adapter.resolveProject(id);
|
|
4610
4560
|
if (!project) throw new HTTPException(404);
|
|
4611
|
-
let
|
|
4561
|
+
let relative6;
|
|
4612
4562
|
try {
|
|
4613
|
-
|
|
4563
|
+
relative6 = requestSubPath(url, "projects/:id/image-thumbnail");
|
|
4614
4564
|
} catch {
|
|
4615
4565
|
throw new HTTPException(400);
|
|
4616
4566
|
}
|
|
4617
|
-
if (!/\.jpe?g$/i.test(
|
|
4618
|
-
const path = pinWithinProject(project.dir,
|
|
4567
|
+
if (!/\.jpe?g$/i.test(relative6)) throw new HTTPException(415);
|
|
4568
|
+
const path = pinWithinProject(project.dir, relative6);
|
|
4619
4569
|
if (!path) throw new HTTPException(404);
|
|
4620
4570
|
const info = await stat(path).catch(() => {
|
|
4621
4571
|
throw new HTTPException(404);
|
|
@@ -4676,14 +4626,14 @@ import {
|
|
|
4676
4626
|
openSync as openSync3,
|
|
4677
4627
|
mkdirSync as mkdirSync6,
|
|
4678
4628
|
readFileSync as readFileSync12,
|
|
4679
|
-
readdirSync as
|
|
4629
|
+
readdirSync as readdirSync5,
|
|
4680
4630
|
renameSync as renameSync3,
|
|
4681
4631
|
rmSync as rmSync4,
|
|
4682
4632
|
statSync as statSync6,
|
|
4683
4633
|
unlinkSync as unlinkSync4,
|
|
4684
4634
|
writeFileSync as writeFileSync6
|
|
4685
4635
|
} from "fs";
|
|
4686
|
-
import { join as
|
|
4636
|
+
import { join as join12 } from "path";
|
|
4687
4637
|
import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
|
|
4688
4638
|
|
|
4689
4639
|
// src/helpers/compositionInputs.ts
|
|
@@ -4694,7 +4644,7 @@ function projectReader(projectDir) {
|
|
|
4694
4644
|
const sources = /* @__PURE__ */ new Map();
|
|
4695
4645
|
return (compPath) => {
|
|
4696
4646
|
if (!sources.has(compPath)) {
|
|
4697
|
-
const file =
|
|
4647
|
+
const file = resolveWithinProject(projectDir, compPath);
|
|
4698
4648
|
let source = null;
|
|
4699
4649
|
try {
|
|
4700
4650
|
source = file ? readFileSync11(file, "utf-8") : null;
|
|
@@ -4753,14 +4703,14 @@ var prunedCacheDirs = /* @__PURE__ */ new Set();
|
|
|
4753
4703
|
function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
|
|
4754
4704
|
let entries;
|
|
4755
4705
|
try {
|
|
4756
|
-
entries =
|
|
4706
|
+
entries = readdirSync5(cacheDir, { withFileTypes: true });
|
|
4757
4707
|
} catch (err) {
|
|
4758
4708
|
if (err.code === "ENOENT") return;
|
|
4759
4709
|
throw err;
|
|
4760
4710
|
}
|
|
4761
4711
|
const files = entries.flatMap((entry) => {
|
|
4762
4712
|
if (!entry.isFile()) return [];
|
|
4763
|
-
const path =
|
|
4713
|
+
const path = join12(cacheDir, entry.name);
|
|
4764
4714
|
try {
|
|
4765
4715
|
const stats = statSync6(path);
|
|
4766
4716
|
return [{ path, bytes: stats.size, mtimeMs: stats.mtimeMs }];
|
|
@@ -4830,7 +4780,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4830
4780
|
const { project, signature: projectSignature } = resolved;
|
|
4831
4781
|
let compPath = requestSubPath(c.req.url, "projects/:id/thumbnail");
|
|
4832
4782
|
if (compPath && !compPath.includes(".")) compPath += ".html";
|
|
4833
|
-
const htmlFile =
|
|
4783
|
+
const htmlFile = resolveWithinProject(project.dir, compPath);
|
|
4834
4784
|
const source = htmlFile ? readFileOnce(htmlFile) : "not-a-file";
|
|
4835
4785
|
if (source === "not-a-file") return c.json({ error: "not found" }, 404);
|
|
4836
4786
|
const inputSignature = compositionInputSignature(project.dir, compPath, projectSignature);
|
|
@@ -4863,12 +4813,12 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4863
4813
|
if (hMatch?.[1]) compH = parseInt(hMatch[1]);
|
|
4864
4814
|
}
|
|
4865
4815
|
}
|
|
4866
|
-
const manualEdits = manifestKey(
|
|
4867
|
-
const motion = manifestKey(
|
|
4816
|
+
const manualEdits = manifestKey(join12(project.dir, STUDIO_MANUAL_EDITS_PATH));
|
|
4817
|
+
const motion = manifestKey(join12(project.dir, STUDIO_MOTION_PATH));
|
|
4868
4818
|
sourceMtime = Math.max(sourceMtime, manualEdits.mtimeMs, motion.mtimeMs);
|
|
4869
4819
|
const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
|
|
4870
4820
|
const previewUrl = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
4871
|
-
const cacheDir =
|
|
4821
|
+
const cacheDir = join12(project.dir, ".thumbnails");
|
|
4872
4822
|
const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
|
|
4873
4823
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
4874
4824
|
const inputSignatureKey = `_${createHash6("sha1").update(inputSignature).digest("hex").slice(0, 16)}`;
|
|
@@ -4876,7 +4826,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4876
4826
|
const outputWidth = Math.max(1, Math.round(compW * outputScale));
|
|
4877
4827
|
const outputHeight = Math.max(1, Math.round(compH * outputScale));
|
|
4878
4828
|
const cacheKey = `${THUMBNAIL_CACHE_VERSION}${urlVersionKey}${inputSignatureKey}${manualEdits.key}${motion.key}${sourceKey}_${format}_${outputMode}_${compPath.replace(/\//g, "_")}_${compW}x${compH}_${outputWidth}x${outputHeight}_${sourceMtime}_${seekTime.toFixed(2)}${selectorKey}.${format === "png" ? "png" : "jpg"}`;
|
|
4879
|
-
const cachePath =
|
|
4829
|
+
const cachePath = join12(cacheDir, cacheKey);
|
|
4880
4830
|
if (!prunedCacheDirs.has(cacheDir)) {
|
|
4881
4831
|
prunedCacheDirs.add(cacheDir);
|
|
4882
4832
|
pruneThumbnailCache(
|
|
@@ -4943,7 +4893,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4943
4893
|
|
|
4944
4894
|
// src/routes/waveform.ts
|
|
4945
4895
|
import { existsSync as existsSync7, readFileSync as readFileSync13, statSync as statSync7 } from "fs";
|
|
4946
|
-
import { join as
|
|
4896
|
+
import { join as join13, resolve as resolve6 } from "path";
|
|
4947
4897
|
import { isWithinProjectRoot as isWithinProjectRoot2 } from "@hyperframes/parsers/asset-resolution";
|
|
4948
4898
|
function registerWaveformRoutes(api, adapter) {
|
|
4949
4899
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
@@ -4956,8 +4906,8 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4956
4906
|
}
|
|
4957
4907
|
const stats = statSync7(audioPath, { throwIfNoEntry: false });
|
|
4958
4908
|
if (!stats) return c.json({ error: "file not found" }, 404);
|
|
4959
|
-
const cacheDir =
|
|
4960
|
-
const cachePath =
|
|
4909
|
+
const cacheDir = join13(project.dir, ".waveform-cache");
|
|
4910
|
+
const cachePath = join13(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
4961
4911
|
try {
|
|
4962
4912
|
if (isWaveformCacheDirectory(cacheDir) && existsSync7(cachePath)) {
|
|
4963
4913
|
const peaks2 = JSON.parse(readFileSync13(cachePath, "utf-8"));
|
|
@@ -5244,7 +5194,7 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
5244
5194
|
// src/routes/media.ts
|
|
5245
5195
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
5246
5196
|
import { existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
5247
|
-
import { basename as
|
|
5197
|
+
import { basename as basename2, dirname as dirname6, extname as extname2, join as join14 } from "path";
|
|
5248
5198
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5249
5199
|
".mp4",
|
|
5250
5200
|
".mov",
|
|
@@ -5274,14 +5224,14 @@ function containsNullByte(path) {
|
|
|
5274
5224
|
return path.includes("\0");
|
|
5275
5225
|
}
|
|
5276
5226
|
function slugFileBase(path) {
|
|
5277
|
-
const name =
|
|
5227
|
+
const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5278
5228
|
return name || "media";
|
|
5279
5229
|
}
|
|
5280
5230
|
function uniqueAssetPath(projectDir, assetPath) {
|
|
5281
5231
|
const ext = extname2(assetPath);
|
|
5282
5232
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
5283
5233
|
let candidate = assetPath;
|
|
5284
|
-
for (let index = 2; existsSync8(
|
|
5234
|
+
for (let index = 2; existsSync8(join14(projectDir, candidate)); index++) {
|
|
5285
5235
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
5286
5236
|
}
|
|
5287
5237
|
return candidate;
|
|
@@ -5330,7 +5280,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5330
5280
|
if (/^(?:https?:|data:|blob:)/i.test(assetPath)) {
|
|
5331
5281
|
return c.json({ error: "media metadata requires a project-local asset" }, 400);
|
|
5332
5282
|
}
|
|
5333
|
-
const filePath =
|
|
5283
|
+
const filePath = resolveWithinProject(project.dir, assetPath);
|
|
5334
5284
|
if (!filePath) return c.json({ error: "forbidden" }, 403);
|
|
5335
5285
|
if (!existsSync8(filePath)) return c.json({ error: "media not found" }, 404);
|
|
5336
5286
|
return c.json({ path: assetPath, metadata: await readMediaMetadata(filePath) });
|
|
@@ -5352,7 +5302,7 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5352
5302
|
if (/^(?:https?:|data:|blob:)/i.test(inputAssetPath)) {
|
|
5353
5303
|
return c.json({ error: "background removal requires a project-local media asset" }, 400);
|
|
5354
5304
|
}
|
|
5355
|
-
const inputPath =
|
|
5305
|
+
const inputPath = resolveWithinProject(project.dir, inputAssetPath);
|
|
5356
5306
|
if (!inputPath) return c.json({ error: "forbidden" }, 403);
|
|
5357
5307
|
if (!existsSync8(inputPath)) return c.json({ error: "input media not found" }, 404);
|
|
5358
5308
|
const inputIsVideo = isVideoPath(inputAssetPath);
|
|
@@ -5364,11 +5314,11 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5364
5314
|
if (requestedOutput && containsNullByte(requestedOutput)) {
|
|
5365
5315
|
return c.json({ error: "forbidden" }, 403);
|
|
5366
5316
|
}
|
|
5367
|
-
if (requestedOutput && !
|
|
5317
|
+
if (requestedOutput && !resolveWithinProject(project.dir, requestedOutput)) {
|
|
5368
5318
|
return c.json({ error: "forbidden" }, 403);
|
|
5369
5319
|
}
|
|
5370
5320
|
const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput) : defaultOutputPath(project.dir, inputAssetPath);
|
|
5371
|
-
const outputPath =
|
|
5321
|
+
const outputPath = resolveWithinProject(project.dir, outputAssetPath);
|
|
5372
5322
|
if (!outputPath) return c.json({ error: "forbidden" }, 403);
|
|
5373
5323
|
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
|
|
5374
5324
|
return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
|
|
@@ -5383,13 +5333,13 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5383
5333
|
return c.json({ error: "background plates are only supported for video inputs" }, 400);
|
|
5384
5334
|
}
|
|
5385
5335
|
backgroundOutputAssetPath = defaultPlatePath(project.dir, inputAssetPath);
|
|
5386
|
-
backgroundOutputPath =
|
|
5336
|
+
backgroundOutputPath = resolveWithinProject(project.dir, backgroundOutputAssetPath) ?? void 0;
|
|
5387
5337
|
if (!backgroundOutputPath) {
|
|
5388
5338
|
return c.json({ error: "forbidden" }, 403);
|
|
5389
5339
|
}
|
|
5390
5340
|
}
|
|
5391
|
-
mkdirSync7(
|
|
5392
|
-
if (backgroundOutputPath) mkdirSync7(
|
|
5341
|
+
mkdirSync7(dirname6(outputPath), { recursive: true });
|
|
5342
|
+
if (backgroundOutputPath) mkdirSync7(dirname6(backgroundOutputPath), { recursive: true });
|
|
5393
5343
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
5394
5344
|
const state = adapter.startBackgroundRemoval({
|
|
5395
5345
|
project,
|
|
@@ -5448,9 +5398,9 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5448
5398
|
// src/routes/globalAssets.ts
|
|
5449
5399
|
import { existsSync as existsSync9, readFileSync as readFileSync14 } from "fs";
|
|
5450
5400
|
import { homedir } from "os";
|
|
5451
|
-
import { join as
|
|
5401
|
+
import { join as join15 } from "path";
|
|
5452
5402
|
function readGlobalAssets(home = homedir()) {
|
|
5453
|
-
const manifestPath =
|
|
5403
|
+
const manifestPath = join15(home, ".media", "manifest.jsonl");
|
|
5454
5404
|
if (!existsSync9(manifestPath)) return [];
|
|
5455
5405
|
const out = [];
|
|
5456
5406
|
for (const line of readFileSync14(manifestPath, "utf8").split("\n")) {
|
|
@@ -5478,19 +5428,19 @@ import {
|
|
|
5478
5428
|
lstatSync as lstatSync4,
|
|
5479
5429
|
statSync as statSync9,
|
|
5480
5430
|
mkdirSync as mkdirSync11,
|
|
5481
|
-
readdirSync as
|
|
5431
|
+
readdirSync as readdirSync6,
|
|
5482
5432
|
readFileSync as readFileSync18,
|
|
5483
|
-
realpathSync
|
|
5433
|
+
realpathSync,
|
|
5484
5434
|
rmSync as rmSync6
|
|
5485
5435
|
} from "fs";
|
|
5486
5436
|
import { homedir as homedir2 } from "os";
|
|
5487
|
-
import { dirname as
|
|
5437
|
+
import { dirname as dirname9, isAbsolute as isAbsolute2, join as join19, relative as relative5, resolve as resolve8, sep as sep3 } from "path";
|
|
5488
5438
|
|
|
5489
5439
|
// src/history/blobStore.ts
|
|
5490
5440
|
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
5491
5441
|
import { constants as constants3, createReadStream as createReadStream2, renameSync as renameSync4 } from "fs";
|
|
5492
5442
|
import { copyFile, mkdir, readdir, readFile as readFile2, rename, rm, stat as stat2 } from "fs/promises";
|
|
5493
|
-
import { dirname as
|
|
5443
|
+
import { dirname as dirname7, join as join16 } from "path";
|
|
5494
5444
|
var BLOB_HASH = /^[0-9a-f]{64}$/;
|
|
5495
5445
|
async function hashFile(path) {
|
|
5496
5446
|
const hash = createHash7("sha256");
|
|
@@ -5498,7 +5448,7 @@ async function hashFile(path) {
|
|
|
5498
5448
|
return hash.digest("hex");
|
|
5499
5449
|
}
|
|
5500
5450
|
async function cloneOrCopy(from, to, beforeReplace) {
|
|
5501
|
-
await mkdir(
|
|
5451
|
+
await mkdir(dirname7(to), { recursive: true });
|
|
5502
5452
|
const temp = `${to}.${randomUUID5()}.tmp`;
|
|
5503
5453
|
try {
|
|
5504
5454
|
await copyFile(from, temp, constants3.COPYFILE_FICLONE);
|
|
@@ -5513,16 +5463,16 @@ async function openBlobStore(dir) {
|
|
|
5513
5463
|
const sizes = /* @__PURE__ */ new Map();
|
|
5514
5464
|
await mkdir(dir, { recursive: true });
|
|
5515
5465
|
for (const shard of await readdir(dir))
|
|
5516
|
-
for (const hash of await readdir(
|
|
5517
|
-
sizes.set(hash, (await stat2(
|
|
5466
|
+
for (const hash of await readdir(join16(dir, shard)).catch(() => []))
|
|
5467
|
+
sizes.set(hash, (await stat2(join16(dir, shard, hash))).size);
|
|
5518
5468
|
const pathOf = (hash) => {
|
|
5519
5469
|
if (!BLOB_HASH.test(hash)) throw new Error("That is not a history blob.");
|
|
5520
|
-
return
|
|
5470
|
+
return join16(dir, hash.slice(0, 2), hash);
|
|
5521
5471
|
};
|
|
5522
5472
|
let total = [...sizes.values()].reduce((sum, size) => sum + size, 0);
|
|
5523
5473
|
return {
|
|
5524
5474
|
async put(absPath) {
|
|
5525
|
-
const temp =
|
|
5475
|
+
const temp = join16(dir, `incoming-${randomUUID5()}`);
|
|
5526
5476
|
await mkdir(dir, { recursive: true });
|
|
5527
5477
|
await copyFile(absPath, temp, constants3.COPYFILE_FICLONE);
|
|
5528
5478
|
const hash = await hashFile(temp);
|
|
@@ -5530,7 +5480,7 @@ async function openBlobStore(dir) {
|
|
|
5530
5480
|
await rm(temp, { force: true });
|
|
5531
5481
|
return hash;
|
|
5532
5482
|
}
|
|
5533
|
-
await mkdir(
|
|
5483
|
+
await mkdir(dirname7(pathOf(hash)), { recursive: true });
|
|
5534
5484
|
const size = (await stat2(temp)).size;
|
|
5535
5485
|
await rename(temp, pathOf(hash));
|
|
5536
5486
|
sizes.set(hash, size);
|
|
@@ -5556,12 +5506,12 @@ async function openBlobStore(dir) {
|
|
|
5556
5506
|
// src/history/historyId.ts
|
|
5557
5507
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5558
5508
|
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
5559
|
-
import { join as
|
|
5560
|
-
var ID_PATH =
|
|
5509
|
+
import { basename as basename3, join as join17, resolve as resolve7 } from "path";
|
|
5510
|
+
var ID_PATH = join17(".hyperframes", "history-id");
|
|
5561
5511
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5562
5512
|
function readId(projectDir) {
|
|
5563
5513
|
try {
|
|
5564
|
-
const id = readFileSync15(
|
|
5514
|
+
const id = readFileSync15(join17(projectDir, ID_PATH), "utf-8").trim();
|
|
5565
5515
|
return ID_SHAPE.test(id) ? id : null;
|
|
5566
5516
|
} catch {
|
|
5567
5517
|
return null;
|
|
@@ -5569,36 +5519,62 @@ function readId(projectDir) {
|
|
|
5569
5519
|
}
|
|
5570
5520
|
var sameFolder = (a, b) => a.ino === b.ino && a.birthtimeMs === b.birthtimeMs;
|
|
5571
5521
|
function isRecordedFolder(historyDir, folder) {
|
|
5522
|
+
const was = readRecord(historyDir);
|
|
5523
|
+
if (typeof was?.ino === "number")
|
|
5524
|
+
return sameFolder({ ino: was.ino, birthtimeMs: was.born ?? NaN }, folder);
|
|
5525
|
+
const unreadableSo0878KeptTheId = typeof was?.dir !== "string";
|
|
5526
|
+
if (unreadableSo0878KeptTheId) return true;
|
|
5527
|
+
return !isCopyOf0878Folder(was.dir, basename3(historyDir), folder);
|
|
5528
|
+
}
|
|
5529
|
+
function readRecord(historyDir) {
|
|
5572
5530
|
try {
|
|
5573
|
-
|
|
5574
|
-
|
|
5531
|
+
return JSON.parse(readFileSync15(join17(historyDir, "project.json"), "utf-8"));
|
|
5532
|
+
} catch {
|
|
5533
|
+
return null;
|
|
5534
|
+
}
|
|
5535
|
+
}
|
|
5536
|
+
function isCopyOf0878Folder(recordedDir, id, folder) {
|
|
5537
|
+
let there;
|
|
5538
|
+
try {
|
|
5539
|
+
there = statSync8(recordedDir, { throwIfNoEntry: false });
|
|
5575
5540
|
} catch {
|
|
5576
5541
|
return false;
|
|
5577
5542
|
}
|
|
5543
|
+
return !!there && !sameFolder(there, folder) && readId(recordedDir) === id;
|
|
5578
5544
|
}
|
|
5579
5545
|
function projectHistoryId(projectDir, historyRoot) {
|
|
5580
5546
|
const dir = resolve7(projectDir);
|
|
5581
5547
|
const folder = statSync8(dir);
|
|
5582
5548
|
let id = readId(dir);
|
|
5583
|
-
if (!id
|
|
5549
|
+
if (!id && existsSync10(join17(dir, ID_PATH)))
|
|
5550
|
+
throw new HistoryIdError(
|
|
5551
|
+
`${join17(dir, ID_PATH)} holds no history id this version can read; move it aside to start anew.`
|
|
5552
|
+
);
|
|
5553
|
+
if (!id || existsSync10(join17(historyRoot, id)) && !isRecordedFolder(join17(historyRoot, id), folder)) {
|
|
5584
5554
|
id = randomUUID6();
|
|
5585
|
-
mkdirSync8(
|
|
5586
|
-
writeFileSync7(
|
|
5555
|
+
mkdirSync8(join17(dir, ".hyperframes"), { recursive: true });
|
|
5556
|
+
writeFileSync7(join17(dir, ID_PATH), `${id}
|
|
5587
5557
|
`);
|
|
5588
5558
|
}
|
|
5589
|
-
recordProject(
|
|
5559
|
+
recordProject(join17(historyRoot, id), dir, folder);
|
|
5590
5560
|
return id;
|
|
5591
5561
|
}
|
|
5562
|
+
var HistoryIdError = class extends Error {
|
|
5563
|
+
constructor(message) {
|
|
5564
|
+
super(message);
|
|
5565
|
+
this.name = "HistoryIdError";
|
|
5566
|
+
}
|
|
5567
|
+
};
|
|
5592
5568
|
function recordProject(historyDir, dir, folder) {
|
|
5593
5569
|
const record = { dir, ino: folder.ino, born: folder.birthtimeMs };
|
|
5594
5570
|
mkdirSync8(historyDir, { recursive: true });
|
|
5595
|
-
replaceFileAtomically(
|
|
5571
|
+
replaceFileAtomically(join17(historyDir, "project.json"), JSON.stringify(record), 420);
|
|
5596
5572
|
}
|
|
5597
5573
|
|
|
5598
5574
|
// src/history/ownerLock.ts
|
|
5599
5575
|
import { randomUUID as randomUUID7 } from "crypto";
|
|
5600
5576
|
import { linkSync, mkdirSync as mkdirSync9, readFileSync as readFileSync16, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
5601
|
-
import { join as
|
|
5577
|
+
import { join as join18 } from "path";
|
|
5602
5578
|
var HistoryBusyError = class extends Error {
|
|
5603
5579
|
constructor(pid) {
|
|
5604
5580
|
super(`This project's history is open in another process (pid ${pid}).`);
|
|
@@ -5657,7 +5633,7 @@ function evictDeadOwner(file) {
|
|
|
5657
5633
|
}
|
|
5658
5634
|
}
|
|
5659
5635
|
async function takeHistoryOwnership(home, waitMs) {
|
|
5660
|
-
const file =
|
|
5636
|
+
const file = join18(home, "owner.pid");
|
|
5661
5637
|
const deadline = Date.now() + waitMs;
|
|
5662
5638
|
mkdirSync9(home, { recursive: true });
|
|
5663
5639
|
for (; ; ) {
|
|
@@ -5678,7 +5654,7 @@ async function takeHistoryOwnership(home, waitMs) {
|
|
|
5678
5654
|
|
|
5679
5655
|
// src/history/historyLog.ts
|
|
5680
5656
|
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync17, writeSync as writeSync2 } from "fs";
|
|
5681
|
-
import { dirname as
|
|
5657
|
+
import { dirname as dirname8 } from "path";
|
|
5682
5658
|
var START = "start";
|
|
5683
5659
|
function parseRecord(line) {
|
|
5684
5660
|
try {
|
|
@@ -5730,7 +5706,7 @@ function writeLog(file, log) {
|
|
|
5730
5706
|
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5731
5707
|
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5732
5708
|
];
|
|
5733
|
-
mkdirSync10(
|
|
5709
|
+
mkdirSync10(dirname8(file), { recursive: true });
|
|
5734
5710
|
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5735
5711
|
`).join(""), 420);
|
|
5736
5712
|
}
|
|
@@ -5803,7 +5779,7 @@ function referencedHashes(log, current) {
|
|
|
5803
5779
|
}
|
|
5804
5780
|
|
|
5805
5781
|
// src/history/projectHistory.ts
|
|
5806
|
-
var DEFAULT_HISTORY_ROOT =
|
|
5782
|
+
var DEFAULT_HISTORY_ROOT = join19(homedir2(), ".cache", "hyperframes", "history");
|
|
5807
5783
|
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5808
5784
|
var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
|
|
5809
5785
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
@@ -5829,20 +5805,20 @@ async function removeEmptyFolders(dir) {
|
|
|
5829
5805
|
throw error;
|
|
5830
5806
|
}
|
|
5831
5807
|
for (const entry of entries)
|
|
5832
|
-
if (entry.isDirectory()) await removeEmptyFolders(
|
|
5808
|
+
if (entry.isDirectory()) await removeEmptyFolders(join19(dir, entry.name));
|
|
5833
5809
|
await rmdir(dir);
|
|
5834
5810
|
}
|
|
5835
5811
|
function fileLeftIn(dir, at, deleted) {
|
|
5836
|
-
for (const entry of
|
|
5812
|
+
for (const entry of readdirSync6(dir, { withFileTypes: true })) {
|
|
5837
5813
|
const path = `${at}/${entry.name}`;
|
|
5838
|
-
const left = entry.isDirectory() ? fileLeftIn(
|
|
5814
|
+
const left = entry.isDirectory() ? fileLeftIn(join19(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
|
|
5839
5815
|
if (left) return left;
|
|
5840
5816
|
}
|
|
5841
5817
|
return void 0;
|
|
5842
5818
|
}
|
|
5843
5819
|
function ignoresCase(dir) {
|
|
5844
|
-
const self = statSync9(
|
|
5845
|
-
const other = statSync9(
|
|
5820
|
+
const self = statSync9(join19(dir, ID_PATH), { throwIfNoEntry: false });
|
|
5821
|
+
const other = statSync9(join19(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
|
|
5846
5822
|
return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
|
|
5847
5823
|
}
|
|
5848
5824
|
var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
|
|
@@ -5859,7 +5835,7 @@ var Engine = class {
|
|
|
5859
5835
|
this.projectId = projectId;
|
|
5860
5836
|
this.blobs = blobs;
|
|
5861
5837
|
this.dir = resolve8(options.projectDir);
|
|
5862
|
-
this.home =
|
|
5838
|
+
this.home = join19(options.historyRoot, projectId);
|
|
5863
5839
|
this.foldsCase = ignoresCase(this.dir);
|
|
5864
5840
|
const folder = statSync9(this.dir, { throwIfNoEntry: false });
|
|
5865
5841
|
if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
|
|
@@ -5892,7 +5868,7 @@ var Engine = class {
|
|
|
5892
5868
|
return this.foldsCase ? path.toLowerCase() : path;
|
|
5893
5869
|
}
|
|
5894
5870
|
get logFile() {
|
|
5895
|
-
return
|
|
5871
|
+
return join19(this.home, "log.jsonl");
|
|
5896
5872
|
}
|
|
5897
5873
|
now() {
|
|
5898
5874
|
return (this.options.now ?? Date.now)();
|
|
@@ -5920,7 +5896,7 @@ var Engine = class {
|
|
|
5920
5896
|
persistLog(record) {
|
|
5921
5897
|
if (record) saveRecord(this.logFile, this.log, record);
|
|
5922
5898
|
else writeLog(this.logFile, this.log);
|
|
5923
|
-
if (!existsSync11(
|
|
5899
|
+
if (!existsSync11(join19(this.home, "project.json")))
|
|
5924
5900
|
recordProject(this.home, this.dir, this.folder);
|
|
5925
5901
|
}
|
|
5926
5902
|
replaced() {
|
|
@@ -5970,7 +5946,7 @@ var Engine = class {
|
|
|
5970
5946
|
}
|
|
5971
5947
|
readStatCache() {
|
|
5972
5948
|
try {
|
|
5973
|
-
const files = JSON.parse(readFileSync18(
|
|
5949
|
+
const files = JSON.parse(readFileSync18(join19(this.home, "stat.json"), "utf-8"));
|
|
5974
5950
|
return new Map(Object.entries(files));
|
|
5975
5951
|
} catch {
|
|
5976
5952
|
return /* @__PURE__ */ new Map();
|
|
@@ -5979,7 +5955,7 @@ var Engine = class {
|
|
|
5979
5955
|
saveStatCache() {
|
|
5980
5956
|
const files = JSON.stringify(Object.fromEntries(this.tracked));
|
|
5981
5957
|
mkdirSync11(this.home, { recursive: true });
|
|
5982
|
-
replaceFileAtomically(
|
|
5958
|
+
replaceFileAtomically(join19(this.home, "stat.json"), files, 420);
|
|
5983
5959
|
}
|
|
5984
5960
|
/**
|
|
5985
5961
|
* Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
|
|
@@ -6019,7 +5995,7 @@ var Engine = class {
|
|
|
6019
5995
|
/** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
|
|
6020
5996
|
async storeIfPresent(path) {
|
|
6021
5997
|
try {
|
|
6022
|
-
return await this.blobs.put(
|
|
5998
|
+
return await this.blobs.put(join19(this.dir, path));
|
|
6023
5999
|
} catch (error) {
|
|
6024
6000
|
if (error.code === "ENOENT") return null;
|
|
6025
6001
|
throw error;
|
|
@@ -6082,7 +6058,7 @@ var Engine = class {
|
|
|
6082
6058
|
return group && taken.every((change) => after(change.path, change.before) === change.before) && [...group.changes.values()].every(untouched) ? group : null;
|
|
6083
6059
|
}
|
|
6084
6060
|
logPath(path) {
|
|
6085
|
-
return
|
|
6061
|
+
return relative5(this.dir, resolve8(this.dir, path)).split(sep3).join("/");
|
|
6086
6062
|
}
|
|
6087
6063
|
/** Takes `paths`' uncommitted changes from others, cut at what `who` overwrote. */
|
|
6088
6064
|
async takeClaimed(who, paths, overwrote) {
|
|
@@ -6106,7 +6082,7 @@ var Engine = class {
|
|
|
6106
6082
|
}
|
|
6107
6083
|
hits.forEach(
|
|
6108
6084
|
({ change }, i) => forgetOverwrittenBytes(
|
|
6109
|
-
|
|
6085
|
+
join19(this.dir, change.path),
|
|
6110
6086
|
new Set([...walked[i]].map(hashVersion))
|
|
6111
6087
|
)
|
|
6112
6088
|
);
|
|
@@ -6114,7 +6090,7 @@ var Engine = class {
|
|
|
6114
6090
|
}
|
|
6115
6091
|
/** Walks the server's writes back from `change.after` to the bytes they replaced, stored so an undo restores them. */
|
|
6116
6092
|
async overwrittenBy(change, told, seen) {
|
|
6117
|
-
const absPath =
|
|
6093
|
+
const absPath = join19(this.dir, change.path);
|
|
6118
6094
|
let bytes;
|
|
6119
6095
|
for (let hash = change.after; hash; ) {
|
|
6120
6096
|
if (seen.has(hash)) return void 0;
|
|
@@ -6126,7 +6102,7 @@ var Engine = class {
|
|
|
6126
6102
|
if (hash === change.before || hash === told) return hash;
|
|
6127
6103
|
}
|
|
6128
6104
|
if (bytes === void 0) return void 0;
|
|
6129
|
-
const staged =
|
|
6105
|
+
const staged = join19(this.home, `overwrote-${randomUUID8()}`);
|
|
6130
6106
|
await writeFile(staged, bytes);
|
|
6131
6107
|
try {
|
|
6132
6108
|
return await this.blobs.put(staged);
|
|
@@ -6148,6 +6124,7 @@ var Engine = class {
|
|
|
6148
6124
|
part.changes.set(path, window.changes.get(path));
|
|
6149
6125
|
window.changes.delete(path);
|
|
6150
6126
|
}
|
|
6127
|
+
(window.parts ??= /* @__PURE__ */ new Set()).add(part.id);
|
|
6151
6128
|
window.entry = await this.commit(part);
|
|
6152
6129
|
}
|
|
6153
6130
|
holdClaim(group, key, idleMs = this.options.quietMs ?? 2e3) {
|
|
@@ -6297,7 +6274,7 @@ var Engine = class {
|
|
|
6297
6274
|
const parts = path.split("/");
|
|
6298
6275
|
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
6299
6276
|
const at = parts.slice(0, depth).join("/");
|
|
6300
|
-
const stat3 = lstatSync4(
|
|
6277
|
+
const stat3 = lstatSync4(join19(this.dir, at), { throwIfNoEntry: false });
|
|
6301
6278
|
if (!stat3) return void 0;
|
|
6302
6279
|
if (at === path || !stat3.isDirectory()) return { at, stat: stat3 };
|
|
6303
6280
|
}
|
|
@@ -6309,8 +6286,8 @@ var Engine = class {
|
|
|
6309
6286
|
if (!standing) return void 0;
|
|
6310
6287
|
if (standing.stat.isSymbolicLink()) return standing.at;
|
|
6311
6288
|
if (standing.at === path && !standing.stat.isDirectory()) return void 0;
|
|
6312
|
-
const real =
|
|
6313
|
-
const at =
|
|
6289
|
+
const real = realpathSync.native(join19(this.dir, standing.at));
|
|
6290
|
+
const at = relative5(realpathSync.native(this.dir), real).split(sep3).join("/");
|
|
6314
6291
|
if (standing.at !== path) return deleted.has(at) ? void 0 : at;
|
|
6315
6292
|
return fileLeftIn(real, at, deleted);
|
|
6316
6293
|
}
|
|
@@ -6334,7 +6311,7 @@ var Engine = class {
|
|
|
6334
6311
|
/** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
|
|
6335
6312
|
async writeProjectFile(path, hash) {
|
|
6336
6313
|
this.assertWritable();
|
|
6337
|
-
const absPath =
|
|
6314
|
+
const absPath = join19(this.dir, path);
|
|
6338
6315
|
const { writeToken } = this;
|
|
6339
6316
|
if (writeToken) {
|
|
6340
6317
|
const version = hash === null ? DELETED_VERSION : hashVersion(hash);
|
|
@@ -6387,7 +6364,13 @@ var Engine = class {
|
|
|
6387
6364
|
(group) => group?.changes.size && mine(group.who)
|
|
6388
6365
|
);
|
|
6389
6366
|
if (pending) return direction === "back" ? this.pendingEntry(pending) : void 0;
|
|
6390
|
-
|
|
6367
|
+
const everyone = this.options.undoScope === "everyone";
|
|
6368
|
+
const ofOpenTurn = (entry) => this.windows.some((open2) => open2.parts?.has(entry.id));
|
|
6369
|
+
return stepTarget(
|
|
6370
|
+
this.log.entries,
|
|
6371
|
+
direction,
|
|
6372
|
+
(entry) => mine(entry.who) || everyone && !ofOpenTurn(entry)
|
|
6373
|
+
);
|
|
6391
6374
|
}
|
|
6392
6375
|
/** A pending group as the entry it becomes once committed (a window's part gets a fresh id). */
|
|
6393
6376
|
pendingEntry(group) {
|
|
@@ -6466,10 +6449,10 @@ var Engine = class {
|
|
|
6466
6449
|
throw new Error(`Checkout writes into an empty folder only: ${dest}`);
|
|
6467
6450
|
const files = manifestAround(this.log, entryId, side);
|
|
6468
6451
|
try {
|
|
6469
|
-
for (const [path, hash] of files) await this.blobs.writeTo(hash,
|
|
6452
|
+
for (const [path, hash] of files) await this.blobs.writeTo(hash, join19(dest, path));
|
|
6470
6453
|
} catch (error) {
|
|
6471
6454
|
for (const name of await readdir2(dest).catch(() => []))
|
|
6472
|
-
await rm2(
|
|
6455
|
+
await rm2(join19(dest, name), { recursive: true, force: true });
|
|
6473
6456
|
throw error;
|
|
6474
6457
|
}
|
|
6475
6458
|
}),
|
|
@@ -6505,9 +6488,9 @@ var Engine = class {
|
|
|
6505
6488
|
};
|
|
6506
6489
|
function isWithin(dir, path) {
|
|
6507
6490
|
let probe = resolve8(path);
|
|
6508
|
-
while (!existsSync11(probe) &&
|
|
6509
|
-
const rel =
|
|
6510
|
-
return rel === "" || rel !== ".." && !rel.startsWith(`..${
|
|
6491
|
+
while (!existsSync11(probe) && dirname9(probe) !== probe) probe = dirname9(probe);
|
|
6492
|
+
const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
|
|
6493
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
|
|
6511
6494
|
}
|
|
6512
6495
|
function missingIsEmpty(error) {
|
|
6513
6496
|
if (error.code === "ENOENT") return [];
|
|
@@ -6524,11 +6507,11 @@ async function openProjectHistory(options) {
|
|
|
6524
6507
|
throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
|
|
6525
6508
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
6526
6509
|
const release = await takeHistoryOwnership(
|
|
6527
|
-
|
|
6510
|
+
join19(options.historyRoot, projectId),
|
|
6528
6511
|
options.ownerWaitMs ?? 5e3
|
|
6529
6512
|
);
|
|
6530
6513
|
try {
|
|
6531
|
-
const blobs = await openBlobStore(
|
|
6514
|
+
const blobs = await openBlobStore(join19(options.historyRoot, projectId, "blobs"));
|
|
6532
6515
|
const engine = new Engine(options, projectId, blobs);
|
|
6533
6516
|
await engine.queue(() => engine.start());
|
|
6534
6517
|
const api = engine.api();
|
|
@@ -6777,10 +6760,10 @@ function numericAttribute(element, name) {
|
|
|
6777
6760
|
|
|
6778
6761
|
// src/helpers/previewDocumentStore.ts
|
|
6779
6762
|
import { createHash as createHash8 } from "crypto";
|
|
6780
|
-
import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as
|
|
6781
|
-
import { join as
|
|
6763
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync7, rmSync as rmSync7 } from "fs";
|
|
6764
|
+
import { join as join20 } from "path";
|
|
6782
6765
|
function createPreviewDocumentStore(dir, salt) {
|
|
6783
|
-
const fileFor = (key) =>
|
|
6766
|
+
const fileFor = (key) => join20(dir, `${createHash8("sha256").update(`${salt}
|
|
6784
6767
|
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
6785
6768
|
return {
|
|
6786
6769
|
read(key) {
|
|
@@ -6795,8 +6778,8 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
|
|
|
6795
6778
|
mkdirSync12(dir, { recursive: true });
|
|
6796
6779
|
const file = fileFor(key);
|
|
6797
6780
|
replaceFileAtomically(file, html, 420);
|
|
6798
|
-
for (const name of
|
|
6799
|
-
if (
|
|
6781
|
+
for (const name of readdirSync7(dir)) {
|
|
6782
|
+
if (join20(dir, name) !== file) rmSync7(join20(dir, name), { force: true });
|
|
6800
6783
|
}
|
|
6801
6784
|
} catch {
|
|
6802
6785
|
}
|
|
@@ -6877,6 +6860,7 @@ export {
|
|
|
6877
6860
|
START as HISTORY_START,
|
|
6878
6861
|
HistoryBusyError,
|
|
6879
6862
|
HistoryClosedError,
|
|
6863
|
+
HistoryIdError,
|
|
6880
6864
|
MAX_WINDOW_IDLE_MS,
|
|
6881
6865
|
MIME_TYPES,
|
|
6882
6866
|
PREVIEW_BUNDLE_OPTIONS,
|