@hyperframes/studio-server 0.8.81 → 0.8.82
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-P5EJPIH3.js → chunk-W6TFBTP2.js} +7 -6
- package/dist/chunk-W6TFBTP2.js.map +1 -0
- package/dist/helpers/mediaProxyPreview.d.ts +1 -1
- package/dist/helpers/sourceMutation.js +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.js +301 -80
- package/dist/index.js.map +1 -1
- package/dist/{mediaProxyPreview-BOp3Wa9v.d.ts → mediaProxyPreview-BMHd9G3N.d.ts} +1 -0
- package/package.json +3 -3
- package/dist/chunk-P5EJPIH3.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
splitElementInHtml,
|
|
14
14
|
unwrapElementsFromHtml,
|
|
15
15
|
wrapElementsInHtml
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-W6TFBTP2.js";
|
|
17
17
|
import {
|
|
18
18
|
getElementScreenshotClip
|
|
19
19
|
} from "./chunk-W2SBTCO2.js";
|
|
@@ -583,27 +583,41 @@ import { spawnSync } from "child_process";
|
|
|
583
583
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
584
584
|
import { tmpdir } from "os";
|
|
585
585
|
import { basename, join as join4 } from "path";
|
|
586
|
+
import { FFPROBE_PATH_ENV, findFfBinary as findFfBinary2 } from "@hyperframes/parsers/ff-binaries";
|
|
586
587
|
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
587
588
|
var AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
|
588
|
-
|
|
589
|
+
var defaultRunner = spawnSync;
|
|
590
|
+
var FFPROBE_MISSING = "not checked: ffprobe was not found. Install FFmpeg or set HYPERFRAMES_FFPROBE_PATH.";
|
|
591
|
+
function validateUploadedMedia(filePath, runner = defaultRunner) {
|
|
589
592
|
const isVideo = VIDEO_EXT.test(filePath);
|
|
590
593
|
const isAudio = AUDIO_EXT.test(filePath);
|
|
591
594
|
if (!isVideo && !isAudio) {
|
|
592
595
|
return { ok: true };
|
|
593
596
|
}
|
|
597
|
+
const ffprobe = findFfBinary2("ffprobe") ?? (runner === defaultRunner ? void 0 : "ffprobe");
|
|
598
|
+
if (!ffprobe) return { ok: true, unchecked: FFPROBE_MISSING };
|
|
594
599
|
const result = runner(
|
|
595
|
-
|
|
600
|
+
ffprobe,
|
|
596
601
|
["-v", "error", "-show_entries", "stream=codec_type", "-of", "json", "--", filePath],
|
|
597
602
|
{ windowsHide: true }
|
|
598
603
|
);
|
|
604
|
+
if (result.error && process.env[FFPROBE_PATH_ENV]?.trim()) {
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
reason: `${FFPROBE_PATH_ENV} names "${ffprobe}", which cannot run: fix it or unset it`
|
|
608
|
+
};
|
|
609
|
+
}
|
|
599
610
|
if (result.error?.code === "ENOENT") {
|
|
600
|
-
return { ok: true };
|
|
611
|
+
return { ok: true, unchecked: FFPROBE_MISSING };
|
|
601
612
|
}
|
|
602
613
|
if (result.status !== 0) {
|
|
603
614
|
return { ok: false, reason: "ffprobe failed to read the media file" };
|
|
604
615
|
}
|
|
616
|
+
return checkStreams(String(result.stdout || "{}"), isVideo, isAudio);
|
|
617
|
+
}
|
|
618
|
+
function checkStreams(stdout, isVideo, isAudio) {
|
|
605
619
|
try {
|
|
606
|
-
const parsed = JSON.parse(
|
|
620
|
+
const parsed = JSON.parse(stdout);
|
|
607
621
|
const streams = parsed.streams ?? [];
|
|
608
622
|
const hasVideo = streams.some((stream) => stream.codec_type === "video");
|
|
609
623
|
const hasAudio = streams.some((stream) => stream.codec_type === "audio");
|
|
@@ -618,7 +632,7 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
|
618
632
|
return { ok: false, reason: "ffprobe returned unreadable media metadata" };
|
|
619
633
|
}
|
|
620
634
|
}
|
|
621
|
-
function validateUploadedMediaBuffer(fileName, buffer, runner =
|
|
635
|
+
function validateUploadedMediaBuffer(fileName, buffer, runner = defaultRunner) {
|
|
622
636
|
const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
|
|
623
637
|
const tempPath = join4(tempDir, basename(fileName));
|
|
624
638
|
try {
|
|
@@ -2502,6 +2516,7 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2502
2516
|
const uploaded = [];
|
|
2503
2517
|
const skipped = [];
|
|
2504
2518
|
const invalid = [];
|
|
2519
|
+
const unchecked = [];
|
|
2505
2520
|
const entries = formData.entries();
|
|
2506
2521
|
const subDir = targetDir === projectDir ? "" : targetDir.slice(projectDir.length + 1);
|
|
2507
2522
|
for (const [, value] of entries) {
|
|
@@ -2564,12 +2579,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2564
2579
|
}
|
|
2565
2580
|
const relativePath = subDir ? join6(subDir, finalName) : finalName;
|
|
2566
2581
|
uploaded.push(relativePath);
|
|
2582
|
+
if (validation.unchecked) unchecked.push({ name: finalName, reason: validation.unchecked });
|
|
2567
2583
|
if (isAudioFile(finalName)) {
|
|
2568
2584
|
generateWaveformCache(projectDir, relativePath).catch(() => {
|
|
2569
2585
|
});
|
|
2570
2586
|
}
|
|
2571
2587
|
}
|
|
2572
|
-
return { uploaded, skipped, invalid };
|
|
2588
|
+
return { uploaded, skipped, invalid, unchecked };
|
|
2573
2589
|
}
|
|
2574
2590
|
function registerFileRoutes(api, adapter) {
|
|
2575
2591
|
api.get("/projects/:id/files/*", async (c) => {
|
|
@@ -3301,7 +3317,13 @@ function registerFileRoutes(api, adapter) {
|
|
|
3301
3317
|
const formData = await c.req.formData();
|
|
3302
3318
|
const result = await processUploadedFiles(formData, targetDir, project.dir);
|
|
3303
3319
|
return c.json(
|
|
3304
|
-
{
|
|
3320
|
+
{
|
|
3321
|
+
ok: true,
|
|
3322
|
+
files: result.uploaded,
|
|
3323
|
+
skipped: result.skipped,
|
|
3324
|
+
invalid: result.invalid,
|
|
3325
|
+
unchecked: result.unchecked
|
|
3326
|
+
},
|
|
3305
3327
|
201
|
|
3306
3328
|
);
|
|
3307
3329
|
}
|
|
@@ -3387,6 +3409,8 @@ import {
|
|
|
3387
3409
|
insertBeforeCloseTag as insertBeforeCloseTag2,
|
|
3388
3410
|
stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2
|
|
3389
3411
|
} from "@hyperframes/core/compiler";
|
|
3412
|
+
import { STUDIO_PREVIEW_MARK_META } from "@hyperframes/core/studio-preview-mark";
|
|
3413
|
+
import { injectTagsAtHeadStart } from "@hyperframes/core/compiler/html-document";
|
|
3390
3414
|
import { isWithinProjectRoot } from "@hyperframes/parsers/asset-resolution";
|
|
3391
3415
|
|
|
3392
3416
|
// src/helpers/subComposition.ts
|
|
@@ -3671,6 +3695,34 @@ function injectPreviewVariables(html, values) {
|
|
|
3671
3695
|
return tag + html;
|
|
3672
3696
|
}
|
|
3673
3697
|
|
|
3698
|
+
// src/helpers/lazyPreviewImages.ts
|
|
3699
|
+
import { findStartTags } from "@hyperframes/core/compiler/html-document";
|
|
3700
|
+
import { parseHTML as parseHTML4 } from "linkedom";
|
|
3701
|
+
import { STUDIO_PREVIEW_LAZY_ATTR } from "@hyperframes/core/studio-preview-mark";
|
|
3702
|
+
function startsAfterZero(el) {
|
|
3703
|
+
for (let node = el; node; node = node.parentElement) {
|
|
3704
|
+
if (Number(node.getAttribute("data-start")) > 0) return true;
|
|
3705
|
+
}
|
|
3706
|
+
return false;
|
|
3707
|
+
}
|
|
3708
|
+
var hasLoading = (img) => Array.from(img.attributes).some((attr) => attr.name.toLowerCase() === "loading");
|
|
3709
|
+
function lazyPreviewImages(html) {
|
|
3710
|
+
if (!/<!doctype|<html[\s>]/i.test(html)) return html;
|
|
3711
|
+
const images = [...parseHTML4(html).document.querySelectorAll("img")];
|
|
3712
|
+
const tags = findStartTags(html, "img");
|
|
3713
|
+
if (tags.length !== images.length) return html;
|
|
3714
|
+
const parts = [];
|
|
3715
|
+
let from = 0;
|
|
3716
|
+
images.forEach((img, i) => {
|
|
3717
|
+
if (hasLoading(img) || !startsAfterZero(img)) return;
|
|
3718
|
+
const at = (tags[i] ?? 0) + 4;
|
|
3719
|
+
parts.push(html.slice(from, at), ` loading="lazy" ${STUDIO_PREVIEW_LAZY_ATTR}`);
|
|
3720
|
+
from = at;
|
|
3721
|
+
});
|
|
3722
|
+
parts.push(html.slice(from));
|
|
3723
|
+
return parts.join("");
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3674
3726
|
// src/routes/preview.ts
|
|
3675
3727
|
var PROJECT_SIGNATURE_META = "hyperframes-project-signature";
|
|
3676
3728
|
var GSAP_CDN_VERSION = "3.15.0";
|
|
@@ -3763,7 +3815,8 @@ function injectStudioMotionScript(html, projectDir, activeCompositionPath) {
|
|
|
3763
3815
|
false
|
|
3764
3816
|
);
|
|
3765
3817
|
}
|
|
3766
|
-
var
|
|
3818
|
+
var GSAP_FALLBACK_ATTR = "data-hf-gsap-fallback";
|
|
3819
|
+
var GSAP_CDN_FALLBACK_SCRIPT = `<script ${GSAP_FALLBACK_ATTR}>
|
|
3767
3820
|
(function(){
|
|
3768
3821
|
var cdnBase="https://cdn.jsdelivr.net/npm/gsap@${GSAP_CDN_VERSION}/dist/";
|
|
3769
3822
|
var loaded={};
|
|
@@ -3784,7 +3837,7 @@ var GSAP_CDN_FALLBACK_SCRIPT = `<script data-hf-gsap-fallback>
|
|
|
3784
3837
|
})();
|
|
3785
3838
|
</script>`;
|
|
3786
3839
|
function injectGsapCdnFallback(html) {
|
|
3787
|
-
if (html.includes(
|
|
3840
|
+
if (html.includes(GSAP_FALLBACK_ATTR)) return html;
|
|
3788
3841
|
if (html.includes("<head>")) return html.replace("<head>", "<head>" + GSAP_CDN_FALLBACK_SCRIPT);
|
|
3789
3842
|
return GSAP_CDN_FALLBACK_SCRIPT + html;
|
|
3790
3843
|
}
|
|
@@ -3810,11 +3863,13 @@ function previewVariablesFromRequest(rawVariables) {
|
|
|
3810
3863
|
if (!parse2.ok) return { error: parse2.error };
|
|
3811
3864
|
return { raw: rawVariables, values: parse2.values };
|
|
3812
3865
|
}
|
|
3813
|
-
|
|
3866
|
+
var PREVIEW_CAPTURE_PARAM = "hf-capture";
|
|
3867
|
+
function injectStudioPreviewAugmentations(html, adapter, projectDir, activeCompositionPath, capture) {
|
|
3868
|
+
const marked = capture ? html : injectTagsAtHeadStart(lazyPreviewImages(html), `<meta name="${STUDIO_PREVIEW_MARK_META}">`);
|
|
3814
3869
|
return injectStudioMotionScript(
|
|
3815
3870
|
injectMotionPathPluginIfNeeded(
|
|
3816
3871
|
injectGsapCdnFallback(
|
|
3817
|
-
injectProjectSignature(
|
|
3872
|
+
injectProjectSignature(marked, resolveProjectSignature(adapter, projectDir))
|
|
3818
3873
|
)
|
|
3819
3874
|
),
|
|
3820
3875
|
projectDir,
|
|
@@ -3872,7 +3927,7 @@ function registerPreviewRoutes(api, adapter) {
|
|
|
3872
3927
|
if (builtPreviews.size > 4) builtPreviews.delete(builtPreviews.keys().next().value);
|
|
3873
3928
|
};
|
|
3874
3929
|
const previewBuilds = /* @__PURE__ */ new Map();
|
|
3875
|
-
async function buildPreview(project, previewVariables, builtKey) {
|
|
3930
|
+
async function buildPreview(project, previewVariables, builtKey, capture) {
|
|
3876
3931
|
const diskMain = resolveProjectMainHtml(project.dir, project.id);
|
|
3877
3932
|
const normalizedDisk = diskMain ? ensureHfIds3(diskMain.html) : null;
|
|
3878
3933
|
try {
|
|
@@ -3902,7 +3957,8 @@ ${runtimeTag}`;
|
|
|
3902
3957
|
ensureHfIds3(await transformPreviewHtml(bundled, adapter, project, mainCompositionPath)),
|
|
3903
3958
|
adapter,
|
|
3904
3959
|
project.dir,
|
|
3905
|
-
mainCompositionPath
|
|
3960
|
+
mainCompositionPath,
|
|
3961
|
+
capture
|
|
3906
3962
|
);
|
|
3907
3963
|
if (previewVariables) bundled = injectPreviewVariables(bundled, previewVariables);
|
|
3908
3964
|
bundled = await injectMediaCodecMap(
|
|
@@ -3914,7 +3970,7 @@ ${runtimeTag}`;
|
|
|
3914
3970
|
);
|
|
3915
3971
|
bundled = addScenePartsManifest(bundled, [`meta[name="${PROJECT_SIGNATURE_META}"]`]);
|
|
3916
3972
|
rememberPreview(builtKey, bundled);
|
|
3917
|
-
adapter.previewDocuments?.write(builtKey, bundled);
|
|
3973
|
+
if (!capture) adapter.previewDocuments?.write(builtKey, bundled);
|
|
3918
3974
|
return bundled;
|
|
3919
3975
|
} catch {
|
|
3920
3976
|
const fallback = resolveProjectMainHtml(project.dir, project.id);
|
|
@@ -3924,7 +3980,8 @@ ${runtimeTag}`;
|
|
|
3924
3980
|
await transformPreviewHtml(fallbackHtml, adapter, project, fallback.compositionPath),
|
|
3925
3981
|
adapter,
|
|
3926
3982
|
project.dir,
|
|
3927
|
-
fallback.compositionPath
|
|
3983
|
+
fallback.compositionPath,
|
|
3984
|
+
capture
|
|
3928
3985
|
);
|
|
3929
3986
|
if (previewVariables) {
|
|
3930
3987
|
fallbackAugmented = injectPreviewVariables(fallbackAugmented, previewVariables);
|
|
@@ -3948,7 +4005,8 @@ ${runtimeTag}`;
|
|
|
3948
4005
|
const vars = previewVariablesFromRequest(c.req.query("variables"));
|
|
3949
4006
|
if (vars.error !== void 0) return c.json({ error: vars.error }, 400);
|
|
3950
4007
|
const previewVariables = vars.values;
|
|
3951
|
-
const
|
|
4008
|
+
const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
|
|
4009
|
+
const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
|
|
3952
4010
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
3953
4011
|
if (ifNoneMatch === etag) {
|
|
3954
4012
|
return new Response(null, {
|
|
@@ -3965,7 +4023,7 @@ ${etag}`;
|
|
|
3965
4023
|
}
|
|
3966
4024
|
let pending = previewBuilds.get(builtKey);
|
|
3967
4025
|
if (!pending) {
|
|
3968
|
-
pending = buildPreview(project, previewVariables, builtKey).finally(
|
|
4026
|
+
pending = buildPreview(project, previewVariables, builtKey, capture).finally(
|
|
3969
4027
|
() => previewBuilds.delete(builtKey)
|
|
3970
4028
|
);
|
|
3971
4029
|
previewBuilds.set(builtKey, pending);
|
|
@@ -3997,7 +4055,8 @@ ${etag}`;
|
|
|
3997
4055
|
return c.text("not found", 404);
|
|
3998
4056
|
}
|
|
3999
4057
|
const compPathHash = createHash4("sha1").update(compPath).digest("hex");
|
|
4000
|
-
const
|
|
4058
|
+
const capture = c.req.query(PREVIEW_CAPTURE_PARAM) !== void 0;
|
|
4059
|
+
const etag = `"comp:v2:${compPathHash}:${signature}${variablesEtagSalt(vars.raw)}${capture ? ":capture" : ""}"`;
|
|
4001
4060
|
const ifNoneMatch = c.req.header("If-None-Match");
|
|
4002
4061
|
if (ifNoneMatch === etag) {
|
|
4003
4062
|
return new Response(null, {
|
|
@@ -4018,7 +4077,7 @@ ${etag}`;
|
|
|
4018
4077
|
if (!html) return c.text("not found", 404);
|
|
4019
4078
|
recordPreviewReferences(project.dir, html);
|
|
4020
4079
|
html = ensureHfIds3(await transformPreviewHtml(html, adapter, project, compPath));
|
|
4021
|
-
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath);
|
|
4080
|
+
html = injectStudioPreviewAugmentations(html, adapter, project.dir, compPath, capture);
|
|
4022
4081
|
if (previewVariables) html = injectPreviewVariables(html, previewVariables);
|
|
4023
4082
|
html = await injectMediaCodecMap(html, adapter, project.dir, compPath, mediaCodecProbeCache);
|
|
4024
4083
|
return c.html(html, 200, previewCacheHeaders(etag));
|
|
@@ -4817,7 +4876,8 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4817
4876
|
const motion = manifestKey(join12(project.dir, STUDIO_MOTION_PATH));
|
|
4818
4877
|
sourceMtime = Math.max(sourceMtime, manualEdits.mtimeMs, motion.mtimeMs);
|
|
4819
4878
|
const projectUrl = `http://${c.req.header("host")}/api/projects/${encodeURIComponent(project.id)}`;
|
|
4820
|
-
const
|
|
4879
|
+
const previewPath = compPath === "index.html" ? `${projectUrl}/preview` : `${projectUrl}/preview/comp/${compPath.split("/").map(encodeURIComponent).join("/")}`;
|
|
4880
|
+
const previewUrl = `${previewPath}?${PREVIEW_CAPTURE_PARAM}=1`;
|
|
4821
4881
|
const cacheDir = join12(project.dir, ".thumbnails");
|
|
4822
4882
|
const selectorKey = selector ? `_${selector.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 80)}_${selectorIndex ?? 0}` : "";
|
|
4823
4883
|
const urlVersionKey = urlVersion ? `_${urlVersion.replace(/[^a-zA-Z0-9_-]+/g, "_").slice(0, 32)}` : "";
|
|
@@ -5194,7 +5254,7 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
5194
5254
|
// src/routes/media.ts
|
|
5195
5255
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
5196
5256
|
import { existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
5197
|
-
import { basename as basename2, dirname as dirname6, extname as extname2, join as join14 } from "path";
|
|
5257
|
+
import { basename as basename2, dirname as dirname6, extname as extname2, join as join14, posix } from "path";
|
|
5198
5258
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5199
5259
|
".mp4",
|
|
5200
5260
|
".mov",
|
|
@@ -5227,21 +5287,31 @@ function slugFileBase(path) {
|
|
|
5227
5287
|
const name = basename2(path, extname2(path)).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5228
5288
|
return name || "media";
|
|
5229
5289
|
}
|
|
5230
|
-
function uniqueAssetPath(projectDir, assetPath) {
|
|
5290
|
+
function uniqueAssetPath(projectDir, assetPath, taken) {
|
|
5231
5291
|
const ext = extname2(assetPath);
|
|
5232
5292
|
const withoutExt = assetPath.slice(0, -ext.length);
|
|
5233
5293
|
let candidate = assetPath;
|
|
5234
|
-
for (let index = 2; existsSync8(join14(projectDir, candidate)); index++) {
|
|
5294
|
+
for (let index = 2; taken.has(candidate) || existsSync8(join14(projectDir, candidate)); index++) {
|
|
5235
5295
|
candidate = `${withoutExt}-${index}${ext}`;
|
|
5236
5296
|
}
|
|
5237
5297
|
return candidate;
|
|
5238
5298
|
}
|
|
5239
|
-
function defaultOutputPath(projectDir, inputPath) {
|
|
5299
|
+
function defaultOutputPath(projectDir, inputPath, taken) {
|
|
5240
5300
|
const ext = isImagePath(inputPath) ? ".png" : ".webm";
|
|
5241
|
-
return uniqueAssetPath(
|
|
5301
|
+
return uniqueAssetPath(
|
|
5302
|
+
projectDir,
|
|
5303
|
+
`assets/cutouts/${slugFileBase(inputPath)}-cutout${ext}`,
|
|
5304
|
+
taken
|
|
5305
|
+
);
|
|
5242
5306
|
}
|
|
5243
|
-
function defaultPlatePath(projectDir, inputPath) {
|
|
5244
|
-
return uniqueAssetPath(projectDir, `assets/cutouts/${slugFileBase(inputPath)}-plate.webm
|
|
5307
|
+
function defaultPlatePath(projectDir, inputPath, taken) {
|
|
5308
|
+
return uniqueAssetPath(projectDir, `assets/cutouts/${slugFileBase(inputPath)}-plate.webm`, taken);
|
|
5309
|
+
}
|
|
5310
|
+
function outputsInFlight(mediaJobs) {
|
|
5311
|
+
const running = [...mediaJobs.values()].filter((job) => job.status === "processing");
|
|
5312
|
+
return new Set(
|
|
5313
|
+
running.flatMap((job) => [job.outputAssetPath, job.backgroundOutputAssetPath ?? ""])
|
|
5314
|
+
);
|
|
5245
5315
|
}
|
|
5246
5316
|
function makeJobId(projectId, mediaJobs) {
|
|
5247
5317
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
|
|
@@ -5310,15 +5380,16 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5310
5380
|
if (!inputIsVideo && !inputIsImage) {
|
|
5311
5381
|
return c.json({ error: "background removal supports video or image assets only" }, 400);
|
|
5312
5382
|
}
|
|
5313
|
-
const requestedOutput = body.outputPath ? normalizeProjectAssetPath(body.outputPath) : "";
|
|
5383
|
+
const requestedOutput = body.outputPath ? posix.normalize(normalizeProjectAssetPath(body.outputPath)) : "";
|
|
5314
5384
|
if (requestedOutput && containsNullByte(requestedOutput)) {
|
|
5315
5385
|
return c.json({ error: "forbidden" }, 403);
|
|
5316
5386
|
}
|
|
5317
5387
|
if (requestedOutput && !resolveWithinProject(project.dir, requestedOutput)) {
|
|
5318
5388
|
return c.json({ error: "forbidden" }, 403);
|
|
5319
5389
|
}
|
|
5320
|
-
const
|
|
5321
|
-
const
|
|
5390
|
+
const taken = outputsInFlight(mediaJobs);
|
|
5391
|
+
const outputAssetPath = requestedOutput ? uniqueAssetPath(project.dir, requestedOutput, taken) : defaultOutputPath(project.dir, inputAssetPath, taken);
|
|
5392
|
+
const outputPath = pinWithinProject(project.dir, outputAssetPath);
|
|
5322
5393
|
if (!outputPath) return c.json({ error: "forbidden" }, 403);
|
|
5323
5394
|
if (inputIsVideo && !VIDEO_OUTPUT_EXTENSIONS.has(extname2(outputAssetPath).toLowerCase())) {
|
|
5324
5395
|
return c.json({ error: "video background removal output must be .webm or .mov" }, 400);
|
|
@@ -5332,8 +5403,9 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
5332
5403
|
if (!inputIsVideo) {
|
|
5333
5404
|
return c.json({ error: "background plates are only supported for video inputs" }, 400);
|
|
5334
5405
|
}
|
|
5335
|
-
|
|
5336
|
-
|
|
5406
|
+
taken.add(outputAssetPath);
|
|
5407
|
+
backgroundOutputAssetPath = defaultPlatePath(project.dir, inputAssetPath, taken);
|
|
5408
|
+
backgroundOutputPath = pinWithinProject(project.dir, backgroundOutputAssetPath) ?? void 0;
|
|
5337
5409
|
if (!backgroundOutputPath) {
|
|
5338
5410
|
return c.json({ error: "forbidden" }, 403);
|
|
5339
5411
|
}
|
|
@@ -5421,20 +5493,20 @@ function registerGlobalAssetRoutes(api) {
|
|
|
5421
5493
|
}
|
|
5422
5494
|
|
|
5423
5495
|
// src/history/projectHistory.ts
|
|
5424
|
-
import { randomUUID as
|
|
5425
|
-
import { readdir as
|
|
5496
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
5497
|
+
import { readdir as readdir3, rm as rm3, rmdir, writeFile } from "fs/promises";
|
|
5426
5498
|
import {
|
|
5427
5499
|
existsSync as existsSync11,
|
|
5428
5500
|
lstatSync as lstatSync4,
|
|
5429
|
-
statSync as
|
|
5501
|
+
statSync as statSync10,
|
|
5430
5502
|
mkdirSync as mkdirSync11,
|
|
5431
|
-
readdirSync as
|
|
5432
|
-
readFileSync as
|
|
5503
|
+
readdirSync as readdirSync7,
|
|
5504
|
+
readFileSync as readFileSync19,
|
|
5433
5505
|
realpathSync,
|
|
5434
5506
|
rmSync as rmSync6
|
|
5435
5507
|
} from "fs";
|
|
5436
5508
|
import { homedir as homedir2 } from "os";
|
|
5437
|
-
import { dirname as
|
|
5509
|
+
import { dirname as dirname10, isAbsolute as isAbsolute2, join as join20, relative as relative5, resolve as resolve8, sep as sep3 } from "path";
|
|
5438
5510
|
|
|
5439
5511
|
// src/history/blobStore.ts
|
|
5440
5512
|
import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
|
|
@@ -5503,12 +5575,20 @@ async function openBlobStore(dir) {
|
|
|
5503
5575
|
};
|
|
5504
5576
|
}
|
|
5505
5577
|
|
|
5578
|
+
// src/history/pruneHistories.ts
|
|
5579
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5580
|
+
import { readdirSync as readdirSync6, readFileSync as readFileSync17, renameSync as renameSync5, statSync as statSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
5581
|
+
import { lstat, readdir as readdir2, rm as rm2 } from "fs/promises";
|
|
5582
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5583
|
+
import { dirname as dirname8, join as join19 } from "path";
|
|
5584
|
+
|
|
5506
5585
|
// src/history/historyId.ts
|
|
5507
5586
|
import { randomUUID as randomUUID6 } from "crypto";
|
|
5508
5587
|
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync15, statSync as statSync8, writeFileSync as writeFileSync7 } from "fs";
|
|
5509
5588
|
import { basename as basename3, join as join17, resolve as resolve7 } from "path";
|
|
5510
5589
|
var ID_PATH = join17(".hyperframes", "history-id");
|
|
5511
5590
|
var ID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5591
|
+
var isHistoryId = (name) => ID_SHAPE.test(name);
|
|
5512
5592
|
function readId(projectDir) {
|
|
5513
5593
|
try {
|
|
5514
5594
|
const id = readFileSync15(join17(projectDir, ID_PATH), "utf-8").trim();
|
|
@@ -5566,7 +5646,7 @@ var HistoryIdError = class extends Error {
|
|
|
5566
5646
|
}
|
|
5567
5647
|
};
|
|
5568
5648
|
function recordProject(historyDir, dir, folder) {
|
|
5569
|
-
const record = { dir, ino: folder.ino, born: folder.birthtimeMs };
|
|
5649
|
+
const record = { dir, ino: folder.ino, born: folder.birthtimeMs, dev: folder.dev };
|
|
5570
5650
|
mkdirSync8(historyDir, { recursive: true });
|
|
5571
5651
|
replaceFileAtomically(join17(historyDir, "project.json"), JSON.stringify(record), 420);
|
|
5572
5652
|
}
|
|
@@ -5652,9 +5732,145 @@ async function takeHistoryOwnership(home, waitMs) {
|
|
|
5652
5732
|
}
|
|
5653
5733
|
}
|
|
5654
5734
|
|
|
5735
|
+
// src/history/pruneHistories.ts
|
|
5736
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
5737
|
+
var KEEP_GONE_PROJECT_HISTORY_MS = 14 * DAY_MS;
|
|
5738
|
+
var PRUNED_PREFIX = ".pruned-";
|
|
5739
|
+
var LAST_PRUNE_FILE = ".last-prune";
|
|
5740
|
+
async function pruneGoneProjectHistories(historyRoot, options = {}) {
|
|
5741
|
+
return (await pruneWithin(historyRoot, options)).pruned;
|
|
5742
|
+
}
|
|
5743
|
+
async function pruneWithin(historyRoot, {
|
|
5744
|
+
now = Date.now(),
|
|
5745
|
+
dryRun = false,
|
|
5746
|
+
tempDir = tmpdir2(),
|
|
5747
|
+
onError = (error) => console.warn("[history] could not prune a history:", error),
|
|
5748
|
+
budgetMs = Infinity,
|
|
5749
|
+
measure = true
|
|
5750
|
+
}) {
|
|
5751
|
+
const started = performance.now();
|
|
5752
|
+
let names;
|
|
5753
|
+
try {
|
|
5754
|
+
names = readdirSync6(historyRoot);
|
|
5755
|
+
} catch (error) {
|
|
5756
|
+
if (error.code === "ENOENT") return { pruned: [], finished: true };
|
|
5757
|
+
throw error;
|
|
5758
|
+
}
|
|
5759
|
+
const pruned = [];
|
|
5760
|
+
for (const name of names) {
|
|
5761
|
+
if (performance.now() - started >= budgetMs) return { pruned, finished: false };
|
|
5762
|
+
try {
|
|
5763
|
+
const gone = await pruneEntry(historyRoot, name, { now, dryRun, tempDir, measure });
|
|
5764
|
+
if (gone) pruned.push(gone);
|
|
5765
|
+
} catch (error) {
|
|
5766
|
+
onError(error);
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
return { pruned, finished: true };
|
|
5770
|
+
}
|
|
5771
|
+
async function pruneEntry(historyRoot, name, run) {
|
|
5772
|
+
if (name.startsWith(PRUNED_PREFIX)) {
|
|
5773
|
+
if (!run.dryRun) await rm2(join19(historyRoot, name), { recursive: true, force: true });
|
|
5774
|
+
return null;
|
|
5775
|
+
}
|
|
5776
|
+
return isHistoryId(name) ? pruneOne(historyRoot, name, run) : null;
|
|
5777
|
+
}
|
|
5778
|
+
async function pruneOne(historyRoot, id, run) {
|
|
5779
|
+
const home = join19(historyRoot, id);
|
|
5780
|
+
if (abandonedProject(home, id, run.now, run.tempDir) === null) return null;
|
|
5781
|
+
const release = await takeUnlessBusy(home);
|
|
5782
|
+
if (!release) return null;
|
|
5783
|
+
let taken;
|
|
5784
|
+
try {
|
|
5785
|
+
taken = recheckAndMoveAway(historyRoot, id, run);
|
|
5786
|
+
} catch (error) {
|
|
5787
|
+
release();
|
|
5788
|
+
throw error;
|
|
5789
|
+
}
|
|
5790
|
+
if (!taken?.trash) release();
|
|
5791
|
+
if (!taken) return null;
|
|
5792
|
+
const bytes = await clearAway(taken.trash, home, run.measure);
|
|
5793
|
+
return { id, projectDir: taken.projectDir, bytes };
|
|
5794
|
+
}
|
|
5795
|
+
async function clearAway(trash, home, measure) {
|
|
5796
|
+
const bytes = measure ? await bytesIn(trash ?? home) : 0;
|
|
5797
|
+
if (trash) await rm2(trash, { recursive: true, force: true });
|
|
5798
|
+
return bytes;
|
|
5799
|
+
}
|
|
5800
|
+
async function takeUnlessBusy(home) {
|
|
5801
|
+
try {
|
|
5802
|
+
return await takeHistoryOwnership(home, 0);
|
|
5803
|
+
} catch (error) {
|
|
5804
|
+
if (error instanceof HistoryBusyError) return null;
|
|
5805
|
+
throw error;
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
function recheckAndMoveAway(historyRoot, id, { now, dryRun, tempDir }) {
|
|
5809
|
+
const home = join19(historyRoot, id);
|
|
5810
|
+
const projectDir = abandonedProject(home, id, now, tempDir);
|
|
5811
|
+
if (projectDir === null) return null;
|
|
5812
|
+
if (dryRun) return { projectDir, trash: null };
|
|
5813
|
+
const trash = join19(historyRoot, `${PRUNED_PREFIX}${id}-${randomUUID8()}`);
|
|
5814
|
+
renameSync5(home, trash);
|
|
5815
|
+
return { projectDir, trash };
|
|
5816
|
+
}
|
|
5817
|
+
function abandonedProject(home, id, now, tempDir) {
|
|
5818
|
+
const record = readRecord(home);
|
|
5819
|
+
const dir = record?.dir;
|
|
5820
|
+
if (typeof dir !== "string") return null;
|
|
5821
|
+
if (now - lastUsed(home) < KEEP_GONE_PROJECT_HISTORY_MS && !isSafePath(tempDir, dir)) return null;
|
|
5822
|
+
return projectGone(dir, id, record?.dev, tempDir) ? dir : null;
|
|
5823
|
+
}
|
|
5824
|
+
function projectGone(dir, id, dev, tempDir) {
|
|
5825
|
+
if (!statSync9(join19(dir, ID_PATH), { throwIfNoEntry: false }))
|
|
5826
|
+
return diskStillHere(dir, dev, tempDir);
|
|
5827
|
+
const there = readId(dir);
|
|
5828
|
+
return there !== null && there !== id;
|
|
5829
|
+
}
|
|
5830
|
+
function diskStillHere(dir, dev, tempDir) {
|
|
5831
|
+
let at = dir;
|
|
5832
|
+
let stat3 = statSync9(at, { throwIfNoEntry: false });
|
|
5833
|
+
while (!stat3) {
|
|
5834
|
+
if (dirname8(at) === at) return false;
|
|
5835
|
+
at = dirname8(at);
|
|
5836
|
+
stat3 = statSync9(at, { throwIfNoEntry: false });
|
|
5837
|
+
}
|
|
5838
|
+
if (typeof dev === "number") return stat3.dev === dev;
|
|
5839
|
+
return at === dir || isSafePath(tempDir, at);
|
|
5840
|
+
}
|
|
5841
|
+
function lastUsed(home) {
|
|
5842
|
+
const mtime = (file) => statSync9(join19(home, file), { throwIfNoEntry: false })?.mtimeMs ?? 0;
|
|
5843
|
+
return Math.max(mtime("project.json"), mtime("log.jsonl"));
|
|
5844
|
+
}
|
|
5845
|
+
async function bytesIn(dir) {
|
|
5846
|
+
let total = 0;
|
|
5847
|
+
for (const entry of await readdir2(dir, { withFileTypes: true })) {
|
|
5848
|
+
const path = join19(dir, entry.name);
|
|
5849
|
+
total += entry.isDirectory() ? await bytesIn(path) : (await lstat(path)).size;
|
|
5850
|
+
}
|
|
5851
|
+
return total;
|
|
5852
|
+
}
|
|
5853
|
+
function prunedWithinADay(stamp, now) {
|
|
5854
|
+
try {
|
|
5855
|
+
const last = Number(readFileSync17(stamp, "utf-8"));
|
|
5856
|
+
return last <= now && now - last < DAY_MS;
|
|
5857
|
+
} catch {
|
|
5858
|
+
return false;
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
var pruning = /* @__PURE__ */ new Set();
|
|
5862
|
+
function pruneGoneProjectHistoriesDaily(historyRoot, now, { onError, budgetMs } = {}) {
|
|
5863
|
+
const stamp = join19(historyRoot, LAST_PRUNE_FILE);
|
|
5864
|
+
if (pruning.has(historyRoot) || prunedWithinADay(stamp, now)) return;
|
|
5865
|
+
pruning.add(historyRoot);
|
|
5866
|
+
setImmediate(() => {
|
|
5867
|
+
pruneWithin(historyRoot, { now, onError, budgetMs, measure: false }).then(({ finished }) => finished && writeFileSync9(stamp, String(now))).catch((error) => onError?.(error)).finally(() => pruning.delete(historyRoot));
|
|
5868
|
+
});
|
|
5869
|
+
}
|
|
5870
|
+
|
|
5655
5871
|
// src/history/historyLog.ts
|
|
5656
|
-
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as
|
|
5657
|
-
import { dirname as
|
|
5872
|
+
import { closeSync as closeSync5, constants as constants4, mkdirSync as mkdirSync10, openSync as openSync5, readFileSync as readFileSync18, writeSync as writeSync2 } from "fs";
|
|
5873
|
+
import { dirname as dirname9 } from "path";
|
|
5658
5874
|
var START = "start";
|
|
5659
5875
|
function parseRecord(line) {
|
|
5660
5876
|
try {
|
|
@@ -5666,7 +5882,7 @@ function parseRecord(line) {
|
|
|
5666
5882
|
function readLog(file, onUnreadable) {
|
|
5667
5883
|
let text2;
|
|
5668
5884
|
try {
|
|
5669
|
-
text2 =
|
|
5885
|
+
text2 = readFileSync18(file, "utf-8");
|
|
5670
5886
|
} catch {
|
|
5671
5887
|
return null;
|
|
5672
5888
|
}
|
|
@@ -5706,7 +5922,7 @@ function writeLog(file, log) {
|
|
|
5706
5922
|
...log.entries.map((entry) => ({ type: "entry", entry })),
|
|
5707
5923
|
...[...log.pins].map((id) => ({ type: "pin", id, pinned: true }))
|
|
5708
5924
|
];
|
|
5709
|
-
mkdirSync10(
|
|
5925
|
+
mkdirSync10(dirname9(file), { recursive: true });
|
|
5710
5926
|
replaceFileAtomically(file, records.map((r) => `${JSON.stringify(r)}
|
|
5711
5927
|
`).join(""), 420);
|
|
5712
5928
|
}
|
|
@@ -5779,7 +5995,7 @@ function referencedHashes(log, current) {
|
|
|
5779
5995
|
}
|
|
5780
5996
|
|
|
5781
5997
|
// src/history/projectHistory.ts
|
|
5782
|
-
var DEFAULT_HISTORY_ROOT =
|
|
5998
|
+
var DEFAULT_HISTORY_ROOT = join20(homedir2(), ".cache", "hyperframes", "history");
|
|
5783
5999
|
var MAX_WINDOW_IDLE_MS = 10 * 6e4;
|
|
5784
6000
|
var UNDO_MODES = ["just-this", "back-to-before", "keep-later-edits"];
|
|
5785
6001
|
var OUTSIDE = { kind: "outside", name: "Outside" };
|
|
@@ -5799,26 +6015,26 @@ var isMissingFolder = (error) => ["ENOENT", "ENOTDIR"].includes(error.code ?? ""
|
|
|
5799
6015
|
async function removeEmptyFolders(dir) {
|
|
5800
6016
|
let entries;
|
|
5801
6017
|
try {
|
|
5802
|
-
entries = await
|
|
6018
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
5803
6019
|
} catch (error) {
|
|
5804
6020
|
if (isMissingFolder(error)) return;
|
|
5805
6021
|
throw error;
|
|
5806
6022
|
}
|
|
5807
6023
|
for (const entry of entries)
|
|
5808
|
-
if (entry.isDirectory()) await removeEmptyFolders(
|
|
6024
|
+
if (entry.isDirectory()) await removeEmptyFolders(join20(dir, entry.name));
|
|
5809
6025
|
await rmdir(dir);
|
|
5810
6026
|
}
|
|
5811
6027
|
function fileLeftIn(dir, at, deleted) {
|
|
5812
|
-
for (const entry of
|
|
6028
|
+
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
5813
6029
|
const path = `${at}/${entry.name}`;
|
|
5814
|
-
const left = entry.isDirectory() ? fileLeftIn(
|
|
6030
|
+
const left = entry.isDirectory() ? fileLeftIn(join20(dir, entry.name), path, deleted) : deleted.has(path) ? void 0 : path;
|
|
5815
6031
|
if (left) return left;
|
|
5816
6032
|
}
|
|
5817
6033
|
return void 0;
|
|
5818
6034
|
}
|
|
5819
6035
|
function ignoresCase(dir) {
|
|
5820
|
-
const self =
|
|
5821
|
-
const other =
|
|
6036
|
+
const self = statSync10(join20(dir, ID_PATH), { throwIfNoEntry: false });
|
|
6037
|
+
const other = statSync10(join20(dir, ID_PATH.toUpperCase()), { throwIfNoEntry: false });
|
|
5822
6038
|
return self !== void 0 && other?.ino === self.ino && other.dev === self.dev;
|
|
5823
6039
|
}
|
|
5824
6040
|
var blocks = (removed, added) => added.startsWith(`${removed}/`) || removed.startsWith(`${added}/`);
|
|
@@ -5835,9 +6051,9 @@ var Engine = class {
|
|
|
5835
6051
|
this.projectId = projectId;
|
|
5836
6052
|
this.blobs = blobs;
|
|
5837
6053
|
this.dir = resolve8(options.projectDir);
|
|
5838
|
-
this.home =
|
|
6054
|
+
this.home = join20(options.historyRoot, projectId);
|
|
5839
6055
|
this.foldsCase = ignoresCase(this.dir);
|
|
5840
|
-
const folder =
|
|
6056
|
+
const folder = statSync10(this.dir, { throwIfNoEntry: false });
|
|
5841
6057
|
if (!folder || readId(this.dir) !== projectId || !isRecordedFolder(this.home, folder))
|
|
5842
6058
|
throw this.replaced();
|
|
5843
6059
|
this.folder = { dev: folder.dev, ino: folder.ino, birthtimeMs: folder.birthtimeMs };
|
|
@@ -5868,7 +6084,7 @@ var Engine = class {
|
|
|
5868
6084
|
return this.foldsCase ? path.toLowerCase() : path;
|
|
5869
6085
|
}
|
|
5870
6086
|
get logFile() {
|
|
5871
|
-
return
|
|
6087
|
+
return join20(this.home, "log.jsonl");
|
|
5872
6088
|
}
|
|
5873
6089
|
now() {
|
|
5874
6090
|
return (this.options.now ?? Date.now)();
|
|
@@ -5883,7 +6099,7 @@ var Engine = class {
|
|
|
5883
6099
|
}
|
|
5884
6100
|
/** Another project if the folder or its id changed, or the id is gone: a recreated folder may reuse the inode. */
|
|
5885
6101
|
whereFolder() {
|
|
5886
|
-
const now =
|
|
6102
|
+
const now = statSync10(this.dir, { throwIfNoEntry: false });
|
|
5887
6103
|
if (!now) return "gone";
|
|
5888
6104
|
const same = now.dev === this.folder.dev && sameFolder(now, this.folder);
|
|
5889
6105
|
return same && readId(this.dir) === this.projectId ? "here" : "replaced";
|
|
@@ -5896,7 +6112,7 @@ var Engine = class {
|
|
|
5896
6112
|
persistLog(record) {
|
|
5897
6113
|
if (record) saveRecord(this.logFile, this.log, record);
|
|
5898
6114
|
else writeLog(this.logFile, this.log);
|
|
5899
|
-
if (!existsSync11(
|
|
6115
|
+
if (!existsSync11(join20(this.home, "project.json")))
|
|
5900
6116
|
recordProject(this.home, this.dir, this.folder);
|
|
5901
6117
|
}
|
|
5902
6118
|
replaced() {
|
|
@@ -5946,7 +6162,7 @@ var Engine = class {
|
|
|
5946
6162
|
}
|
|
5947
6163
|
readStatCache() {
|
|
5948
6164
|
try {
|
|
5949
|
-
const files = JSON.parse(
|
|
6165
|
+
const files = JSON.parse(readFileSync19(join20(this.home, "stat.json"), "utf-8"));
|
|
5950
6166
|
return new Map(Object.entries(files));
|
|
5951
6167
|
} catch {
|
|
5952
6168
|
return /* @__PURE__ */ new Map();
|
|
@@ -5955,7 +6171,7 @@ var Engine = class {
|
|
|
5955
6171
|
saveStatCache() {
|
|
5956
6172
|
const files = JSON.stringify(Object.fromEntries(this.tracked));
|
|
5957
6173
|
mkdirSync11(this.home, { recursive: true });
|
|
5958
|
-
replaceFileAtomically(
|
|
6174
|
+
replaceFileAtomically(join20(this.home, "stat.json"), files, 420);
|
|
5959
6175
|
}
|
|
5960
6176
|
/**
|
|
5961
6177
|
* Takes in every write since the last sweep and files each change to its writer. ponytail: always the whole
|
|
@@ -5995,7 +6211,7 @@ var Engine = class {
|
|
|
5995
6211
|
/** The file's hash once copied in; null when it was removed before the copy (the next sweep records that). */
|
|
5996
6212
|
async storeIfPresent(path) {
|
|
5997
6213
|
try {
|
|
5998
|
-
return await this.blobs.put(
|
|
6214
|
+
return await this.blobs.put(join20(this.dir, path));
|
|
5999
6215
|
} catch (error) {
|
|
6000
6216
|
if (error.code === "ENOENT") return null;
|
|
6001
6217
|
throw error;
|
|
@@ -6082,7 +6298,7 @@ var Engine = class {
|
|
|
6082
6298
|
}
|
|
6083
6299
|
hits.forEach(
|
|
6084
6300
|
({ change }, i) => forgetOverwrittenBytes(
|
|
6085
|
-
|
|
6301
|
+
join20(this.dir, change.path),
|
|
6086
6302
|
new Set([...walked[i]].map(hashVersion))
|
|
6087
6303
|
)
|
|
6088
6304
|
);
|
|
@@ -6090,7 +6306,7 @@ var Engine = class {
|
|
|
6090
6306
|
}
|
|
6091
6307
|
/** Walks the server's writes back from `change.after` to the bytes they replaced, stored so an undo restores them. */
|
|
6092
6308
|
async overwrittenBy(change, told, seen) {
|
|
6093
|
-
const absPath =
|
|
6309
|
+
const absPath = join20(this.dir, change.path);
|
|
6094
6310
|
let bytes;
|
|
6095
6311
|
for (let hash = change.after; hash; ) {
|
|
6096
6312
|
if (seen.has(hash)) return void 0;
|
|
@@ -6102,12 +6318,12 @@ var Engine = class {
|
|
|
6102
6318
|
if (hash === change.before || hash === told) return hash;
|
|
6103
6319
|
}
|
|
6104
6320
|
if (bytes === void 0) return void 0;
|
|
6105
|
-
const staged =
|
|
6321
|
+
const staged = join20(this.home, `overwrote-${randomUUID9()}`);
|
|
6106
6322
|
await writeFile(staged, bytes);
|
|
6107
6323
|
try {
|
|
6108
6324
|
return await this.blobs.put(staged);
|
|
6109
6325
|
} finally {
|
|
6110
|
-
await
|
|
6326
|
+
await rm3(staged, { force: true });
|
|
6111
6327
|
}
|
|
6112
6328
|
}
|
|
6113
6329
|
cutOut(group, change, cut) {
|
|
@@ -6157,7 +6373,7 @@ var Engine = class {
|
|
|
6157
6373
|
return this.outside;
|
|
6158
6374
|
}
|
|
6159
6375
|
newGroup(who, label) {
|
|
6160
|
-
return { id:
|
|
6376
|
+
return { id: randomUUID9(), who, label, startedAt: this.now(), changes: /* @__PURE__ */ new Map() };
|
|
6161
6377
|
}
|
|
6162
6378
|
async commitOutside() {
|
|
6163
6379
|
if (this.outside?.changes.size) await this.commitClaim();
|
|
@@ -6274,7 +6490,7 @@ var Engine = class {
|
|
|
6274
6490
|
const parts = path.split("/");
|
|
6275
6491
|
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
6276
6492
|
const at = parts.slice(0, depth).join("/");
|
|
6277
|
-
const stat3 = lstatSync4(
|
|
6493
|
+
const stat3 = lstatSync4(join20(this.dir, at), { throwIfNoEntry: false });
|
|
6278
6494
|
if (!stat3) return void 0;
|
|
6279
6495
|
if (at === path || !stat3.isDirectory()) return { at, stat: stat3 };
|
|
6280
6496
|
}
|
|
@@ -6286,7 +6502,7 @@ var Engine = class {
|
|
|
6286
6502
|
if (!standing) return void 0;
|
|
6287
6503
|
if (standing.stat.isSymbolicLink()) return standing.at;
|
|
6288
6504
|
if (standing.at === path && !standing.stat.isDirectory()) return void 0;
|
|
6289
|
-
const real = realpathSync.native(
|
|
6505
|
+
const real = realpathSync.native(join20(this.dir, standing.at));
|
|
6290
6506
|
const at = relative5(realpathSync.native(this.dir), real).split(sep3).join("/");
|
|
6291
6507
|
if (standing.at !== path) return deleted.has(at) ? void 0 : at;
|
|
6292
6508
|
return fileLeftIn(real, at, deleted);
|
|
@@ -6311,7 +6527,7 @@ var Engine = class {
|
|
|
6311
6527
|
/** Writes one project file (null deletes it), first leaving the running operation's receipt for its echo. */
|
|
6312
6528
|
async writeProjectFile(path, hash) {
|
|
6313
6529
|
this.assertWritable();
|
|
6314
|
-
const absPath =
|
|
6530
|
+
const absPath = join20(this.dir, path);
|
|
6315
6531
|
const { writeToken } = this;
|
|
6316
6532
|
if (writeToken) {
|
|
6317
6533
|
const version = hash === null ? DELETED_VERSION : hashVersion(hash);
|
|
@@ -6445,14 +6661,14 @@ var Engine = class {
|
|
|
6445
6661
|
const dest = resolve8(emptyDir);
|
|
6446
6662
|
if (isWithin(this.dir, dest))
|
|
6447
6663
|
throw new Error(`Checkout writes outside the project only: ${dest}`);
|
|
6448
|
-
if ((await
|
|
6664
|
+
if ((await readdir3(dest).catch(missingIsEmpty)).length > 0)
|
|
6449
6665
|
throw new Error(`Checkout writes into an empty folder only: ${dest}`);
|
|
6450
6666
|
const files = manifestAround(this.log, entryId, side);
|
|
6451
6667
|
try {
|
|
6452
|
-
for (const [path, hash] of files) await this.blobs.writeTo(hash,
|
|
6668
|
+
for (const [path, hash] of files) await this.blobs.writeTo(hash, join20(dest, path));
|
|
6453
6669
|
} catch (error) {
|
|
6454
|
-
for (const name of await
|
|
6455
|
-
await
|
|
6670
|
+
for (const name of await readdir3(dest).catch(() => []))
|
|
6671
|
+
await rm3(join20(dest, name), { recursive: true, force: true });
|
|
6456
6672
|
throw error;
|
|
6457
6673
|
}
|
|
6458
6674
|
}),
|
|
@@ -6488,7 +6704,7 @@ var Engine = class {
|
|
|
6488
6704
|
};
|
|
6489
6705
|
function isWithin(dir, path) {
|
|
6490
6706
|
let probe = resolve8(path);
|
|
6491
|
-
while (!existsSync11(probe) &&
|
|
6707
|
+
while (!existsSync11(probe) && dirname10(probe) !== probe) probe = dirname10(probe);
|
|
6492
6708
|
const rel = relative5(realpathSync.native(dir), realpathSync.native(probe));
|
|
6493
6709
|
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
|
|
6494
6710
|
}
|
|
@@ -6507,13 +6723,17 @@ async function openProjectHistory(options) {
|
|
|
6507
6723
|
throw new HistoryClosedError(`${options.projectDir} is gone or now another project.`);
|
|
6508
6724
|
const projectId = projectHistoryId(options.projectDir, options.historyRoot);
|
|
6509
6725
|
const release = await takeHistoryOwnership(
|
|
6510
|
-
|
|
6726
|
+
join20(options.historyRoot, projectId),
|
|
6511
6727
|
options.ownerWaitMs ?? 5e3
|
|
6512
6728
|
);
|
|
6513
6729
|
try {
|
|
6514
|
-
const blobs = await openBlobStore(
|
|
6730
|
+
const blobs = await openBlobStore(join20(options.historyRoot, projectId, "blobs"));
|
|
6515
6731
|
const engine = new Engine(options, projectId, blobs);
|
|
6516
6732
|
await engine.queue(() => engine.start());
|
|
6733
|
+
pruneGoneProjectHistoriesDaily(options.historyRoot, engine.now(), {
|
|
6734
|
+
onError: options.onError,
|
|
6735
|
+
budgetMs: options.pruneGoneProjectsBudgetMs
|
|
6736
|
+
});
|
|
6517
6737
|
const api = engine.api();
|
|
6518
6738
|
return {
|
|
6519
6739
|
...api,
|
|
@@ -6760,15 +6980,15 @@ function numericAttribute(element, name) {
|
|
|
6760
6980
|
|
|
6761
6981
|
// src/helpers/previewDocumentStore.ts
|
|
6762
6982
|
import { createHash as createHash8 } from "crypto";
|
|
6763
|
-
import { mkdirSync as mkdirSync12, readFileSync as
|
|
6764
|
-
import { join as
|
|
6983
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, readdirSync as readdirSync8, rmSync as rmSync7 } from "fs";
|
|
6984
|
+
import { join as join21 } from "path";
|
|
6765
6985
|
function createPreviewDocumentStore(dir, salt) {
|
|
6766
|
-
const fileFor = (key) =>
|
|
6986
|
+
const fileFor = (key) => join21(dir, `${createHash8("sha256").update(`${salt}
|
|
6767
6987
|
${key}`).digest("hex").slice(0, 32)}.html`);
|
|
6768
6988
|
return {
|
|
6769
6989
|
read(key) {
|
|
6770
6990
|
try {
|
|
6771
|
-
return
|
|
6991
|
+
return readFileSync20(fileFor(key), "utf-8");
|
|
6772
6992
|
} catch {
|
|
6773
6993
|
return null;
|
|
6774
6994
|
}
|
|
@@ -6778,8 +6998,8 @@ ${key}`).digest("hex").slice(0, 32)}.html`);
|
|
|
6778
6998
|
mkdirSync12(dir, { recursive: true });
|
|
6779
6999
|
const file = fileFor(key);
|
|
6780
7000
|
replaceFileAtomically(file, html, 420);
|
|
6781
|
-
for (const name of
|
|
6782
|
-
if (
|
|
7001
|
+
for (const name of readdirSync8(dir)) {
|
|
7002
|
+
if (join21(dir, name) !== file) rmSync7(join21(dir, name), { force: true });
|
|
6783
7003
|
}
|
|
6784
7004
|
} catch {
|
|
6785
7005
|
}
|
|
@@ -6892,6 +7112,7 @@ export {
|
|
|
6892
7112
|
openProjectHistory,
|
|
6893
7113
|
parseSourceDocument,
|
|
6894
7114
|
patchElementInHtml,
|
|
7115
|
+
pruneGoneProjectHistories,
|
|
6895
7116
|
removeElementFromHtml,
|
|
6896
7117
|
requestSubPath,
|
|
6897
7118
|
settledFileTag,
|