@holo-js/media 0.1.4 → 0.1.5
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.d.ts +906 -6001
- package/dist/index.mjs +263 -73
- package/package.json +7 -7
package/dist/index.mjs
CHANGED
|
@@ -76,6 +76,9 @@ function getContentSize(value) {
|
|
|
76
76
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
77
77
|
return value.byteLength;
|
|
78
78
|
}
|
|
79
|
+
if (value instanceof ArrayBuffer) {
|
|
80
|
+
return value.byteLength;
|
|
81
|
+
}
|
|
79
82
|
return value.size;
|
|
80
83
|
}
|
|
81
84
|
function isImageSource(mimeType, extension) {
|
|
@@ -269,7 +272,17 @@ function resolveMediaCollection(target, collectionName = "default") {
|
|
|
269
272
|
acceptedExtensions: Object.freeze([])
|
|
270
273
|
});
|
|
271
274
|
}
|
|
272
|
-
|
|
275
|
+
const collection2 = definition.collectionsByName[collectionName];
|
|
276
|
+
if (collection2) {
|
|
277
|
+
return collection2;
|
|
278
|
+
}
|
|
279
|
+
if (collectionName !== "default") {
|
|
280
|
+
const model = resolveDefinition(target);
|
|
281
|
+
throw new Error(
|
|
282
|
+
`[Holo Media] Unknown media collection "${collectionName}" for model "${model.name}".`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
return Object.freeze({
|
|
273
286
|
kind: "collection",
|
|
274
287
|
name: collectionName,
|
|
275
288
|
singleFile: false,
|
|
@@ -356,6 +369,7 @@ import { basename as basename2 } from "path";
|
|
|
356
369
|
import { readFile } from "fs/promises";
|
|
357
370
|
import { randomUUID } from "crypto";
|
|
358
371
|
import { Storage as Storage3 } from "@holo-js/storage/runtime";
|
|
372
|
+
import { connectionAsyncContext as connectionAsyncContext4 } from "@holo-js/db";
|
|
359
373
|
|
|
360
374
|
// src/queue.ts
|
|
361
375
|
import {
|
|
@@ -364,10 +378,59 @@ import {
|
|
|
364
378
|
getRegisteredQueueJob,
|
|
365
379
|
registerQueueJob
|
|
366
380
|
} from "@holo-js/queue";
|
|
367
|
-
import { connectionAsyncContext } from "@holo-js/db";
|
|
381
|
+
import { connectionAsyncContext as connectionAsyncContext2 } from "@holo-js/db";
|
|
368
382
|
|
|
369
383
|
// src/model/conversions.ts
|
|
370
384
|
import { Storage } from "@holo-js/storage/runtime";
|
|
385
|
+
import { connectionAsyncContext } from "@holo-js/db";
|
|
386
|
+
function getActiveTransaction() {
|
|
387
|
+
const active = connectionAsyncContext.getActive()?.connection;
|
|
388
|
+
if (!active || active.getScope().kind === "root") {
|
|
389
|
+
return void 0;
|
|
390
|
+
}
|
|
391
|
+
return active;
|
|
392
|
+
}
|
|
393
|
+
function registerStorageWriteRollback(snapshot) {
|
|
394
|
+
const active = getActiveTransaction();
|
|
395
|
+
if (!active) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
active.afterRollback(async () => {
|
|
399
|
+
await restoreStoredFileSnapshot(snapshot);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
async function restoreStoredFileSnapshot(snapshot) {
|
|
403
|
+
if (snapshot.contents) {
|
|
404
|
+
await Storage.disk(snapshot.disk).put(snapshot.path, snapshot.contents);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
await Storage.disk(snapshot.disk).delete(snapshot.path);
|
|
408
|
+
}
|
|
409
|
+
async function restoreStoredFileSnapshots(snapshots) {
|
|
410
|
+
for (const snapshot of [...snapshots].reverse()) {
|
|
411
|
+
await restoreStoredFileSnapshot(snapshot).catch(() => void 0);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function putFileWithRollbackRestore(disk, path, contents) {
|
|
415
|
+
const previous = await Storage.disk(disk).getBytes(path);
|
|
416
|
+
await Storage.disk(disk).put(path, contents);
|
|
417
|
+
const snapshot = {
|
|
418
|
+
disk,
|
|
419
|
+
path,
|
|
420
|
+
contents: previous
|
|
421
|
+
};
|
|
422
|
+
registerStorageWriteRollback(snapshot);
|
|
423
|
+
return snapshot;
|
|
424
|
+
}
|
|
425
|
+
async function deleteFileWithRollbackRestore(disk, path) {
|
|
426
|
+
const previous = await Storage.disk(disk).getBytes(path);
|
|
427
|
+
await Storage.disk(disk).delete(path);
|
|
428
|
+
registerStorageWriteRollback({
|
|
429
|
+
disk,
|
|
430
|
+
path,
|
|
431
|
+
contents: previous
|
|
432
|
+
});
|
|
433
|
+
}
|
|
371
434
|
function fallbackCollectionDefinition(collectionName) {
|
|
372
435
|
return Object.freeze({
|
|
373
436
|
kind: "collection",
|
|
@@ -447,6 +510,7 @@ async function generateStoredConversions(options) {
|
|
|
447
510
|
}
|
|
448
511
|
const generatedConversions = /* @__PURE__ */ Object.create(null);
|
|
449
512
|
const executor = getMediaConversionExecutor();
|
|
513
|
+
const writtenSnapshots = [];
|
|
450
514
|
try {
|
|
451
515
|
for (const conversion2 of matchingConversions) {
|
|
452
516
|
const generated = await executor.generate({
|
|
@@ -469,7 +533,7 @@ async function generateStoredConversions(options) {
|
|
|
469
533
|
const targetDisk = generated.disk ?? options.conversionsDisk;
|
|
470
534
|
const conversionMimeType = inferMimeType(generatedFileName, generated.mimeType);
|
|
471
535
|
const conversionContents = await toBinaryContent(generated.contents);
|
|
472
|
-
await
|
|
536
|
+
writtenSnapshots.push(await putFileWithRollbackRestore(targetDisk, conversionPath, conversionContents));
|
|
473
537
|
generatedConversions[conversion2.name] = Object.freeze({
|
|
474
538
|
path: conversionPath,
|
|
475
539
|
disk: targetDisk,
|
|
@@ -479,19 +543,11 @@ async function generateStoredConversions(options) {
|
|
|
479
543
|
});
|
|
480
544
|
}
|
|
481
545
|
} catch (error) {
|
|
482
|
-
await
|
|
483
|
-
generatedConversions,
|
|
484
|
-
options.conversionsDisk
|
|
485
|
-
);
|
|
546
|
+
await restoreStoredFileSnapshots(writtenSnapshots);
|
|
486
547
|
throw error;
|
|
487
548
|
}
|
|
488
549
|
return Object.freeze(generatedConversions);
|
|
489
550
|
}
|
|
490
|
-
async function deleteStoredConversions(conversions, _fallbackDisk) {
|
|
491
|
-
for (const conversion2 of Object.values(conversions)) {
|
|
492
|
-
await Storage.disk(conversion2.disk).delete(conversion2.path);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
551
|
async function deleteObsoleteConversions(current, next, fallbackDisk, requested) {
|
|
496
552
|
for (const [name, conversion2] of Object.entries(current)) {
|
|
497
553
|
if (requested?.length && !requested.includes(name)) {
|
|
@@ -504,7 +560,7 @@ async function deleteObsoleteConversions(current, next, fallbackDisk, requested)
|
|
|
504
560
|
if (nextConversion?.path === conversion2.path && nextConversion.disk === (conversion2.disk ?? fallbackDisk)) {
|
|
505
561
|
continue;
|
|
506
562
|
}
|
|
507
|
-
await
|
|
563
|
+
await deleteFileWithRollbackRestore(conversion2.disk ?? fallbackDisk, conversion2.path);
|
|
508
564
|
}
|
|
509
565
|
}
|
|
510
566
|
async function regenerateMediaEntityConversions(options) {
|
|
@@ -588,22 +644,30 @@ function normalizePayload(payload) {
|
|
|
588
644
|
}
|
|
589
645
|
async function runMediaGenerateConversionsJob(payload) {
|
|
590
646
|
const normalized = normalizePayload(payload);
|
|
591
|
-
|
|
592
|
-
if (!media) {
|
|
647
|
+
if (normalized.conversionNames.length === 0) {
|
|
593
648
|
return Object.freeze({
|
|
594
|
-
status: "
|
|
595
|
-
conversionNames:
|
|
649
|
+
status: "processed",
|
|
650
|
+
conversionNames: normalized.conversionNames
|
|
596
651
|
});
|
|
597
652
|
}
|
|
598
|
-
await
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
653
|
+
const media = typeof normalized.mediaId === "number" ? await Media.find(normalized.mediaId) : await Media.where("id", normalized.mediaId).first();
|
|
654
|
+
switch (media) {
|
|
655
|
+
case void 0:
|
|
656
|
+
return Object.freeze({
|
|
657
|
+
status: "missing-media",
|
|
658
|
+
conversionNames: Object.freeze([])
|
|
659
|
+
});
|
|
660
|
+
default:
|
|
661
|
+
await regenerateMediaEntityConversions({
|
|
662
|
+
media,
|
|
663
|
+
conversions: normalized.conversionNames,
|
|
664
|
+
includeQueued: true
|
|
665
|
+
});
|
|
666
|
+
return Object.freeze({
|
|
667
|
+
status: "processed",
|
|
668
|
+
conversionNames: normalized.conversionNames
|
|
669
|
+
});
|
|
670
|
+
}
|
|
607
671
|
}
|
|
608
672
|
function ensureMediaQueueJobRegistered() {
|
|
609
673
|
if (getRegisteredQueueJob(MEDIA_GENERATE_CONVERSIONS_JOB)) {
|
|
@@ -618,8 +682,8 @@ function ensureMediaQueueJobRegistered() {
|
|
|
618
682
|
name: MEDIA_GENERATE_CONVERSIONS_JOB
|
|
619
683
|
});
|
|
620
684
|
}
|
|
621
|
-
function
|
|
622
|
-
const active =
|
|
685
|
+
function getActiveTransaction2() {
|
|
686
|
+
const active = connectionAsyncContext2.getActive()?.connection;
|
|
623
687
|
if (!active || active.getScope().kind === "root") {
|
|
624
688
|
return void 0;
|
|
625
689
|
}
|
|
@@ -637,7 +701,7 @@ async function dispatchQueuedMediaConversions(payload) {
|
|
|
637
701
|
return;
|
|
638
702
|
}
|
|
639
703
|
ensureMediaQueueJobRegistered();
|
|
640
|
-
const activeTransaction =
|
|
704
|
+
const activeTransaction = getActiveTransaction2();
|
|
641
705
|
if (activeTransaction) {
|
|
642
706
|
activeTransaction.afterCommit(() => executeQueuedMediaDispatch(normalized));
|
|
643
707
|
return;
|
|
@@ -650,7 +714,7 @@ async function dispatchQueuedMediaConversionsForModel(payload, onSynchronousDisp
|
|
|
650
714
|
return;
|
|
651
715
|
}
|
|
652
716
|
ensureMediaQueueJobRegistered();
|
|
653
|
-
const activeTransaction =
|
|
717
|
+
const activeTransaction = getActiveTransaction2();
|
|
654
718
|
if (activeTransaction) {
|
|
655
719
|
activeTransaction.afterCommit(() => executeQueuedMediaDispatch(normalized, onSynchronousDispatch));
|
|
656
720
|
return;
|
|
@@ -660,6 +724,29 @@ async function dispatchQueuedMediaConversionsForModel(payload, onSynchronousDisp
|
|
|
660
724
|
|
|
661
725
|
// src/model/item.ts
|
|
662
726
|
import { Storage as Storage2 } from "@holo-js/storage/runtime";
|
|
727
|
+
import { connectionAsyncContext as connectionAsyncContext3 } from "@holo-js/db";
|
|
728
|
+
function toUrlPath(url) {
|
|
729
|
+
if (url.startsWith("/")) {
|
|
730
|
+
return url;
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const parsed = new URL(url);
|
|
734
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
735
|
+
} catch {
|
|
736
|
+
return `/${url.replace(/^\/+/, "")}`;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function registerDeletedVariantRollbackRestore(snapshots) {
|
|
740
|
+
const active = connectionAsyncContext3.getActive()?.connection;
|
|
741
|
+
if (!active || active.getScope().kind === "root") {
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
active.afterRollback(async () => {
|
|
745
|
+
for (const snapshot of [...snapshots].reverse()) {
|
|
746
|
+
await Storage2.disk(snapshot.disk).put(snapshot.path, snapshot.contents).catch(() => void 0);
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
663
750
|
var MediaItem = class {
|
|
664
751
|
constructor(entity, owner) {
|
|
665
752
|
this.entity = entity;
|
|
@@ -690,15 +777,8 @@ var MediaItem = class {
|
|
|
690
777
|
);
|
|
691
778
|
}
|
|
692
779
|
getPath(conversion2) {
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
return null;
|
|
696
|
-
}
|
|
697
|
-
try {
|
|
698
|
-
return Storage2.disk(variant.disk).path(variant.path);
|
|
699
|
-
} catch {
|
|
700
|
-
return null;
|
|
701
|
-
}
|
|
780
|
+
const url = this.getUrl(conversion2);
|
|
781
|
+
return url ? toUrlPath(url) : null;
|
|
702
782
|
}
|
|
703
783
|
getUrl(conversion2) {
|
|
704
784
|
const variant = this.resolveVariant(conversion2);
|
|
@@ -737,6 +817,7 @@ var MediaItem = class {
|
|
|
737
817
|
}
|
|
738
818
|
}
|
|
739
819
|
await this.entity.delete();
|
|
820
|
+
registerDeletedVariantRollbackRestore(deletedSnapshots);
|
|
740
821
|
this.owner?.forgetRelation("media");
|
|
741
822
|
} catch (error) {
|
|
742
823
|
for (const snapshot of deletedSnapshots.reverse()) {
|
|
@@ -806,6 +887,15 @@ var MediaItem = class {
|
|
|
806
887
|
};
|
|
807
888
|
|
|
808
889
|
// src/model/adder.ts
|
|
890
|
+
var MediaAddValidationException = class extends Error {
|
|
891
|
+
error;
|
|
892
|
+
constructor(details) {
|
|
893
|
+
const error = createMediaAddError(details);
|
|
894
|
+
super(error.message);
|
|
895
|
+
this.name = "MediaAddValidationException";
|
|
896
|
+
this.error = error;
|
|
897
|
+
}
|
|
898
|
+
};
|
|
809
899
|
function resolveImplicitDiskName() {
|
|
810
900
|
const defaultDisk = Storage3.disk();
|
|
811
901
|
if (defaultDisk.visibility === "public") {
|
|
@@ -828,10 +918,44 @@ function parseRemoteFileName(url) {
|
|
|
828
918
|
return "media.bin";
|
|
829
919
|
}
|
|
830
920
|
}
|
|
831
|
-
function
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
921
|
+
function formatBytes(bytes) {
|
|
922
|
+
if (!Number.isFinite(bytes) || bytes < 0) {
|
|
923
|
+
return "0 bytes";
|
|
924
|
+
}
|
|
925
|
+
const units = ["bytes", "KB", "MB", "GB"];
|
|
926
|
+
let value = bytes;
|
|
927
|
+
let unitIndex = 0;
|
|
928
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
929
|
+
value /= 1024;
|
|
930
|
+
unitIndex += 1;
|
|
931
|
+
}
|
|
932
|
+
const formatted = unitIndex === 0 ? String(Math.round(value)) : value.toFixed(value >= 10 ? 0 : 1).replace(/\.0$/, "");
|
|
933
|
+
return `${formatted} ${units[unitIndex]}`;
|
|
934
|
+
}
|
|
935
|
+
function createMediaValidationMessage(details) {
|
|
936
|
+
if (details.code === "max_size_exceeded") {
|
|
937
|
+
return `The selected file must be ${formatBytes(details.maxSize)} or smaller.`;
|
|
938
|
+
}
|
|
939
|
+
if (details.code === "invalid_mime_type") {
|
|
940
|
+
return `The selected file must be one of these types: ${details.acceptedMimeTypes.join(", ")}.`;
|
|
941
|
+
}
|
|
942
|
+
return `The selected file must use one of these extensions: ${details.acceptedExtensions.join(", ")}.`;
|
|
943
|
+
}
|
|
944
|
+
function createMediaAddError(details) {
|
|
945
|
+
return Object.freeze({
|
|
946
|
+
...details,
|
|
947
|
+
status: 422,
|
|
948
|
+
message: createMediaValidationMessage(details)
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
function createMaxSizeError(fileName, collectionName, maxSize, actualSize) {
|
|
952
|
+
return new MediaAddValidationException({
|
|
953
|
+
code: "max_size_exceeded",
|
|
954
|
+
collection: collectionName,
|
|
955
|
+
fileName,
|
|
956
|
+
maxSize,
|
|
957
|
+
actualSize
|
|
958
|
+
});
|
|
835
959
|
}
|
|
836
960
|
async function readRemoteMediaContents(response, fileName, maxSize, collectionName) {
|
|
837
961
|
if (typeof maxSize !== "number" || !response.body) {
|
|
@@ -852,7 +976,7 @@ async function readRemoteMediaContents(response, fileName, maxSize, collectionNa
|
|
|
852
976
|
totalSize += value.byteLength;
|
|
853
977
|
if (totalSize > maxSize) {
|
|
854
978
|
await reader.cancel().catch(() => void 0);
|
|
855
|
-
throw createMaxSizeError(fileName, collectionName);
|
|
979
|
+
throw createMaxSizeError(fileName, collectionName ?? "default", maxSize, totalSize);
|
|
856
980
|
}
|
|
857
981
|
chunks.push(value);
|
|
858
982
|
}
|
|
@@ -912,16 +1036,24 @@ async function resolveMediaSource(input, overrideFileName, overrideName, options
|
|
|
912
1036
|
};
|
|
913
1037
|
}
|
|
914
1038
|
const contents = await toBinaryContent(input);
|
|
915
|
-
const fileName = sanitizeFileName(overrideFileName ??
|
|
1039
|
+
const fileName = sanitizeFileName(overrideFileName ?? getBinaryFileName(input));
|
|
916
1040
|
return {
|
|
917
1041
|
contents,
|
|
918
1042
|
fileName,
|
|
919
|
-
mimeType: inferMimeType(fileName),
|
|
1043
|
+
mimeType: inferMimeType(fileName, getBinaryMimeType(input)),
|
|
920
1044
|
extension: getExtension(fileName),
|
|
921
1045
|
size: getContentSize(contents),
|
|
922
1046
|
name: getDisplayName(fileName, overrideName)
|
|
923
1047
|
};
|
|
924
1048
|
}
|
|
1049
|
+
function getBinaryFileName(input) {
|
|
1050
|
+
const name = input.name;
|
|
1051
|
+
return typeof name === "string" && name.trim() ? name : "media.bin";
|
|
1052
|
+
}
|
|
1053
|
+
function getBinaryMimeType(input) {
|
|
1054
|
+
const type = input.type;
|
|
1055
|
+
return typeof type === "string" && type.trim() ? type : void 0;
|
|
1056
|
+
}
|
|
925
1057
|
function isPathInput(input) {
|
|
926
1058
|
return typeof input === "object" && input !== null && "path" in input;
|
|
927
1059
|
}
|
|
@@ -942,7 +1074,7 @@ async function resolveRemoteMediaSource(input, overrideFileName, overrideName, m
|
|
|
942
1074
|
const contentLengthHeader = response.headers.get("content-length");
|
|
943
1075
|
const contentLength = contentLengthHeader?.trim() ? Number(contentLengthHeader) : Number.NaN;
|
|
944
1076
|
if (typeof maxSize === "number" && Number.isFinite(contentLength) && contentLength > maxSize) {
|
|
945
|
-
throw createMaxSizeError(fileName, collectionName);
|
|
1077
|
+
throw createMaxSizeError(fileName, collectionName ?? "default", maxSize, contentLength);
|
|
946
1078
|
}
|
|
947
1079
|
const contents = await readRemoteMediaContents(
|
|
948
1080
|
response,
|
|
@@ -961,23 +1093,29 @@ async function resolveRemoteMediaSource(input, overrideFileName, overrideName, m
|
|
|
961
1093
|
}
|
|
962
1094
|
function validateSource(collection2, source) {
|
|
963
1095
|
if (typeof collection2.maxSize === "number" && source.size > collection2.maxSize) {
|
|
964
|
-
throw
|
|
965
|
-
`[Holo Media] File "${source.fileName}" exceeds the max size for collection "${collection2.name}".`
|
|
966
|
-
);
|
|
1096
|
+
throw createMaxSizeError(source.fileName, collection2.name, collection2.maxSize, source.size);
|
|
967
1097
|
}
|
|
968
1098
|
if (collection2.acceptedMimeTypes.length > 0) {
|
|
969
1099
|
const mimeType = source.mimeType?.trim().toLowerCase();
|
|
970
1100
|
if (!mimeType || !collection2.acceptedMimeTypes.includes(mimeType)) {
|
|
971
|
-
throw new
|
|
972
|
-
|
|
973
|
-
|
|
1101
|
+
throw new MediaAddValidationException({
|
|
1102
|
+
code: "invalid_mime_type",
|
|
1103
|
+
collection: collection2.name,
|
|
1104
|
+
fileName: source.fileName,
|
|
1105
|
+
mimeType,
|
|
1106
|
+
acceptedMimeTypes: collection2.acceptedMimeTypes
|
|
1107
|
+
});
|
|
974
1108
|
}
|
|
975
1109
|
}
|
|
976
1110
|
if (collection2.acceptedExtensions.length > 0) {
|
|
977
1111
|
if (!source.extension || !collection2.acceptedExtensions.includes(source.extension)) {
|
|
978
|
-
throw new
|
|
979
|
-
|
|
980
|
-
|
|
1112
|
+
throw new MediaAddValidationException({
|
|
1113
|
+
code: "invalid_extension",
|
|
1114
|
+
collection: collection2.name,
|
|
1115
|
+
fileName: source.fileName,
|
|
1116
|
+
extension: source.extension,
|
|
1117
|
+
acceptedExtensions: collection2.acceptedExtensions
|
|
1118
|
+
});
|
|
981
1119
|
}
|
|
982
1120
|
}
|
|
983
1121
|
}
|
|
@@ -994,23 +1132,29 @@ async function deleteMediaItemsWithRollback(items) {
|
|
|
994
1132
|
deletedSnapshots.push(snapshot);
|
|
995
1133
|
}
|
|
996
1134
|
} catch (error) {
|
|
997
|
-
|
|
998
|
-
await restoreDeletedMediaSnapshot(snapshot).catch(() => void 0);
|
|
999
|
-
}
|
|
1135
|
+
await restoreDeletedMediaSnapshots(deletedSnapshots);
|
|
1000
1136
|
throw error;
|
|
1001
1137
|
}
|
|
1138
|
+
return deletedSnapshots;
|
|
1002
1139
|
}
|
|
1003
1140
|
async function deleteOverflowItems(items, limit) {
|
|
1004
1141
|
if (items.length <= limit) {
|
|
1005
|
-
return;
|
|
1142
|
+
return [];
|
|
1006
1143
|
}
|
|
1007
|
-
await deleteMediaItemsWithRollback(items.slice(0, items.length - limit));
|
|
1144
|
+
return await deleteMediaItemsWithRollback(items.slice(0, items.length - limit));
|
|
1008
1145
|
}
|
|
1009
1146
|
async function cleanupGeneratedConversions(conversions, _fallbackDisk) {
|
|
1010
1147
|
for (const conversion2 of Object.values(conversions)) {
|
|
1011
1148
|
await Storage3.disk(conversion2.disk).delete(conversion2.path).catch(() => void 0);
|
|
1012
1149
|
}
|
|
1013
1150
|
}
|
|
1151
|
+
function registerCreatedMediaRollbackCleanup(cleanup) {
|
|
1152
|
+
const active = connectionAsyncContext4.getActive()?.connection;
|
|
1153
|
+
if (!active || active.getScope().kind === "root") {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
active.afterRollback(cleanup);
|
|
1157
|
+
}
|
|
1014
1158
|
async function snapshotDeletedMediaItem(item) {
|
|
1015
1159
|
const record = item.record;
|
|
1016
1160
|
const fileTargets = [{
|
|
@@ -1060,6 +1204,11 @@ async function restoreDeletedMediaSnapshot(snapshot) {
|
|
|
1060
1204
|
}
|
|
1061
1205
|
await Media.create(snapshot.record);
|
|
1062
1206
|
}
|
|
1207
|
+
async function restoreDeletedMediaSnapshots(snapshots) {
|
|
1208
|
+
for (const snapshot of snapshots.reverse()) {
|
|
1209
|
+
await restoreDeletedMediaSnapshot(snapshot).catch(() => void 0);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1063
1212
|
var MediaAdder = class _MediaAdder {
|
|
1064
1213
|
constructor(entity, source, state = {}) {
|
|
1065
1214
|
this.entity = entity;
|
|
@@ -1085,6 +1234,23 @@ var MediaAdder = class _MediaAdder {
|
|
|
1085
1234
|
});
|
|
1086
1235
|
}
|
|
1087
1236
|
async toMediaCollection(collectionName = "default") {
|
|
1237
|
+
try {
|
|
1238
|
+
const media = await this.storeInMediaCollection(collectionName);
|
|
1239
|
+
return Object.assign(media, {
|
|
1240
|
+
data: media,
|
|
1241
|
+
error: null
|
|
1242
|
+
});
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
if (error instanceof MediaAddValidationException) {
|
|
1245
|
+
return {
|
|
1246
|
+
data: null,
|
|
1247
|
+
error: error.error
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async storeInMediaCollection(collectionName = "default") {
|
|
1088
1254
|
const mediaDefinition = requireMediaDefinition(this.entity);
|
|
1089
1255
|
const ownerDefinition = this.entity.getRepository().definition;
|
|
1090
1256
|
const ownerId = this.entity.get(ownerDefinition.primaryKey);
|
|
@@ -1117,9 +1283,14 @@ var MediaAdder = class _MediaAdder {
|
|
|
1117
1283
|
let originalStored = false;
|
|
1118
1284
|
let createdMedia;
|
|
1119
1285
|
let generatedConversions = Object.freeze({});
|
|
1286
|
+
const deletedMediaSnapshots = [];
|
|
1120
1287
|
try {
|
|
1121
1288
|
await Storage3.disk(diskName).put(originalPath, source.contents);
|
|
1122
1289
|
originalStored = true;
|
|
1290
|
+
registerCreatedMediaRollbackCleanup(async () => {
|
|
1291
|
+
await cleanupGeneratedConversions(generatedConversions, conversionsDisk).catch(() => void 0);
|
|
1292
|
+
await Storage3.disk(diskName).delete(originalPath).catch(() => void 0);
|
|
1293
|
+
});
|
|
1123
1294
|
generatedConversions = await generateStoredConversions({
|
|
1124
1295
|
definition: mediaDefinition,
|
|
1125
1296
|
collection: collection2,
|
|
@@ -1155,10 +1326,10 @@ var MediaAdder = class _MediaAdder {
|
|
|
1155
1326
|
});
|
|
1156
1327
|
createdMedia = media;
|
|
1157
1328
|
this.entity.forgetRelation("media");
|
|
1158
|
-
await deleteMediaItemsWithRollback(existing);
|
|
1329
|
+
deletedMediaSnapshots.push(...await deleteMediaItemsWithRollback(existing));
|
|
1159
1330
|
if (typeof collection2.onlyKeepLatest === "number") {
|
|
1160
1331
|
const items = await this.entity.getMedia(collectionName);
|
|
1161
|
-
await deleteOverflowItems(items, collection2.onlyKeepLatest);
|
|
1332
|
+
deletedMediaSnapshots.push(...await deleteOverflowItems(items, collection2.onlyKeepLatest));
|
|
1162
1333
|
}
|
|
1163
1334
|
const queuedConversions = resolveQueuedConversionNames({
|
|
1164
1335
|
definition: mediaDefinition,
|
|
@@ -1180,6 +1351,7 @@ var MediaAdder = class _MediaAdder {
|
|
|
1180
1351
|
await createdMedia.refresh().catch(() => void 0);
|
|
1181
1352
|
await createdMedia.delete().catch(() => void 0);
|
|
1182
1353
|
}
|
|
1354
|
+
await restoreDeletedMediaSnapshots(deletedMediaSnapshots);
|
|
1183
1355
|
this.entity.forgetRelation("media");
|
|
1184
1356
|
throw error;
|
|
1185
1357
|
}
|
|
@@ -1286,6 +1458,12 @@ function installEntityMediaMethods() {
|
|
|
1286
1458
|
import { morphMany } from "@holo-js/db";
|
|
1287
1459
|
|
|
1288
1460
|
// src/definitions/collections.ts
|
|
1461
|
+
function normalizePositiveInteger(value, fieldName) {
|
|
1462
|
+
if (!Number.isFinite(value)) {
|
|
1463
|
+
throw new Error(`[Holo Media] Media collection ${fieldName} must be a finite number.`);
|
|
1464
|
+
}
|
|
1465
|
+
return Math.max(1, Math.floor(value));
|
|
1466
|
+
}
|
|
1289
1467
|
function decorateCollection(definition) {
|
|
1290
1468
|
const clone = (overrides) => decorateCollection(Object.freeze({
|
|
1291
1469
|
...definition,
|
|
@@ -1306,7 +1484,7 @@ function decorateCollection(definition) {
|
|
|
1306
1484
|
},
|
|
1307
1485
|
onlyKeepLatest(limit) {
|
|
1308
1486
|
return clone({
|
|
1309
|
-
onlyKeepLatest:
|
|
1487
|
+
onlyKeepLatest: normalizePositiveInteger(limit, "onlyKeepLatest")
|
|
1310
1488
|
});
|
|
1311
1489
|
},
|
|
1312
1490
|
acceptsMimeTypes(mimeTypes) {
|
|
@@ -1325,7 +1503,7 @@ function decorateCollection(definition) {
|
|
|
1325
1503
|
},
|
|
1326
1504
|
maxSize(bytes) {
|
|
1327
1505
|
return clone({
|
|
1328
|
-
maxFileSize:
|
|
1506
|
+
maxFileSize: normalizePositiveInteger(bytes, "maxFileSize")
|
|
1329
1507
|
});
|
|
1330
1508
|
}
|
|
1331
1509
|
});
|
|
@@ -1346,19 +1524,31 @@ function normalizeCollectionDefinitions(definitions) {
|
|
|
1346
1524
|
disk: definition.diskName?.trim() || void 0,
|
|
1347
1525
|
conversionsDisk: definition.conversionsDiskName?.trim() || void 0,
|
|
1348
1526
|
singleFile: Boolean(definition.singleFile),
|
|
1349
|
-
onlyKeepLatest: typeof definition.onlyKeepLatest === "number" ?
|
|
1527
|
+
onlyKeepLatest: typeof definition.onlyKeepLatest === "number" ? normalizePositiveInteger(definition.onlyKeepLatest, "onlyKeepLatest") : void 0,
|
|
1350
1528
|
acceptedMimeTypes: Object.freeze(
|
|
1351
1529
|
definition.acceptedMimeTypes.map((mimeType) => mimeType.trim()).map((mimeType) => mimeType.toLowerCase()).filter(Boolean)
|
|
1352
1530
|
),
|
|
1353
1531
|
acceptedExtensions: Object.freeze(
|
|
1354
1532
|
definition.acceptedExtensions.map((extension) => extension.trim().replace(/^\./, "").toLowerCase()).filter(Boolean)
|
|
1355
1533
|
),
|
|
1356
|
-
maxSize: typeof definition.maxFileSize === "number" ?
|
|
1534
|
+
maxSize: typeof definition.maxFileSize === "number" ? normalizePositiveInteger(definition.maxFileSize, "maxFileSize") : void 0
|
|
1357
1535
|
});
|
|
1358
1536
|
}));
|
|
1359
1537
|
}
|
|
1360
1538
|
|
|
1361
1539
|
// src/definitions/conversions.ts
|
|
1540
|
+
function normalizePositiveInteger2(value, fieldName) {
|
|
1541
|
+
if (!Number.isFinite(value)) {
|
|
1542
|
+
throw new Error(`[Holo Media] Media conversion ${fieldName} must be a finite number.`);
|
|
1543
|
+
}
|
|
1544
|
+
return Math.max(1, Math.floor(value));
|
|
1545
|
+
}
|
|
1546
|
+
function normalizeQuality(value) {
|
|
1547
|
+
if (!Number.isFinite(value)) {
|
|
1548
|
+
throw new Error("[Holo Media] Media conversion quality must be a finite number.");
|
|
1549
|
+
}
|
|
1550
|
+
return Math.max(1, Math.min(100, Math.floor(value)));
|
|
1551
|
+
}
|
|
1362
1552
|
function decorateConversion(definition) {
|
|
1363
1553
|
const clone = (overrides) => decorateConversion(Object.freeze({
|
|
1364
1554
|
...definition,
|
|
@@ -1377,12 +1567,12 @@ function decorateConversion(definition) {
|
|
|
1377
1567
|
},
|
|
1378
1568
|
width(pixels) {
|
|
1379
1569
|
return clone({
|
|
1380
|
-
width:
|
|
1570
|
+
width: normalizePositiveInteger2(pixels, "width")
|
|
1381
1571
|
});
|
|
1382
1572
|
},
|
|
1383
1573
|
height(pixels) {
|
|
1384
1574
|
return clone({
|
|
1385
|
-
height:
|
|
1575
|
+
height: normalizePositiveInteger2(pixels, "height")
|
|
1386
1576
|
});
|
|
1387
1577
|
},
|
|
1388
1578
|
fit(mode) {
|
|
@@ -1393,7 +1583,7 @@ function decorateConversion(definition) {
|
|
|
1393
1583
|
},
|
|
1394
1584
|
quality(value) {
|
|
1395
1585
|
return clone({
|
|
1396
|
-
quality:
|
|
1586
|
+
quality: normalizeQuality(value)
|
|
1397
1587
|
});
|
|
1398
1588
|
},
|
|
1399
1589
|
queued() {
|
|
@@ -1419,9 +1609,9 @@ function normalizeConversionDefinitions(definitions) {
|
|
|
1419
1609
|
collections: Object.freeze(
|
|
1420
1610
|
definition.collections.filter(Boolean)
|
|
1421
1611
|
),
|
|
1422
|
-
width: typeof definition.width === "number" ?
|
|
1423
|
-
height: typeof definition.height === "number" ?
|
|
1424
|
-
quality: typeof definition.quality === "number" ?
|
|
1612
|
+
width: typeof definition.width === "number" ? normalizePositiveInteger2(definition.width, "width") : void 0,
|
|
1613
|
+
height: typeof definition.height === "number" ? normalizePositiveInteger2(definition.height, "height") : void 0,
|
|
1614
|
+
quality: typeof definition.quality === "number" ? normalizeQuality(definition.quality) : void 0,
|
|
1425
1615
|
queued: Boolean(definition.queued)
|
|
1426
1616
|
});
|
|
1427
1617
|
}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holo-js/media",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Holo-JS Framework - media collections and conversions for Holo-JS models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,20 +16,20 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "
|
|
20
|
-
"stub": "
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"stub": "tsup",
|
|
21
21
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
22
22
|
"test": "vitest --run"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@holo-js/db": "^0.1.
|
|
26
|
-
"@holo-js/queue": "^0.1.
|
|
27
|
-
"@holo-js/storage": "^0.1.
|
|
25
|
+
"@holo-js/db": "^0.1.5",
|
|
26
|
+
"@holo-js/queue": "^0.1.5",
|
|
27
|
+
"@holo-js/storage": "^0.1.5",
|
|
28
28
|
"sharp": "^0.34.4"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"tsup": "^8.3.5",
|
|
32
32
|
"typescript": "^5.7.2",
|
|
33
|
-
"vitest": "^
|
|
33
|
+
"vitest": "^4.1.5"
|
|
34
34
|
}
|
|
35
35
|
}
|