@hyperframes/studio-server 0.8.30 → 0.8.32
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/index.js +125 -58
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -364,12 +364,12 @@ import {
|
|
|
364
364
|
writeSync,
|
|
365
365
|
mkdirSync as mkdirSync3,
|
|
366
366
|
unlinkSync as unlinkSync2,
|
|
367
|
-
rmSync as
|
|
367
|
+
rmSync as rmSync3,
|
|
368
368
|
statSync as statSync2,
|
|
369
|
-
renameSync,
|
|
369
|
+
renameSync as renameSync2,
|
|
370
370
|
readdirSync as readdirSync4
|
|
371
371
|
} from "fs";
|
|
372
|
-
import { resolve as resolve3, dirname as
|
|
372
|
+
import { resolve as resolve3, dirname as dirname3, join as join6 } from "path";
|
|
373
373
|
|
|
374
374
|
// src/helpers/mime.ts
|
|
375
375
|
var MIME_TYPES = {
|
|
@@ -419,8 +419,17 @@ function isAudioFile(name) {
|
|
|
419
419
|
|
|
420
420
|
// src/helpers/waveform.ts
|
|
421
421
|
import { spawn } from "child_process";
|
|
422
|
-
import {
|
|
423
|
-
|
|
422
|
+
import {
|
|
423
|
+
existsSync as existsSync2,
|
|
424
|
+
writeFileSync,
|
|
425
|
+
mkdirSync,
|
|
426
|
+
lstatSync as lstatSync2,
|
|
427
|
+
mkdtempSync,
|
|
428
|
+
renameSync,
|
|
429
|
+
rmSync,
|
|
430
|
+
statSync
|
|
431
|
+
} from "fs";
|
|
432
|
+
import { dirname, join as join3 } from "path";
|
|
424
433
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
425
434
|
var SAMPLE_RATE = 4e3;
|
|
426
435
|
var PEAK_COUNT = 4e3;
|
|
@@ -492,15 +501,39 @@ async function generateWaveformCache(projectDir, assetPath) {
|
|
|
492
501
|
const stats = statSync(audioPath);
|
|
493
502
|
const cacheDir = join3(projectDir, ".waveform-cache");
|
|
494
503
|
const cachePath = join3(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
495
|
-
if (existsSync2(cachePath)) return;
|
|
504
|
+
if (isWaveformCacheDirectory(cacheDir) && existsSync2(cachePath)) return;
|
|
496
505
|
const peaks = await decodeAudioPeaks(audioPath);
|
|
497
|
-
|
|
498
|
-
|
|
506
|
+
writeWaveformCache(cachePath, peaks);
|
|
507
|
+
}
|
|
508
|
+
function isWaveformCacheDirectory(cacheDir) {
|
|
509
|
+
return lstatSync2(cacheDir, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
510
|
+
}
|
|
511
|
+
function writeWaveformCache(cachePath, peaks) {
|
|
512
|
+
const cacheDir = dirname(cachePath);
|
|
513
|
+
try {
|
|
514
|
+
mkdirSync(cacheDir, { mode: 448 });
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
517
|
+
}
|
|
518
|
+
if (!isWaveformCacheDirectory(cacheDir)) {
|
|
519
|
+
throw new Error("Waveform cache must be a directory, not a symlink");
|
|
520
|
+
}
|
|
521
|
+
const stagingDir = mkdtempSync(join3(cacheDir, ".waveform-"));
|
|
522
|
+
try {
|
|
523
|
+
const stagingPath = join3(stagingDir, "peaks.json");
|
|
524
|
+
writeFileSync(stagingPath, JSON.stringify(peaks), { flag: "wx" });
|
|
525
|
+
renameSync(stagingPath, cachePath);
|
|
526
|
+
} finally {
|
|
527
|
+
try {
|
|
528
|
+
rmSync(stagingDir, { recursive: true, force: true });
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
499
532
|
}
|
|
500
533
|
|
|
501
534
|
// src/helpers/mediaValidation.ts
|
|
502
535
|
import { spawnSync } from "child_process";
|
|
503
|
-
import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
536
|
+
import { mkdtempSync as mkdtempSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
504
537
|
import { tmpdir } from "os";
|
|
505
538
|
import { basename, join as join4 } from "path";
|
|
506
539
|
var VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
|
@@ -539,13 +572,13 @@ function validateUploadedMedia(filePath, runner = spawnSync) {
|
|
|
539
572
|
}
|
|
540
573
|
}
|
|
541
574
|
function validateUploadedMediaBuffer(fileName, buffer, runner = spawnSync) {
|
|
542
|
-
const tempDir =
|
|
575
|
+
const tempDir = mkdtempSync2(join4(tmpdir(), "hyperframes-upload-"));
|
|
543
576
|
const tempPath = join4(tempDir, basename(fileName));
|
|
544
577
|
try {
|
|
545
578
|
writeFileSync2(tempPath, buffer);
|
|
546
579
|
return validateUploadedMedia(tempPath, runner);
|
|
547
580
|
} finally {
|
|
548
|
-
|
|
581
|
+
rmSync2(tempDir, { recursive: true, force: true });
|
|
549
582
|
}
|
|
550
583
|
}
|
|
551
584
|
|
|
@@ -687,7 +720,7 @@ import { parseHTML as parseHTML2 } from "linkedom";
|
|
|
687
720
|
// src/helpers/compositionInsertion.ts
|
|
688
721
|
import { existsSync as existsSync3, readFileSync as readFileSync4, realpathSync } from "fs";
|
|
689
722
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
690
|
-
import { dirname, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
723
|
+
import { dirname as dirname2, relative as relative3, resolve as resolve2, sep as sep2 } from "path";
|
|
691
724
|
import { parseHTML } from "linkedom";
|
|
692
725
|
var CompositionInsertionError = class extends Error {
|
|
693
726
|
constructor(message, status) {
|
|
@@ -742,7 +775,7 @@ function canonicalDependency(projectDir, ownerAbs, sourcePath) {
|
|
|
742
775
|
validateSourcePath(sourcePath);
|
|
743
776
|
return canonicalProjectPath(
|
|
744
777
|
projectDir,
|
|
745
|
-
resolveWithinProject(projectDir, relative3(projectDir, resolve2(
|
|
778
|
+
resolveWithinProject(projectDir, relative3(projectDir, resolve2(dirname2(ownerAbs), sourcePath)))
|
|
746
779
|
);
|
|
747
780
|
}
|
|
748
781
|
function validateDependencyGraph(projectDir, targetAbs, sourceAbs) {
|
|
@@ -815,7 +848,7 @@ function uniqueHostId(root, base) {
|
|
|
815
848
|
return `${base}_${suffix}`;
|
|
816
849
|
}
|
|
817
850
|
function relativeSourcePath(targetAbs, sourceAbs) {
|
|
818
|
-
return relative3(
|
|
851
|
+
return relative3(dirname2(targetAbs), sourceAbs).split(sep2).join("/");
|
|
819
852
|
}
|
|
820
853
|
function insertCompositionIntoSource(input) {
|
|
821
854
|
const targetAbs = canonicalProjectFile(input.projectDir, input.targetPath);
|
|
@@ -1153,7 +1186,7 @@ async function parseMutationBody(c) {
|
|
|
1153
1186
|
return { target: body.target, body };
|
|
1154
1187
|
}
|
|
1155
1188
|
function ensureDir(filePath) {
|
|
1156
|
-
const dir =
|
|
1189
|
+
const dir = dirname3(filePath);
|
|
1157
1190
|
if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
|
|
1158
1191
|
}
|
|
1159
1192
|
function generateCopyPath(projectDir, originalPath) {
|
|
@@ -2285,12 +2318,13 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2285
2318
|
if (!isSafePath(projectDir, destPath)) continue;
|
|
2286
2319
|
let finalPath = destPath;
|
|
2287
2320
|
let finalName = name;
|
|
2321
|
+
const dotIdx = name.indexOf(".", name.startsWith(".") ? 1 : 0);
|
|
2322
|
+
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
|
|
2323
|
+
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
2324
|
+
const MAX_COPY_INDEX = 1e4;
|
|
2325
|
+
let n = 1;
|
|
2288
2326
|
if (existsSync4(finalPath)) {
|
|
2289
|
-
|
|
2290
|
-
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
|
|
2291
|
-
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
|
2292
|
-
let n = 2;
|
|
2293
|
-
const MAX_COPY_INDEX = 1e4;
|
|
2327
|
+
n = 2;
|
|
2294
2328
|
while (n < MAX_COPY_INDEX && existsSync4(resolve3(targetDir, `${base} (${n})${ext}`))) n++;
|
|
2295
2329
|
if (n >= MAX_COPY_INDEX) {
|
|
2296
2330
|
skipped.push(name);
|
|
@@ -2306,7 +2340,30 @@ async function processUploadedFiles(formData, targetDir, projectDir) {
|
|
|
2306
2340
|
invalid.push({ name: finalName, reason: validation.reason });
|
|
2307
2341
|
continue;
|
|
2308
2342
|
}
|
|
2309
|
-
|
|
2343
|
+
let written = false;
|
|
2344
|
+
while (n < MAX_COPY_INDEX && isSafePath(projectDir, finalPath)) {
|
|
2345
|
+
try {
|
|
2346
|
+
const fd = openSync(finalPath, "wx");
|
|
2347
|
+
try {
|
|
2348
|
+
writeFileSync4(fd, buffer);
|
|
2349
|
+
} finally {
|
|
2350
|
+
closeSync(fd);
|
|
2351
|
+
}
|
|
2352
|
+
written = true;
|
|
2353
|
+
break;
|
|
2354
|
+
} catch (error) {
|
|
2355
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
|
|
2356
|
+
throw error;
|
|
2357
|
+
}
|
|
2358
|
+
n++;
|
|
2359
|
+
finalName = `${base} (${n})${ext}`;
|
|
2360
|
+
finalPath = resolve3(targetDir, finalName);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
if (!written) {
|
|
2364
|
+
if (n >= MAX_COPY_INDEX) skipped.push(name);
|
|
2365
|
+
continue;
|
|
2366
|
+
}
|
|
2310
2367
|
const relativePath = subDir ? join6(subDir, finalName) : finalName;
|
|
2311
2368
|
uploaded.push(relativePath);
|
|
2312
2369
|
if (isAudioFile(finalName)) {
|
|
@@ -2457,7 +2514,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2457
2514
|
const backup = snapshotBeforeWrite(res.project.dir, res.absPath);
|
|
2458
2515
|
if (backup.error) console.warn(`Failed to create backup for ${res.filePath}: ${backup.error}`);
|
|
2459
2516
|
if (stat.isDirectory()) {
|
|
2460
|
-
|
|
2517
|
+
rmSync3(res.absPath, { recursive: true });
|
|
2461
2518
|
} else {
|
|
2462
2519
|
unlinkSync2(res.absPath);
|
|
2463
2520
|
}
|
|
@@ -2953,7 +3010,7 @@ function registerFileRoutes(api, adapter) {
|
|
|
2953
3010
|
return c.json({ error: "already exists" }, 409);
|
|
2954
3011
|
}
|
|
2955
3012
|
ensureDir(newAbs);
|
|
2956
|
-
|
|
3013
|
+
renameSync2(res.absPath, newAbs);
|
|
2957
3014
|
const updatedFiles = updateReferences(res.project.dir, res.filePath, body.newPath);
|
|
2958
3015
|
return c.json({ ok: true, path: body.newPath, updatedReferences: updatedFiles });
|
|
2959
3016
|
});
|
|
@@ -3073,7 +3130,8 @@ function registerFileRoutes(api, adapter) {
|
|
|
3073
3130
|
}
|
|
3074
3131
|
|
|
3075
3132
|
// src/routes/preview.ts
|
|
3076
|
-
import { existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
|
|
3133
|
+
import { createReadStream, existsSync as existsSync6, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
|
|
3134
|
+
import { Readable } from "stream";
|
|
3077
3135
|
import { join as join8, resolve as resolve4 } from "path";
|
|
3078
3136
|
import { createHash as createHash3 } from "crypto";
|
|
3079
3137
|
import { injectScriptsIntoHtml, stripEmbeddedRuntimeScripts as stripEmbeddedRuntimeScripts2 } from "@hyperframes/core/compiler";
|
|
@@ -3714,29 +3772,39 @@ ${runtimeTag}`;
|
|
|
3714
3772
|
}
|
|
3715
3773
|
servedContentType = PROXY_VARIANT_CONFIG[proxyVariant].contentType;
|
|
3716
3774
|
}
|
|
3717
|
-
const
|
|
3718
|
-
const totalSize =
|
|
3775
|
+
const textBuffer = isText ? Buffer.from(readFileSync8(file, "utf-8"), "utf-8") : null;
|
|
3776
|
+
const totalSize = textBuffer ? textBuffer.length : statSync3(servedPath).size;
|
|
3777
|
+
const bodyFor = (start, end) => textBuffer ? new Uint8Array(textBuffer.subarray(start, end + 1)) : (
|
|
3778
|
+
// Node's web stream type and the DOM one do not overlap for tsc on
|
|
3779
|
+
// every platform's lib set; the double cast is the documented bridge.
|
|
3780
|
+
Readable.toWeb(
|
|
3781
|
+
createReadStream(servedPath, { start, end })
|
|
3782
|
+
)
|
|
3783
|
+
);
|
|
3719
3784
|
const rangeHeader = c.req.header("Range");
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
headers: {
|
|
3730
|
-
...cacheHeaders,
|
|
3731
|
-
"Content-Type": servedContentType,
|
|
3732
|
-
"Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
|
|
3733
|
-
"Accept-Ranges": "bytes",
|
|
3734
|
-
"Content-Length": String(chunkSize)
|
|
3735
|
-
}
|
|
3785
|
+
const match = rangeHeader ? /bytes=(\d+)-(\d*)/.exec(rangeHeader) : null;
|
|
3786
|
+
if (match) {
|
|
3787
|
+
const start = parseInt(match[1], 10);
|
|
3788
|
+
const end = match[2] ? parseInt(match[2], 10) : totalSize - 1;
|
|
3789
|
+
const safeEnd = Math.min(end, totalSize - 1);
|
|
3790
|
+
if (start > safeEnd) {
|
|
3791
|
+
return new Response(null, {
|
|
3792
|
+
status: 416,
|
|
3793
|
+
headers: { ...cacheHeaders, "Content-Range": `bytes */${totalSize}` }
|
|
3736
3794
|
});
|
|
3737
3795
|
}
|
|
3796
|
+
return new Response(bodyFor(start, safeEnd), {
|
|
3797
|
+
status: 206,
|
|
3798
|
+
headers: {
|
|
3799
|
+
...cacheHeaders,
|
|
3800
|
+
"Content-Type": servedContentType,
|
|
3801
|
+
"Content-Range": `bytes ${start}-${safeEnd}/${totalSize}`,
|
|
3802
|
+
"Accept-Ranges": "bytes",
|
|
3803
|
+
"Content-Length": String(safeEnd - start + 1)
|
|
3804
|
+
}
|
|
3805
|
+
});
|
|
3738
3806
|
}
|
|
3739
|
-
return new Response(
|
|
3807
|
+
return new Response(totalSize > 0 ? bodyFor(0, totalSize - 1) : null, {
|
|
3740
3808
|
headers: {
|
|
3741
3809
|
...cacheHeaders,
|
|
3742
3810
|
"Content-Type": servedContentType,
|
|
@@ -4041,8 +4109,8 @@ import {
|
|
|
4041
4109
|
mkdirSync as mkdirSync5,
|
|
4042
4110
|
readFileSync as readFileSync11,
|
|
4043
4111
|
readdirSync as readdirSync6,
|
|
4044
|
-
renameSync as
|
|
4045
|
-
rmSync as
|
|
4112
|
+
renameSync as renameSync3,
|
|
4113
|
+
rmSync as rmSync4,
|
|
4046
4114
|
statSync as statSync5,
|
|
4047
4115
|
unlinkSync as unlinkSync4,
|
|
4048
4116
|
writeFileSync as writeFileSync6
|
|
@@ -4176,7 +4244,7 @@ function pruneThumbnailCache(cacheDir, protectedPaths, now = Date.now()) {
|
|
|
4176
4244
|
const retained = [];
|
|
4177
4245
|
for (const file of files) {
|
|
4178
4246
|
if (!protectedPaths.has(file.path) && now - file.mtimeMs > THUMBNAIL_CACHE_MAX_AGE_MS) {
|
|
4179
|
-
|
|
4247
|
+
rmSync4(file.path, { force: true });
|
|
4180
4248
|
} else {
|
|
4181
4249
|
retained.push(file);
|
|
4182
4250
|
}
|
|
@@ -4196,9 +4264,9 @@ function writeThumbnailAtomically(path, buffer) {
|
|
|
4196
4264
|
const temporaryPath = `${path}.${process.pid}.${randomUUID3()}.tmp`;
|
|
4197
4265
|
try {
|
|
4198
4266
|
writeFileSync6(temporaryPath, buffer, { flag: "wx" });
|
|
4199
|
-
|
|
4267
|
+
renameSync3(temporaryPath, path);
|
|
4200
4268
|
} finally {
|
|
4201
|
-
|
|
4269
|
+
rmSync4(temporaryPath, { force: true });
|
|
4202
4270
|
}
|
|
4203
4271
|
}
|
|
4204
4272
|
function registerThumbnailRoutes(api, adapter) {
|
|
@@ -4334,7 +4402,7 @@ function registerThumbnailRoutes(api, adapter) {
|
|
|
4334
4402
|
}
|
|
4335
4403
|
|
|
4336
4404
|
// src/routes/waveform.ts
|
|
4337
|
-
import { existsSync as existsSync9, readFileSync as readFileSync12,
|
|
4405
|
+
import { existsSync as existsSync9, readFileSync as readFileSync12, statSync as statSync6 } from "fs";
|
|
4338
4406
|
import { join as join12 } from "path";
|
|
4339
4407
|
function registerWaveformRoutes(api, adapter) {
|
|
4340
4408
|
api.get("/projects/:id/waveform/*", async (c) => {
|
|
@@ -4348,12 +4416,12 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4348
4416
|
if (!stats) return c.json({ error: "file not found" }, 404);
|
|
4349
4417
|
const cacheDir = join12(project.dir, ".waveform-cache");
|
|
4350
4418
|
const cachePath = join12(cacheDir, buildWaveformCacheKey(assetPath, stats));
|
|
4351
|
-
|
|
4352
|
-
|
|
4419
|
+
try {
|
|
4420
|
+
if (isWaveformCacheDirectory(cacheDir) && existsSync9(cachePath)) {
|
|
4353
4421
|
const peaks2 = JSON.parse(readFileSync12(cachePath, "utf-8"));
|
|
4354
4422
|
return c.json({ peaks: peaks2 });
|
|
4355
|
-
} catch {
|
|
4356
4423
|
}
|
|
4424
|
+
} catch {
|
|
4357
4425
|
}
|
|
4358
4426
|
let peaks;
|
|
4359
4427
|
try {
|
|
@@ -4362,8 +4430,7 @@ function registerWaveformRoutes(api, adapter) {
|
|
|
4362
4430
|
return c.json({ error: "failed to decode audio" }, 500);
|
|
4363
4431
|
}
|
|
4364
4432
|
try {
|
|
4365
|
-
|
|
4366
|
-
writeFileSync7(cachePath, JSON.stringify(peaks));
|
|
4433
|
+
writeWaveformCache(cachePath, peaks);
|
|
4367
4434
|
} catch {
|
|
4368
4435
|
}
|
|
4369
4436
|
return c.json({ peaks });
|
|
@@ -4634,8 +4701,8 @@ function registerSelectionRoutes(api, adapter) {
|
|
|
4634
4701
|
|
|
4635
4702
|
// src/routes/media.ts
|
|
4636
4703
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
4637
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
4638
|
-
import { basename as basename2, dirname as
|
|
4704
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
|
|
4705
|
+
import { basename as basename2, dirname as dirname4, extname as extname2, join as join13 } from "path";
|
|
4639
4706
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4640
4707
|
".mp4",
|
|
4641
4708
|
".mov",
|
|
@@ -4779,8 +4846,8 @@ function registerMediaRoutes(api, adapter, options = {}) {
|
|
|
4779
4846
|
return c.json({ error: "forbidden" }, 403);
|
|
4780
4847
|
}
|
|
4781
4848
|
}
|
|
4782
|
-
|
|
4783
|
-
if (backgroundOutputPath)
|
|
4849
|
+
mkdirSync6(dirname4(outputPath), { recursive: true });
|
|
4850
|
+
if (backgroundOutputPath) mkdirSync6(dirname4(backgroundOutputPath), { recursive: true });
|
|
4784
4851
|
const jobId = makeJobId(project.id, mediaJobs);
|
|
4785
4852
|
const state = adapter.startBackgroundRemoval({
|
|
4786
4853
|
project,
|