@holo-js/media 0.1.0
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 +8214 -0
- package/dist/index.mjs +1520 -0
- package/package.json +35 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1520 @@
|
|
|
1
|
+
// src/model/entity.ts
|
|
2
|
+
import { Entity as Entity2 } from "@holo-js/db";
|
|
3
|
+
|
|
4
|
+
// src/registry.ts
|
|
5
|
+
import { Entity } from "@holo-js/db";
|
|
6
|
+
|
|
7
|
+
// src/runtime/binary.ts
|
|
8
|
+
import { basename, extname } from "path";
|
|
9
|
+
var MIME_TYPES = Object.freeze({
|
|
10
|
+
avif: "image/avif",
|
|
11
|
+
gif: "image/gif",
|
|
12
|
+
jpeg: "image/jpeg",
|
|
13
|
+
jpg: "image/jpeg",
|
|
14
|
+
json: "application/json",
|
|
15
|
+
pdf: "application/pdf",
|
|
16
|
+
png: "image/png",
|
|
17
|
+
svg: "image/svg+xml",
|
|
18
|
+
txt: "text/plain",
|
|
19
|
+
webp: "image/webp"
|
|
20
|
+
});
|
|
21
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
22
|
+
"avif",
|
|
23
|
+
"gif",
|
|
24
|
+
"heic",
|
|
25
|
+
"heif",
|
|
26
|
+
"jpeg",
|
|
27
|
+
"jpg",
|
|
28
|
+
"png",
|
|
29
|
+
"svg",
|
|
30
|
+
"tif",
|
|
31
|
+
"tiff",
|
|
32
|
+
"webp"
|
|
33
|
+
]);
|
|
34
|
+
var SHARP_OUTPUT_FORMATS = /* @__PURE__ */ new Set([
|
|
35
|
+
"avif",
|
|
36
|
+
"jpeg",
|
|
37
|
+
"jpg",
|
|
38
|
+
"png",
|
|
39
|
+
"webp"
|
|
40
|
+
]);
|
|
41
|
+
function sanitizeFileName(value) {
|
|
42
|
+
const sanitized = value.trim().replace(/\\/g, "/").split("/").filter(Boolean).join("-");
|
|
43
|
+
return sanitized || "media.bin";
|
|
44
|
+
}
|
|
45
|
+
function inferMimeType(fileName, explicit) {
|
|
46
|
+
if (explicit?.trim()) {
|
|
47
|
+
return explicit.trim();
|
|
48
|
+
}
|
|
49
|
+
const extension = extname(fileName).replace(/^\./, "").toLowerCase();
|
|
50
|
+
return MIME_TYPES[extension];
|
|
51
|
+
}
|
|
52
|
+
function getExtension(fileName) {
|
|
53
|
+
const extension = extname(fileName).replace(/^\./, "").toLowerCase();
|
|
54
|
+
return extension || void 0;
|
|
55
|
+
}
|
|
56
|
+
function getDisplayName(fileName, explicit) {
|
|
57
|
+
if (explicit?.trim()) {
|
|
58
|
+
return explicit.trim();
|
|
59
|
+
}
|
|
60
|
+
return basename(fileName, extname(fileName));
|
|
61
|
+
}
|
|
62
|
+
async function toBinaryContent(value) {
|
|
63
|
+
if (value instanceof Blob) {
|
|
64
|
+
return new Uint8Array(await value.arrayBuffer());
|
|
65
|
+
}
|
|
66
|
+
if (value instanceof ArrayBuffer) {
|
|
67
|
+
return new Uint8Array(value);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
async function toBuffer(value) {
|
|
72
|
+
const content = await toBinaryContent(value);
|
|
73
|
+
return Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
74
|
+
}
|
|
75
|
+
function getContentSize(value) {
|
|
76
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
77
|
+
return value.byteLength;
|
|
78
|
+
}
|
|
79
|
+
return value.size;
|
|
80
|
+
}
|
|
81
|
+
function isImageSource(mimeType, extension) {
|
|
82
|
+
if (mimeType?.trim().toLowerCase().startsWith("image/")) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return extension ? IMAGE_EXTENSIONS.has(extension.trim().toLowerCase()) : false;
|
|
86
|
+
}
|
|
87
|
+
function normalizeOutputFormat(extension, conversionFormat) {
|
|
88
|
+
if (conversionFormat) {
|
|
89
|
+
return conversionFormat;
|
|
90
|
+
}
|
|
91
|
+
const normalized = extension?.trim().toLowerCase();
|
|
92
|
+
if (normalized && SHARP_OUTPUT_FORMATS.has(normalized)) {
|
|
93
|
+
return normalized;
|
|
94
|
+
}
|
|
95
|
+
return "png";
|
|
96
|
+
}
|
|
97
|
+
function replaceFileExtension(fileName, extension) {
|
|
98
|
+
const baseName = basename(fileName, extname(fileName)) || "media";
|
|
99
|
+
return `${baseName}.${extension}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/runtime/image.ts
|
|
103
|
+
var sharpFactoryPromise;
|
|
104
|
+
async function loadSharp() {
|
|
105
|
+
sharpFactoryPromise ??= import("sharp").then((module) => {
|
|
106
|
+
const resolved = module;
|
|
107
|
+
return resolved.default ?? resolved;
|
|
108
|
+
}).catch((error) => {
|
|
109
|
+
sharpFactoryPromise = void 0;
|
|
110
|
+
throw error;
|
|
111
|
+
});
|
|
112
|
+
return sharpFactoryPromise;
|
|
113
|
+
}
|
|
114
|
+
function resolveFit(mode) {
|
|
115
|
+
switch (mode) {
|
|
116
|
+
case "contain":
|
|
117
|
+
case "cover":
|
|
118
|
+
case "fill":
|
|
119
|
+
case "inside":
|
|
120
|
+
case "outside":
|
|
121
|
+
return mode;
|
|
122
|
+
default:
|
|
123
|
+
return "cover";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function createDefaultMediaConversionExecutor() {
|
|
127
|
+
return {
|
|
128
|
+
async generate({ source, conversion: conversion2 }) {
|
|
129
|
+
if (!isImageSource(source.mimeType, source.extension)) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const outputFormat = normalizeOutputFormat(source.extension, conversion2.format);
|
|
133
|
+
try {
|
|
134
|
+
const sharp = await loadSharp();
|
|
135
|
+
let pipeline = sharp(await toBuffer(source.contents), {
|
|
136
|
+
animated: true,
|
|
137
|
+
failOn: "warning"
|
|
138
|
+
}).rotate();
|
|
139
|
+
if (conversion2.width || conversion2.height) {
|
|
140
|
+
pipeline = pipeline.resize({
|
|
141
|
+
width: conversion2.width,
|
|
142
|
+
height: conversion2.height,
|
|
143
|
+
fit: resolveFit(conversion2.fit),
|
|
144
|
+
withoutEnlargement: true
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
switch (outputFormat) {
|
|
148
|
+
case "avif":
|
|
149
|
+
pipeline = pipeline.avif({
|
|
150
|
+
quality: conversion2.quality ?? 80,
|
|
151
|
+
effort: 4
|
|
152
|
+
});
|
|
153
|
+
break;
|
|
154
|
+
case "jpeg":
|
|
155
|
+
case "jpg":
|
|
156
|
+
pipeline = pipeline.jpeg({
|
|
157
|
+
quality: conversion2.quality ?? 82,
|
|
158
|
+
mozjpeg: true
|
|
159
|
+
});
|
|
160
|
+
break;
|
|
161
|
+
case "png":
|
|
162
|
+
pipeline = pipeline.png({
|
|
163
|
+
compressionLevel: 9,
|
|
164
|
+
quality: conversion2.quality
|
|
165
|
+
});
|
|
166
|
+
break;
|
|
167
|
+
case "webp":
|
|
168
|
+
pipeline = pipeline.webp({
|
|
169
|
+
quality: conversion2.quality ?? 82,
|
|
170
|
+
effort: 4
|
|
171
|
+
});
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
const contents = await pipeline.toBuffer();
|
|
175
|
+
const fileName = replaceFileExtension(source.fileName, outputFormat);
|
|
176
|
+
return {
|
|
177
|
+
contents,
|
|
178
|
+
fileName,
|
|
179
|
+
mimeType: inferMimeType(fileName)
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`[Holo Media] Failed to generate conversion "${conversion2.name}" for "${source.fileName}": ${error.message}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
var defaultMediaConversionExecutor = createDefaultMediaConversionExecutor();
|
|
190
|
+
|
|
191
|
+
// src/registry.ts
|
|
192
|
+
var mediaDefinitionRegistry = /* @__PURE__ */ new WeakMap();
|
|
193
|
+
var mediaDefinitionMorphClassRegistry = /* @__PURE__ */ new Map();
|
|
194
|
+
function sanitizePathSegment(value) {
|
|
195
|
+
return value.trim().replace(/\\/g, "/").split("/").filter(Boolean).join("-");
|
|
196
|
+
}
|
|
197
|
+
function resolveConversionExtension(fileName, conversionFormat) {
|
|
198
|
+
if (conversionFormat) {
|
|
199
|
+
return conversionFormat === "jpg" ? "jpg" : conversionFormat;
|
|
200
|
+
}
|
|
201
|
+
const extension = fileName.split(".").pop()?.trim().toLowerCase();
|
|
202
|
+
return extension || "bin";
|
|
203
|
+
}
|
|
204
|
+
var defaultMediaPathGenerator = Object.freeze({
|
|
205
|
+
originalPath({
|
|
206
|
+
uuid,
|
|
207
|
+
fileName
|
|
208
|
+
}) {
|
|
209
|
+
return `media/${sanitizePathSegment(uuid)}/original/${sanitizePathSegment(fileName)}`;
|
|
210
|
+
},
|
|
211
|
+
conversionPath({
|
|
212
|
+
uuid,
|
|
213
|
+
conversion: conversion2,
|
|
214
|
+
generatedFileName,
|
|
215
|
+
fileName
|
|
216
|
+
}) {
|
|
217
|
+
const extension = resolveConversionExtension(generatedFileName ?? fileName, conversion2.format);
|
|
218
|
+
const safeName = sanitizePathSegment(conversion2.name);
|
|
219
|
+
return `media/${sanitizePathSegment(uuid)}/conversions/${safeName}.${extension}`;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
var mediaPathGenerator = defaultMediaPathGenerator;
|
|
223
|
+
var mediaConversionExecutor;
|
|
224
|
+
function resolveDefinition(target) {
|
|
225
|
+
if (target instanceof Entity) {
|
|
226
|
+
return target.getRepository().definition;
|
|
227
|
+
}
|
|
228
|
+
return target.definition;
|
|
229
|
+
}
|
|
230
|
+
function registerMediaDefinition(target, definition) {
|
|
231
|
+
const resolvedTarget = resolveDefinition(target);
|
|
232
|
+
mediaDefinitionRegistry.set(resolvedTarget, definition);
|
|
233
|
+
mediaDefinitionMorphClassRegistry.set(resolvedTarget.morphClass ?? resolvedTarget.name, definition);
|
|
234
|
+
return definition;
|
|
235
|
+
}
|
|
236
|
+
function getMediaDefinition(target) {
|
|
237
|
+
return mediaDefinitionRegistry.get(resolveDefinition(target));
|
|
238
|
+
}
|
|
239
|
+
function requireMediaDefinition(target) {
|
|
240
|
+
const definition = getMediaDefinition(target);
|
|
241
|
+
if (!definition) {
|
|
242
|
+
const model = resolveDefinition(target);
|
|
243
|
+
throw new Error(
|
|
244
|
+
`[Holo Media] Model "${model.name}" is not configured for media. Wrap it with defineMediaModel().`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return definition;
|
|
248
|
+
}
|
|
249
|
+
function getMediaDefinitionForMorphClass(morphClass) {
|
|
250
|
+
return mediaDefinitionMorphClassRegistry.get(morphClass);
|
|
251
|
+
}
|
|
252
|
+
function requireMediaDefinitionForMorphClass(morphClass) {
|
|
253
|
+
const definition = getMediaDefinitionForMorphClass(morphClass);
|
|
254
|
+
if (!definition) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`[Holo Media] Model "${morphClass}" is not configured for media. Wrap it with defineMediaModel().`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return definition;
|
|
260
|
+
}
|
|
261
|
+
function resolveMediaCollection(target, collectionName = "default") {
|
|
262
|
+
const definition = getMediaDefinition(target);
|
|
263
|
+
if (!definition) {
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
kind: "collection",
|
|
266
|
+
name: collectionName,
|
|
267
|
+
singleFile: false,
|
|
268
|
+
acceptedMimeTypes: Object.freeze([]),
|
|
269
|
+
acceptedExtensions: Object.freeze([])
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return definition.collectionsByName[collectionName] ?? Object.freeze({
|
|
273
|
+
kind: "collection",
|
|
274
|
+
name: collectionName,
|
|
275
|
+
singleFile: false,
|
|
276
|
+
acceptedMimeTypes: Object.freeze([]),
|
|
277
|
+
acceptedExtensions: Object.freeze([])
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
function resolveMediaConversion(target, conversionName) {
|
|
281
|
+
return getMediaDefinition(target)?.conversionsByName[conversionName];
|
|
282
|
+
}
|
|
283
|
+
function setMediaPathGenerator(generator) {
|
|
284
|
+
mediaPathGenerator = generator;
|
|
285
|
+
}
|
|
286
|
+
function resetMediaPathGenerator() {
|
|
287
|
+
mediaPathGenerator = defaultMediaPathGenerator;
|
|
288
|
+
}
|
|
289
|
+
function getMediaPathGenerator() {
|
|
290
|
+
return mediaPathGenerator;
|
|
291
|
+
}
|
|
292
|
+
function setMediaConversionExecutor(executor) {
|
|
293
|
+
mediaConversionExecutor = executor;
|
|
294
|
+
}
|
|
295
|
+
function getMediaConversionExecutor() {
|
|
296
|
+
return mediaConversionExecutor ?? defaultMediaConversionExecutor;
|
|
297
|
+
}
|
|
298
|
+
function resetMediaRuntime() {
|
|
299
|
+
mediaPathGenerator = defaultMediaPathGenerator;
|
|
300
|
+
mediaConversionExecutor = void 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/model/Media.ts
|
|
304
|
+
import { column, defineGeneratedTable, defineModel, morphTo } from "@holo-js/db";
|
|
305
|
+
var mediaTable = defineGeneratedTable("media", {
|
|
306
|
+
id: column.id(),
|
|
307
|
+
uuid: column.uuid().unique(),
|
|
308
|
+
model_type: column.string(),
|
|
309
|
+
model_id: column.string(),
|
|
310
|
+
collection_name: column.string().default("default"),
|
|
311
|
+
name: column.string(),
|
|
312
|
+
file_name: column.string(),
|
|
313
|
+
disk: column.string(),
|
|
314
|
+
conversions_disk: column.string().nullable(),
|
|
315
|
+
mime_type: column.string().nullable(),
|
|
316
|
+
extension: column.string().nullable(),
|
|
317
|
+
size: column.bigInteger(),
|
|
318
|
+
path: column.string(),
|
|
319
|
+
generated_conversions: column.json("generated_conversions").default({}),
|
|
320
|
+
order_column: column.integer().default(1),
|
|
321
|
+
created_at: column.timestamp().defaultNow(),
|
|
322
|
+
updated_at: column.timestamp().defaultNow()
|
|
323
|
+
}, {
|
|
324
|
+
indexes: [
|
|
325
|
+
{ columns: ["model_type", "model_id"], unique: false },
|
|
326
|
+
{ columns: ["model_type", "model_id", "collection_name"], unique: false }
|
|
327
|
+
]
|
|
328
|
+
});
|
|
329
|
+
var Media = defineModel(mediaTable, {
|
|
330
|
+
fillable: [
|
|
331
|
+
"uuid",
|
|
332
|
+
"model_type",
|
|
333
|
+
"model_id",
|
|
334
|
+
"collection_name",
|
|
335
|
+
"name",
|
|
336
|
+
"file_name",
|
|
337
|
+
"disk",
|
|
338
|
+
"conversions_disk",
|
|
339
|
+
"mime_type",
|
|
340
|
+
"extension",
|
|
341
|
+
"size",
|
|
342
|
+
"path",
|
|
343
|
+
"generated_conversions",
|
|
344
|
+
"order_column"
|
|
345
|
+
],
|
|
346
|
+
casts: {
|
|
347
|
+
generated_conversions: "json"
|
|
348
|
+
},
|
|
349
|
+
relations: {
|
|
350
|
+
model: morphTo("model")
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// src/model/adder.ts
|
|
355
|
+
import { basename as basename2 } from "path";
|
|
356
|
+
import { readFile } from "fs/promises";
|
|
357
|
+
import { randomUUID } from "crypto";
|
|
358
|
+
import { Storage as Storage3 } from "@holo-js/storage/runtime";
|
|
359
|
+
|
|
360
|
+
// src/queue.ts
|
|
361
|
+
import {
|
|
362
|
+
dispatch,
|
|
363
|
+
defineJob,
|
|
364
|
+
getRegisteredQueueJob,
|
|
365
|
+
registerQueueJob
|
|
366
|
+
} from "@holo-js/queue";
|
|
367
|
+
import { connectionAsyncContext } from "@holo-js/db";
|
|
368
|
+
|
|
369
|
+
// src/model/conversions.ts
|
|
370
|
+
import { Storage } from "@holo-js/storage/runtime";
|
|
371
|
+
function fallbackCollectionDefinition(collectionName) {
|
|
372
|
+
return Object.freeze({
|
|
373
|
+
kind: "collection",
|
|
374
|
+
name: collectionName,
|
|
375
|
+
singleFile: collectionName === "default",
|
|
376
|
+
acceptedMimeTypes: Object.freeze([]),
|
|
377
|
+
acceptedExtensions: Object.freeze([])
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function normalizeRequestedConversions(requested) {
|
|
381
|
+
if (typeof requested === "string") {
|
|
382
|
+
return Object.freeze([requested]);
|
|
383
|
+
}
|
|
384
|
+
if (!requested || requested.length === 0) {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
return Object.freeze([...new Set(requested.filter(Boolean))]);
|
|
388
|
+
}
|
|
389
|
+
function resolveCollectionDefinition(definition, collectionName) {
|
|
390
|
+
return definition.collectionsByName[collectionName] ?? fallbackCollectionDefinition(collectionName);
|
|
391
|
+
}
|
|
392
|
+
function resolveMatchingConversions(definition, collectionName, requested, options = {}) {
|
|
393
|
+
if (requested?.length) {
|
|
394
|
+
const unknown = requested.filter((name) => {
|
|
395
|
+
return !definition.conversionsByName[name];
|
|
396
|
+
});
|
|
397
|
+
if (unknown.length > 0) {
|
|
398
|
+
throw new Error(`[Holo Media] Unknown media conversion "${unknown[0]}".`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const matching = definition.conversions.filter((conversion2) => {
|
|
402
|
+
const appliesToCollection = conversion2.collections.length === 0 || conversion2.collections.includes(collectionName);
|
|
403
|
+
if (!appliesToCollection) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (!requested?.length) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
return requested.includes(conversion2.name);
|
|
410
|
+
});
|
|
411
|
+
if (requested?.length) {
|
|
412
|
+
const missing = requested.filter((name) => {
|
|
413
|
+
return !matching.some((conversion2) => conversion2.name === name);
|
|
414
|
+
});
|
|
415
|
+
if (missing.length > 0) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`[Holo Media] Conversion "${missing[0]}" is not registered for collection "${collectionName}".`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (options.includeQueued) {
|
|
422
|
+
return matching;
|
|
423
|
+
}
|
|
424
|
+
return matching.filter((conversion2) => !conversion2.queued);
|
|
425
|
+
}
|
|
426
|
+
function resolveQueuedConversionNames(options) {
|
|
427
|
+
const requested = normalizeRequestedConversions(options.requestedConversions);
|
|
428
|
+
return Object.freeze(
|
|
429
|
+
resolveMatchingConversions(
|
|
430
|
+
options.definition,
|
|
431
|
+
options.collectionName,
|
|
432
|
+
requested,
|
|
433
|
+
{ includeQueued: true }
|
|
434
|
+
).filter((conversion2) => conversion2.queued).map((conversion2) => conversion2.name)
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
async function generateStoredConversions(options) {
|
|
438
|
+
const requested = normalizeRequestedConversions(options.requestedConversions);
|
|
439
|
+
const matchingConversions = resolveMatchingConversions(
|
|
440
|
+
options.definition,
|
|
441
|
+
options.collection.name,
|
|
442
|
+
requested,
|
|
443
|
+
{ includeQueued: options.includeQueued === true }
|
|
444
|
+
);
|
|
445
|
+
if (matchingConversions.length === 0) {
|
|
446
|
+
return Object.freeze({});
|
|
447
|
+
}
|
|
448
|
+
const generatedConversions = /* @__PURE__ */ Object.create(null);
|
|
449
|
+
const executor = getMediaConversionExecutor();
|
|
450
|
+
try {
|
|
451
|
+
for (const conversion2 of matchingConversions) {
|
|
452
|
+
const generated = await executor.generate({
|
|
453
|
+
source: options.source,
|
|
454
|
+
collection: options.collection,
|
|
455
|
+
conversion: conversion2
|
|
456
|
+
});
|
|
457
|
+
if (!generated) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const generatedFileName = sanitizeFileName(generated.fileName ?? options.source.fileName);
|
|
461
|
+
const conversionPath = getMediaPathGenerator().conversionPath({
|
|
462
|
+
uuid: options.source.uuid,
|
|
463
|
+
fileName: options.source.fileName,
|
|
464
|
+
extension: options.source.extension,
|
|
465
|
+
collection: options.collection,
|
|
466
|
+
conversion: conversion2,
|
|
467
|
+
generatedFileName
|
|
468
|
+
});
|
|
469
|
+
const targetDisk = generated.disk ?? options.conversionsDisk;
|
|
470
|
+
const conversionMimeType = inferMimeType(generatedFileName, generated.mimeType);
|
|
471
|
+
const conversionContents = await toBinaryContent(generated.contents);
|
|
472
|
+
await Storage.disk(targetDisk).put(conversionPath, conversionContents);
|
|
473
|
+
generatedConversions[conversion2.name] = Object.freeze({
|
|
474
|
+
path: conversionPath,
|
|
475
|
+
disk: targetDisk,
|
|
476
|
+
fileName: generatedFileName,
|
|
477
|
+
mimeType: conversionMimeType,
|
|
478
|
+
size: getContentSize(conversionContents)
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
await deleteStoredConversions(
|
|
483
|
+
generatedConversions,
|
|
484
|
+
options.conversionsDisk
|
|
485
|
+
);
|
|
486
|
+
throw error;
|
|
487
|
+
}
|
|
488
|
+
return Object.freeze(generatedConversions);
|
|
489
|
+
}
|
|
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
|
+
async function deleteObsoleteConversions(current, next, fallbackDisk, requested) {
|
|
496
|
+
for (const [name, conversion2] of Object.entries(current)) {
|
|
497
|
+
if (requested?.length && !requested.includes(name)) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (!conversion2?.path) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const nextConversion = next[name];
|
|
504
|
+
if (nextConversion?.path === conversion2.path && nextConversion.disk === (conversion2.disk ?? fallbackDisk)) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
await Storage.disk(conversion2.disk ?? fallbackDisk).delete(conversion2.path);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function regenerateMediaEntityConversions(options) {
|
|
511
|
+
const media = options.media;
|
|
512
|
+
const modelType = String(media.get("model_type"));
|
|
513
|
+
const definition = options.owner ? requireMediaDefinition(options.owner) : requireMediaDefinitionForMorphClass(modelType);
|
|
514
|
+
const collectionName = String(media.get("collection_name"));
|
|
515
|
+
const collection2 = resolveCollectionDefinition(definition, collectionName);
|
|
516
|
+
const requested = normalizeRequestedConversions(options.conversions);
|
|
517
|
+
const current = media.get("generated_conversions") ?? {};
|
|
518
|
+
const conversionsDisk = String(media.get("conversions_disk") ?? media.get("disk"));
|
|
519
|
+
resolveMatchingConversions(definition, collectionName, requested, {
|
|
520
|
+
includeQueued: options.includeQueued === true
|
|
521
|
+
});
|
|
522
|
+
const sourceContents = await Storage.disk(String(media.get("disk"))).getBytes(String(media.get("path")));
|
|
523
|
+
if (!sourceContents) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`[Holo Media] Cannot regenerate conversions for media "${media.get("uuid")}" because the original file is missing.`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
const nextBase = requested?.length ? Object.fromEntries(
|
|
529
|
+
Object.entries(current).filter(([name]) => !requested.includes(name))
|
|
530
|
+
) : {};
|
|
531
|
+
const regenerated = await generateStoredConversions({
|
|
532
|
+
definition,
|
|
533
|
+
collection: collection2,
|
|
534
|
+
conversionsDisk,
|
|
535
|
+
requestedConversions: requested,
|
|
536
|
+
includeQueued: options.includeQueued === true,
|
|
537
|
+
source: {
|
|
538
|
+
uuid: String(media.get("uuid")),
|
|
539
|
+
fileName: String(media.get("file_name")),
|
|
540
|
+
extension: media.get("extension") ?? void 0,
|
|
541
|
+
mimeType: media.get("mime_type") ?? void 0,
|
|
542
|
+
size: sourceContents.byteLength,
|
|
543
|
+
contents: sourceContents
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
const generatedConversions = Object.freeze({
|
|
547
|
+
...nextBase,
|
|
548
|
+
...regenerated
|
|
549
|
+
});
|
|
550
|
+
media.forceFill({
|
|
551
|
+
generated_conversions: generatedConversions
|
|
552
|
+
});
|
|
553
|
+
await media.save();
|
|
554
|
+
await deleteObsoleteConversions(current, generatedConversions, conversionsDisk, requested);
|
|
555
|
+
options.owner?.forgetRelation("media");
|
|
556
|
+
return generatedConversions;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/queue.ts
|
|
560
|
+
var MEDIA_GENERATE_CONVERSIONS_JOB = "media.generate-conversions";
|
|
561
|
+
function normalizeConversionNames(conversionNames) {
|
|
562
|
+
if (!conversionNames || conversionNames.length === 0) {
|
|
563
|
+
return void 0;
|
|
564
|
+
}
|
|
565
|
+
const normalized = [...new Set(
|
|
566
|
+
conversionNames.map((name) => name.trim()).filter(Boolean)
|
|
567
|
+
)];
|
|
568
|
+
return normalized.length > 0 ? Object.freeze(normalized) : void 0;
|
|
569
|
+
}
|
|
570
|
+
function normalizeMediaIdentifier(mediaId) {
|
|
571
|
+
if (typeof mediaId === "number") {
|
|
572
|
+
if (!Number.isFinite(mediaId)) {
|
|
573
|
+
throw new Error("[Holo Media] Queued media conversion jobs require a finite media identifier.");
|
|
574
|
+
}
|
|
575
|
+
return mediaId;
|
|
576
|
+
}
|
|
577
|
+
const normalized = mediaId.trim();
|
|
578
|
+
if (!normalized) {
|
|
579
|
+
throw new Error("[Holo Media] Queued media conversion jobs require a non-empty media identifier.");
|
|
580
|
+
}
|
|
581
|
+
return normalized;
|
|
582
|
+
}
|
|
583
|
+
function normalizePayload(payload) {
|
|
584
|
+
return Object.freeze({
|
|
585
|
+
mediaId: normalizeMediaIdentifier(payload.mediaId),
|
|
586
|
+
conversionNames: normalizeConversionNames(payload.conversionNames) ?? Object.freeze([])
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
async function runMediaGenerateConversionsJob(payload) {
|
|
590
|
+
const normalized = normalizePayload(payload);
|
|
591
|
+
const media = await Media.find(normalized.mediaId);
|
|
592
|
+
if (!media) {
|
|
593
|
+
return Object.freeze({
|
|
594
|
+
status: "missing-media",
|
|
595
|
+
conversionNames: Object.freeze([])
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
await regenerateMediaEntityConversions({
|
|
599
|
+
media,
|
|
600
|
+
conversions: normalized.conversionNames,
|
|
601
|
+
includeQueued: true
|
|
602
|
+
});
|
|
603
|
+
return Object.freeze({
|
|
604
|
+
status: "processed",
|
|
605
|
+
conversionNames: normalized.conversionNames
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function ensureMediaQueueJobRegistered() {
|
|
609
|
+
if (getRegisteredQueueJob(MEDIA_GENERATE_CONVERSIONS_JOB)) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
registerQueueJob(defineJob({
|
|
613
|
+
queue: "media",
|
|
614
|
+
async handle(payload) {
|
|
615
|
+
return runMediaGenerateConversionsJob(payload);
|
|
616
|
+
}
|
|
617
|
+
}), {
|
|
618
|
+
name: MEDIA_GENERATE_CONVERSIONS_JOB
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
function getActiveTransaction() {
|
|
622
|
+
const active = connectionAsyncContext.getActive()?.connection;
|
|
623
|
+
if (!active || active.getScope().kind === "root") {
|
|
624
|
+
return void 0;
|
|
625
|
+
}
|
|
626
|
+
return active;
|
|
627
|
+
}
|
|
628
|
+
async function executeQueuedMediaDispatch(payload, onSynchronousDispatch) {
|
|
629
|
+
const dispatched = await dispatch(MEDIA_GENERATE_CONVERSIONS_JOB, payload);
|
|
630
|
+
if (dispatched.synchronous) {
|
|
631
|
+
await onSynchronousDispatch?.();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function dispatchQueuedMediaConversions(payload) {
|
|
635
|
+
const normalized = normalizePayload(payload);
|
|
636
|
+
if (normalized.conversionNames.length === 0) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
ensureMediaQueueJobRegistered();
|
|
640
|
+
const activeTransaction = getActiveTransaction();
|
|
641
|
+
if (activeTransaction) {
|
|
642
|
+
activeTransaction.afterCommit(() => executeQueuedMediaDispatch(normalized));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
await executeQueuedMediaDispatch(normalized);
|
|
646
|
+
}
|
|
647
|
+
async function dispatchQueuedMediaConversionsForModel(payload, onSynchronousDispatch) {
|
|
648
|
+
const normalized = normalizePayload(payload);
|
|
649
|
+
if (normalized.conversionNames.length === 0) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
ensureMediaQueueJobRegistered();
|
|
653
|
+
const activeTransaction = getActiveTransaction();
|
|
654
|
+
if (activeTransaction) {
|
|
655
|
+
activeTransaction.afterCommit(() => executeQueuedMediaDispatch(normalized, onSynchronousDispatch));
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
await executeQueuedMediaDispatch(normalized, onSynchronousDispatch);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/model/item.ts
|
|
662
|
+
import { Storage as Storage2 } from "@holo-js/storage/runtime";
|
|
663
|
+
var MediaItem = class {
|
|
664
|
+
constructor(entity, owner) {
|
|
665
|
+
this.entity = entity;
|
|
666
|
+
this.owner = owner;
|
|
667
|
+
}
|
|
668
|
+
get record() {
|
|
669
|
+
return this.entity.toAttributes();
|
|
670
|
+
}
|
|
671
|
+
get collectionName() {
|
|
672
|
+
return this.entity.get("collection_name");
|
|
673
|
+
}
|
|
674
|
+
get fileName() {
|
|
675
|
+
return this.entity.get("file_name");
|
|
676
|
+
}
|
|
677
|
+
get mimeType() {
|
|
678
|
+
return this.entity.get("mime_type");
|
|
679
|
+
}
|
|
680
|
+
get size() {
|
|
681
|
+
return this.entity.get("size");
|
|
682
|
+
}
|
|
683
|
+
getEntity() {
|
|
684
|
+
return this.entity;
|
|
685
|
+
}
|
|
686
|
+
getAvailableConversions() {
|
|
687
|
+
const conversions = this.entity.get("generated_conversions");
|
|
688
|
+
return Object.freeze(
|
|
689
|
+
Object.keys(conversions ?? {})
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
getPath(conversion2) {
|
|
693
|
+
const variant = this.resolveVariant(conversion2);
|
|
694
|
+
if (!variant) {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
return Storage2.disk(variant.disk).path(variant.path);
|
|
699
|
+
} catch {
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
getUrl(conversion2) {
|
|
704
|
+
const variant = this.resolveVariant(conversion2);
|
|
705
|
+
if (!variant) {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
return Storage2.disk(variant.disk).url(variant.path);
|
|
710
|
+
} catch {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
getTemporaryUrl(conversion2, options) {
|
|
715
|
+
const variant = this.resolveVariant(conversion2);
|
|
716
|
+
if (!variant) {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
return Storage2.disk(variant.disk).temporaryUrl(variant.path, options);
|
|
721
|
+
} catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async delete() {
|
|
726
|
+
const deletedSnapshots = [];
|
|
727
|
+
try {
|
|
728
|
+
for (const variant of this.resolveStoredVariants()) {
|
|
729
|
+
const disk = Storage2.disk(variant.disk);
|
|
730
|
+
const contents = await disk.getBytes(variant.path);
|
|
731
|
+
await disk.delete(variant.path);
|
|
732
|
+
if (contents) {
|
|
733
|
+
deletedSnapshots.push({
|
|
734
|
+
...variant,
|
|
735
|
+
contents
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
await this.entity.delete();
|
|
740
|
+
this.owner?.forgetRelation("media");
|
|
741
|
+
} catch (error) {
|
|
742
|
+
for (const snapshot of deletedSnapshots.reverse()) {
|
|
743
|
+
await Storage2.disk(snapshot.disk).put(snapshot.path, snapshot.contents).catch(() => void 0);
|
|
744
|
+
}
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
async regenerate(conversions) {
|
|
749
|
+
await regenerateMediaEntityConversions({
|
|
750
|
+
media: this.entity,
|
|
751
|
+
owner: this.owner,
|
|
752
|
+
conversions
|
|
753
|
+
});
|
|
754
|
+
const definition = this.owner ? requireMediaDefinition(this.owner) : requireMediaDefinitionForMorphClass(String(this.entity.get("model_type")));
|
|
755
|
+
const queuedConversions = resolveQueuedConversionNames({
|
|
756
|
+
definition,
|
|
757
|
+
collectionName: String(this.entity.get("collection_name")),
|
|
758
|
+
requestedConversions: conversions
|
|
759
|
+
});
|
|
760
|
+
await dispatchQueuedMediaConversionsForModel({
|
|
761
|
+
mediaId: this.entity.get("id"),
|
|
762
|
+
conversionNames: queuedConversions
|
|
763
|
+
}, async () => {
|
|
764
|
+
await this.entity.refresh();
|
|
765
|
+
});
|
|
766
|
+
return this;
|
|
767
|
+
}
|
|
768
|
+
toJSON() {
|
|
769
|
+
return this.entity.toJSON();
|
|
770
|
+
}
|
|
771
|
+
resolveVariant(conversion2) {
|
|
772
|
+
if (!conversion2) {
|
|
773
|
+
return {
|
|
774
|
+
path: this.entity.get("path"),
|
|
775
|
+
disk: this.entity.get("disk")
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const conversions = this.entity.get("generated_conversions");
|
|
779
|
+
const variant = conversions?.[conversion2];
|
|
780
|
+
if (!variant?.path) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
return {
|
|
784
|
+
path: variant.path,
|
|
785
|
+
disk: variant.disk ?? this.entity.get("conversions_disk") ?? this.entity.get("disk")
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
resolveStoredVariants() {
|
|
789
|
+
const variants = [{
|
|
790
|
+
path: this.entity.get("path"),
|
|
791
|
+
disk: this.entity.get("disk")
|
|
792
|
+
}];
|
|
793
|
+
const fallbackDisk = this.entity.get("conversions_disk") ?? this.entity.get("disk");
|
|
794
|
+
const conversions = this.entity.get("generated_conversions") ?? {};
|
|
795
|
+
for (const value of Object.values(conversions)) {
|
|
796
|
+
if (!value?.path) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
variants.push({
|
|
800
|
+
path: value.path,
|
|
801
|
+
disk: value.disk ?? fallbackDisk
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
return variants;
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// src/model/adder.ts
|
|
809
|
+
function resolveImplicitDiskName() {
|
|
810
|
+
const defaultDisk = Storage3.disk();
|
|
811
|
+
if (defaultDisk.visibility === "public") {
|
|
812
|
+
return defaultDisk.name;
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
const publicDisk = Storage3.disk("public");
|
|
816
|
+
if (publicDisk.visibility === "public") {
|
|
817
|
+
return publicDisk.name;
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
}
|
|
821
|
+
return defaultDisk.name;
|
|
822
|
+
}
|
|
823
|
+
function parseRemoteFileName(url) {
|
|
824
|
+
try {
|
|
825
|
+
const parsedUrl = new URL(url);
|
|
826
|
+
return basename2(parsedUrl.pathname) || "media.bin";
|
|
827
|
+
} catch {
|
|
828
|
+
return "media.bin";
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function createMaxSizeError(fileName, collectionName) {
|
|
832
|
+
return new Error(
|
|
833
|
+
`[Holo Media] File "${fileName}" exceeds the max size for collection "${collectionName}".`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
async function readRemoteMediaContents(response, fileName, maxSize, collectionName) {
|
|
837
|
+
if (typeof maxSize !== "number" || !response.body) {
|
|
838
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
839
|
+
}
|
|
840
|
+
const reader = response.body.getReader();
|
|
841
|
+
const chunks = [];
|
|
842
|
+
let totalSize = 0;
|
|
843
|
+
try {
|
|
844
|
+
while (true) {
|
|
845
|
+
const { done, value } = await reader.read();
|
|
846
|
+
if (done) {
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
if (!value) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
totalSize += value.byteLength;
|
|
853
|
+
if (totalSize > maxSize) {
|
|
854
|
+
await reader.cancel().catch(() => void 0);
|
|
855
|
+
throw createMaxSizeError(fileName, collectionName);
|
|
856
|
+
}
|
|
857
|
+
chunks.push(value);
|
|
858
|
+
}
|
|
859
|
+
} finally {
|
|
860
|
+
reader.releaseLock();
|
|
861
|
+
}
|
|
862
|
+
const contents = new Uint8Array(totalSize);
|
|
863
|
+
let offset = 0;
|
|
864
|
+
for (const chunk of chunks) {
|
|
865
|
+
contents.set(chunk, offset);
|
|
866
|
+
offset += chunk.byteLength;
|
|
867
|
+
}
|
|
868
|
+
return contents;
|
|
869
|
+
}
|
|
870
|
+
async function resolveMediaSource(input, overrideFileName, overrideName, options) {
|
|
871
|
+
if (typeof input === "string" && /^https?:\/\//i.test(input)) {
|
|
872
|
+
return resolveRemoteMediaSource(
|
|
873
|
+
{ url: input },
|
|
874
|
+
overrideFileName,
|
|
875
|
+
overrideName,
|
|
876
|
+
options?.maxSize,
|
|
877
|
+
options?.collectionName
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
if (typeof input !== "string" && isUrlInput(input)) {
|
|
881
|
+
return resolveRemoteMediaSource(
|
|
882
|
+
input,
|
|
883
|
+
overrideFileName,
|
|
884
|
+
overrideName,
|
|
885
|
+
options?.maxSize,
|
|
886
|
+
options?.collectionName
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
if (typeof input === "string" || isPathInput(input)) {
|
|
890
|
+
const path = typeof input === "string" ? input : input.path;
|
|
891
|
+
const contents2 = await readFile(path);
|
|
892
|
+
const fileName2 = sanitizeFileName(overrideFileName ?? basename2(path));
|
|
893
|
+
return {
|
|
894
|
+
contents: contents2,
|
|
895
|
+
fileName: fileName2,
|
|
896
|
+
mimeType: inferMimeType(fileName2),
|
|
897
|
+
extension: getExtension(fileName2),
|
|
898
|
+
size: contents2.byteLength,
|
|
899
|
+
name: getDisplayName(fileName2, overrideName)
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if ("contents" in input) {
|
|
903
|
+
const contents2 = await toBinaryContent(input.contents);
|
|
904
|
+
const fileName2 = sanitizeFileName(overrideFileName ?? input.fileName ?? "media.bin");
|
|
905
|
+
return {
|
|
906
|
+
contents: contents2,
|
|
907
|
+
fileName: fileName2,
|
|
908
|
+
mimeType: inferMimeType(fileName2, input.mimeType),
|
|
909
|
+
extension: getExtension(fileName2),
|
|
910
|
+
size: getContentSize(contents2),
|
|
911
|
+
name: getDisplayName(fileName2, overrideName ?? input.name)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
const contents = await toBinaryContent(input);
|
|
915
|
+
const fileName = sanitizeFileName(overrideFileName ?? "media.bin");
|
|
916
|
+
return {
|
|
917
|
+
contents,
|
|
918
|
+
fileName,
|
|
919
|
+
mimeType: inferMimeType(fileName),
|
|
920
|
+
extension: getExtension(fileName),
|
|
921
|
+
size: getContentSize(contents),
|
|
922
|
+
name: getDisplayName(fileName, overrideName)
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function isPathInput(input) {
|
|
926
|
+
return typeof input === "object" && input !== null && "path" in input;
|
|
927
|
+
}
|
|
928
|
+
function isUrlInput(input) {
|
|
929
|
+
return typeof input === "object" && input !== null && "url" in input;
|
|
930
|
+
}
|
|
931
|
+
async function resolveRemoteMediaSource(input, overrideFileName, overrideName, maxSize, collectionName) {
|
|
932
|
+
const response = await fetch(input.url);
|
|
933
|
+
if (!response.ok) {
|
|
934
|
+
throw new Error(
|
|
935
|
+
`[Holo Media] Failed to download media from "${input.url}" (${response.status} ${response.statusText}).`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
const fileName = sanitizeFileName(
|
|
939
|
+
overrideFileName ?? input.fileName ?? parseRemoteFileName(input.url)
|
|
940
|
+
);
|
|
941
|
+
const responseMimeType = response.headers.get("content-type")?.split(";")[0];
|
|
942
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
943
|
+
const contentLength = contentLengthHeader?.trim() ? Number(contentLengthHeader) : Number.NaN;
|
|
944
|
+
if (typeof maxSize === "number" && Number.isFinite(contentLength) && contentLength > maxSize) {
|
|
945
|
+
throw createMaxSizeError(fileName, collectionName);
|
|
946
|
+
}
|
|
947
|
+
const contents = await readRemoteMediaContents(
|
|
948
|
+
response,
|
|
949
|
+
fileName,
|
|
950
|
+
maxSize,
|
|
951
|
+
collectionName
|
|
952
|
+
);
|
|
953
|
+
return {
|
|
954
|
+
contents,
|
|
955
|
+
fileName,
|
|
956
|
+
mimeType: inferMimeType(fileName, input.mimeType ?? responseMimeType ?? void 0),
|
|
957
|
+
extension: getExtension(fileName),
|
|
958
|
+
size: contents.byteLength,
|
|
959
|
+
name: getDisplayName(fileName, overrideName ?? input.name)
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
function validateSource(collection2, source) {
|
|
963
|
+
if (typeof collection2.maxSize === "number" && source.size > collection2.maxSize) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
`[Holo Media] File "${source.fileName}" exceeds the max size for collection "${collection2.name}".`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
if (collection2.acceptedMimeTypes.length > 0) {
|
|
969
|
+
const mimeType = source.mimeType?.trim().toLowerCase();
|
|
970
|
+
if (!mimeType || !collection2.acceptedMimeTypes.includes(mimeType)) {
|
|
971
|
+
throw new Error(
|
|
972
|
+
`[Holo Media] File "${source.fileName}" is not an accepted MIME type for collection "${collection2.name}".`
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (collection2.acceptedExtensions.length > 0) {
|
|
977
|
+
if (!source.extension || !collection2.acceptedExtensions.includes(source.extension)) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`[Holo Media] File "${source.fileName}" is not an accepted extension for collection "${collection2.name}".`
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async function resolveNextOrderColumn(modelType, modelId, collectionName) {
|
|
985
|
+
const max = await Media.query().where("model_type", modelType).where("model_id", modelId).where("collection_name", collectionName).max("order_column");
|
|
986
|
+
return (max ?? 0) + 1;
|
|
987
|
+
}
|
|
988
|
+
async function deleteMediaItemsWithRollback(items) {
|
|
989
|
+
const deletedSnapshots = [];
|
|
990
|
+
try {
|
|
991
|
+
for (const item of items) {
|
|
992
|
+
const snapshot = await snapshotDeletedMediaItem(item);
|
|
993
|
+
await item.delete();
|
|
994
|
+
deletedSnapshots.push(snapshot);
|
|
995
|
+
}
|
|
996
|
+
} catch (error) {
|
|
997
|
+
for (const snapshot of deletedSnapshots.reverse()) {
|
|
998
|
+
await restoreDeletedMediaSnapshot(snapshot).catch(() => void 0);
|
|
999
|
+
}
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
async function deleteOverflowItems(items, limit) {
|
|
1004
|
+
if (items.length <= limit) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
await deleteMediaItemsWithRollback(items.slice(0, items.length - limit));
|
|
1008
|
+
}
|
|
1009
|
+
async function cleanupGeneratedConversions(conversions, _fallbackDisk) {
|
|
1010
|
+
for (const conversion2 of Object.values(conversions)) {
|
|
1011
|
+
await Storage3.disk(conversion2.disk).delete(conversion2.path).catch(() => void 0);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async function snapshotDeletedMediaItem(item) {
|
|
1015
|
+
const record = item.record;
|
|
1016
|
+
const fileTargets = [{
|
|
1017
|
+
disk: record.disk,
|
|
1018
|
+
path: record.path
|
|
1019
|
+
}];
|
|
1020
|
+
const fallbackDisk = record.conversions_disk ?? record.disk;
|
|
1021
|
+
for (const conversion2 of Object.values(record.generated_conversions ?? {})) {
|
|
1022
|
+
if (!conversion2?.path) {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
fileTargets.push({
|
|
1026
|
+
disk: conversion2.disk ?? fallbackDisk,
|
|
1027
|
+
path: conversion2.path
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
const files = (await Promise.all(fileTargets.map(async (file) => {
|
|
1031
|
+
const contents = await Storage3.disk(file.disk).getBytes(file.path);
|
|
1032
|
+
return contents ? {
|
|
1033
|
+
...file,
|
|
1034
|
+
contents
|
|
1035
|
+
} : null;
|
|
1036
|
+
}))).filter((file) => Boolean(file));
|
|
1037
|
+
return {
|
|
1038
|
+
record: {
|
|
1039
|
+
uuid: record.uuid,
|
|
1040
|
+
model_type: record.model_type,
|
|
1041
|
+
model_id: record.model_id,
|
|
1042
|
+
collection_name: record.collection_name,
|
|
1043
|
+
name: record.name,
|
|
1044
|
+
file_name: record.file_name,
|
|
1045
|
+
disk: record.disk,
|
|
1046
|
+
conversions_disk: record.conversions_disk,
|
|
1047
|
+
mime_type: record.mime_type,
|
|
1048
|
+
extension: record.extension,
|
|
1049
|
+
size: record.size,
|
|
1050
|
+
path: record.path,
|
|
1051
|
+
generated_conversions: record.generated_conversions,
|
|
1052
|
+
order_column: record.order_column
|
|
1053
|
+
},
|
|
1054
|
+
files
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
async function restoreDeletedMediaSnapshot(snapshot) {
|
|
1058
|
+
for (const file of snapshot.files) {
|
|
1059
|
+
await Storage3.disk(file.disk).put(file.path, file.contents);
|
|
1060
|
+
}
|
|
1061
|
+
await Media.create(snapshot.record);
|
|
1062
|
+
}
|
|
1063
|
+
var MediaAdder = class _MediaAdder {
|
|
1064
|
+
constructor(entity, source, state = {}) {
|
|
1065
|
+
this.entity = entity;
|
|
1066
|
+
this.source = source;
|
|
1067
|
+
this.state = state;
|
|
1068
|
+
}
|
|
1069
|
+
usingName(name) {
|
|
1070
|
+
return new _MediaAdder(this.entity, this.source, {
|
|
1071
|
+
...this.state,
|
|
1072
|
+
name
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
usingFileName(fileName) {
|
|
1076
|
+
return new _MediaAdder(this.entity, this.source, {
|
|
1077
|
+
...this.state,
|
|
1078
|
+
fileName
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
onDisk(disk) {
|
|
1082
|
+
return new _MediaAdder(this.entity, this.source, {
|
|
1083
|
+
...this.state,
|
|
1084
|
+
disk
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
async toMediaCollection(collectionName = "default") {
|
|
1088
|
+
const mediaDefinition = requireMediaDefinition(this.entity);
|
|
1089
|
+
const ownerDefinition = this.entity.getRepository().definition;
|
|
1090
|
+
const ownerId = this.entity.get(ownerDefinition.primaryKey);
|
|
1091
|
+
if (ownerId === null || typeof ownerId === "undefined") {
|
|
1092
|
+
throw new Error(
|
|
1093
|
+
`[Holo Media] Cannot attach media to "${ownerDefinition.name}" before it has a persisted primary key.`
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
const collection2 = resolveMediaCollection(this.entity, collectionName);
|
|
1097
|
+
const source = await resolveMediaSource(
|
|
1098
|
+
this.source,
|
|
1099
|
+
this.state.fileName,
|
|
1100
|
+
this.state.name,
|
|
1101
|
+
{
|
|
1102
|
+
maxSize: collection2.maxSize,
|
|
1103
|
+
collectionName: collection2.name
|
|
1104
|
+
}
|
|
1105
|
+
);
|
|
1106
|
+
validateSource(collection2, source);
|
|
1107
|
+
const existing = collection2.singleFile ? await this.entity.getMedia(collectionName) : [];
|
|
1108
|
+
const uuid = randomUUID();
|
|
1109
|
+
const diskName = this.state.disk ?? collection2.disk ?? resolveImplicitDiskName();
|
|
1110
|
+
const conversionsDisk = collection2.conversionsDisk ?? diskName;
|
|
1111
|
+
const originalPath = getMediaPathGenerator().originalPath({
|
|
1112
|
+
uuid,
|
|
1113
|
+
fileName: source.fileName,
|
|
1114
|
+
extension: source.extension,
|
|
1115
|
+
collection: collection2
|
|
1116
|
+
});
|
|
1117
|
+
let originalStored = false;
|
|
1118
|
+
let createdMedia;
|
|
1119
|
+
let generatedConversions = Object.freeze({});
|
|
1120
|
+
try {
|
|
1121
|
+
await Storage3.disk(diskName).put(originalPath, source.contents);
|
|
1122
|
+
originalStored = true;
|
|
1123
|
+
generatedConversions = await generateStoredConversions({
|
|
1124
|
+
definition: mediaDefinition,
|
|
1125
|
+
collection: collection2,
|
|
1126
|
+
conversionsDisk,
|
|
1127
|
+
source: {
|
|
1128
|
+
uuid,
|
|
1129
|
+
fileName: source.fileName,
|
|
1130
|
+
extension: source.extension,
|
|
1131
|
+
mimeType: source.mimeType,
|
|
1132
|
+
size: source.size,
|
|
1133
|
+
contents: source.contents
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
const media = await Media.create({
|
|
1137
|
+
uuid,
|
|
1138
|
+
model_type: ownerDefinition.morphClass,
|
|
1139
|
+
model_id: String(ownerId),
|
|
1140
|
+
collection_name: collectionName,
|
|
1141
|
+
name: source.name,
|
|
1142
|
+
file_name: source.fileName,
|
|
1143
|
+
disk: diskName,
|
|
1144
|
+
conversions_disk: conversionsDisk,
|
|
1145
|
+
mime_type: source.mimeType ?? null,
|
|
1146
|
+
extension: source.extension ?? null,
|
|
1147
|
+
size: source.size,
|
|
1148
|
+
path: originalPath,
|
|
1149
|
+
generated_conversions: generatedConversions,
|
|
1150
|
+
order_column: await resolveNextOrderColumn(
|
|
1151
|
+
ownerDefinition.morphClass,
|
|
1152
|
+
String(ownerId),
|
|
1153
|
+
collectionName
|
|
1154
|
+
)
|
|
1155
|
+
});
|
|
1156
|
+
createdMedia = media;
|
|
1157
|
+
this.entity.forgetRelation("media");
|
|
1158
|
+
await deleteMediaItemsWithRollback(existing);
|
|
1159
|
+
if (typeof collection2.onlyKeepLatest === "number") {
|
|
1160
|
+
const items = await this.entity.getMedia(collectionName);
|
|
1161
|
+
await deleteOverflowItems(items, collection2.onlyKeepLatest);
|
|
1162
|
+
}
|
|
1163
|
+
const queuedConversions = resolveQueuedConversionNames({
|
|
1164
|
+
definition: mediaDefinition,
|
|
1165
|
+
collectionName: collection2.name
|
|
1166
|
+
});
|
|
1167
|
+
await dispatchQueuedMediaConversionsForModel({
|
|
1168
|
+
mediaId: media.get("id"),
|
|
1169
|
+
conversionNames: queuedConversions
|
|
1170
|
+
}, async () => {
|
|
1171
|
+
await media.refresh();
|
|
1172
|
+
});
|
|
1173
|
+
return new MediaItem(media, this.entity);
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
await cleanupGeneratedConversions(generatedConversions, conversionsDisk).catch(() => void 0);
|
|
1176
|
+
if (originalStored) {
|
|
1177
|
+
await Storage3.disk(diskName).delete(originalPath).catch(() => void 0);
|
|
1178
|
+
}
|
|
1179
|
+
if (createdMedia) {
|
|
1180
|
+
await createdMedia.refresh().catch(() => void 0);
|
|
1181
|
+
await createdMedia.delete().catch(() => void 0);
|
|
1182
|
+
}
|
|
1183
|
+
this.entity.forgetRelation("media");
|
|
1184
|
+
throw error;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
|
|
1189
|
+
// src/model/entity.ts
|
|
1190
|
+
function getOwnerKey(entity) {
|
|
1191
|
+
const ownerDefinition = entity.getRepository().definition;
|
|
1192
|
+
const ownerId = entity.get(ownerDefinition.primaryKey);
|
|
1193
|
+
if (ownerId === null || typeof ownerId === "undefined") {
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
return String(ownerId);
|
|
1197
|
+
}
|
|
1198
|
+
function createQuery(entity, collectionName) {
|
|
1199
|
+
const mediaDefinition = getMediaDefinition(entity);
|
|
1200
|
+
if (!mediaDefinition) {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
const ownerDefinition = entity.getRepository().definition;
|
|
1204
|
+
const ownerId = getOwnerKey(entity);
|
|
1205
|
+
if (!ownerId) {
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
let query = Media.query().where("model_type", ownerDefinition.morphClass).where("model_id", ownerId);
|
|
1209
|
+
if (collectionName) {
|
|
1210
|
+
query = query.where("collection_name", collectionName);
|
|
1211
|
+
}
|
|
1212
|
+
return query.orderBy("order_column").orderBy("id");
|
|
1213
|
+
}
|
|
1214
|
+
function installEntityMediaMethods() {
|
|
1215
|
+
const prototype = Entity2.prototype;
|
|
1216
|
+
if (prototype.__holoMediaInstalled__) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
prototype.__holoMediaInstalled__ = true;
|
|
1220
|
+
prototype.addMedia = function addMedia(source) {
|
|
1221
|
+
return new MediaAdder(this, source);
|
|
1222
|
+
};
|
|
1223
|
+
prototype.addMediaFromUrl = function addMediaFromUrl(url) {
|
|
1224
|
+
return new MediaAdder(this, { url });
|
|
1225
|
+
};
|
|
1226
|
+
prototype.getMedia = async function getMedia(collectionName) {
|
|
1227
|
+
const query = createQuery(this, collectionName);
|
|
1228
|
+
if (!query) {
|
|
1229
|
+
return [];
|
|
1230
|
+
}
|
|
1231
|
+
const rows = await query.get();
|
|
1232
|
+
return rows.map((row) => {
|
|
1233
|
+
return new MediaItem(row, this);
|
|
1234
|
+
});
|
|
1235
|
+
};
|
|
1236
|
+
prototype.getMediaUrls = async function getMediaUrls(collectionName, conversionName) {
|
|
1237
|
+
const items = await this.getMedia(collectionName);
|
|
1238
|
+
return items.map((item) => item.getUrl(conversionName)).filter((value) => typeof value === "string");
|
|
1239
|
+
};
|
|
1240
|
+
prototype.getMediaPaths = async function getMediaPaths(collectionName, conversionName) {
|
|
1241
|
+
const items = await this.getMedia(collectionName);
|
|
1242
|
+
return items.map((item) => item.getPath(conversionName)).filter((value) => typeof value === "string");
|
|
1243
|
+
};
|
|
1244
|
+
prototype.getFirstMedia = async function getFirstMedia(collectionName) {
|
|
1245
|
+
const query = createQuery(this, collectionName);
|
|
1246
|
+
if (!query) {
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
const row = await query.first();
|
|
1250
|
+
return row ? new MediaItem(row, this) : null;
|
|
1251
|
+
};
|
|
1252
|
+
prototype.getFirstMediaUrl = async function getFirstMediaUrl(collectionName, conversionName) {
|
|
1253
|
+
const item = await this.getFirstMedia(collectionName);
|
|
1254
|
+
return item?.getUrl(conversionName) ?? null;
|
|
1255
|
+
};
|
|
1256
|
+
prototype.getFirstMediaPath = async function getFirstMediaPath(collectionName, conversionName) {
|
|
1257
|
+
const item = await this.getFirstMedia(collectionName);
|
|
1258
|
+
return item?.getPath(conversionName) ?? null;
|
|
1259
|
+
};
|
|
1260
|
+
prototype.getFirstTemporaryUrl = async function getFirstTemporaryUrl(collectionName, conversionName, options) {
|
|
1261
|
+
const item = await this.getFirstMedia(collectionName);
|
|
1262
|
+
return item?.getTemporaryUrl(conversionName, options) ?? null;
|
|
1263
|
+
};
|
|
1264
|
+
prototype.hasMedia = async function hasMedia(collectionName) {
|
|
1265
|
+
const query = createQuery(this, collectionName);
|
|
1266
|
+
if (!query) {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
return query.exists();
|
|
1270
|
+
};
|
|
1271
|
+
prototype.clearMediaCollection = async function clearMediaCollection(collectionName) {
|
|
1272
|
+
const items = await this.getMedia(collectionName);
|
|
1273
|
+
for (const item of items) {
|
|
1274
|
+
await item.delete();
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
prototype.regenerateMedia = async function regenerateMedia(collectionName, conversions) {
|
|
1278
|
+
const items = await this.getMedia(collectionName);
|
|
1279
|
+
for (const item of items) {
|
|
1280
|
+
await item.regenerate(conversions);
|
|
1281
|
+
}
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
// src/defineMediaModel.ts
|
|
1286
|
+
import { morphMany } from "@holo-js/db";
|
|
1287
|
+
|
|
1288
|
+
// src/definitions/collections.ts
|
|
1289
|
+
function decorateCollection(definition) {
|
|
1290
|
+
const clone = (overrides) => decorateCollection(Object.freeze({
|
|
1291
|
+
...definition,
|
|
1292
|
+
...overrides
|
|
1293
|
+
}));
|
|
1294
|
+
return Object.freeze({
|
|
1295
|
+
kind: definition.kind,
|
|
1296
|
+
name: definition.name,
|
|
1297
|
+
definition,
|
|
1298
|
+
disk(diskName) {
|
|
1299
|
+
return clone({ diskName: diskName.trim() || void 0 });
|
|
1300
|
+
},
|
|
1301
|
+
conversionsDisk(diskName) {
|
|
1302
|
+
return clone({ conversionsDiskName: diskName.trim() || void 0 });
|
|
1303
|
+
},
|
|
1304
|
+
singleFile() {
|
|
1305
|
+
return clone({ singleFile: true, onlyKeepLatest: 1 });
|
|
1306
|
+
},
|
|
1307
|
+
onlyKeepLatest(limit) {
|
|
1308
|
+
return clone({
|
|
1309
|
+
onlyKeepLatest: Math.max(1, Math.floor(limit))
|
|
1310
|
+
});
|
|
1311
|
+
},
|
|
1312
|
+
acceptsMimeTypes(mimeTypes) {
|
|
1313
|
+
return clone({
|
|
1314
|
+
acceptedMimeTypes: Object.freeze(
|
|
1315
|
+
mimeTypes.map((mimeType) => mimeType.trim()).map((mimeType) => mimeType.toLowerCase()).filter(Boolean)
|
|
1316
|
+
)
|
|
1317
|
+
});
|
|
1318
|
+
},
|
|
1319
|
+
acceptsExtensions(extensions) {
|
|
1320
|
+
return clone({
|
|
1321
|
+
acceptedExtensions: Object.freeze(
|
|
1322
|
+
extensions.map((extension) => extension.trim().replace(/^\./, "").toLowerCase()).filter(Boolean)
|
|
1323
|
+
)
|
|
1324
|
+
});
|
|
1325
|
+
},
|
|
1326
|
+
maxSize(bytes) {
|
|
1327
|
+
return clone({
|
|
1328
|
+
maxFileSize: Math.max(1, Math.floor(bytes))
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
function collection(name) {
|
|
1334
|
+
return decorateCollection(Object.freeze({
|
|
1335
|
+
kind: "collection",
|
|
1336
|
+
name,
|
|
1337
|
+
singleFile: false,
|
|
1338
|
+
acceptedMimeTypes: Object.freeze([]),
|
|
1339
|
+
acceptedExtensions: Object.freeze([])
|
|
1340
|
+
}));
|
|
1341
|
+
}
|
|
1342
|
+
function normalizeCollectionDefinitions(definitions) {
|
|
1343
|
+
return Object.freeze(definitions.map((definition) => {
|
|
1344
|
+
return Object.freeze({
|
|
1345
|
+
...definition,
|
|
1346
|
+
disk: definition.diskName?.trim() || void 0,
|
|
1347
|
+
conversionsDisk: definition.conversionsDiskName?.trim() || void 0,
|
|
1348
|
+
singleFile: Boolean(definition.singleFile),
|
|
1349
|
+
onlyKeepLatest: typeof definition.onlyKeepLatest === "number" ? Math.max(1, Math.floor(definition.onlyKeepLatest)) : void 0,
|
|
1350
|
+
acceptedMimeTypes: Object.freeze(
|
|
1351
|
+
definition.acceptedMimeTypes.map((mimeType) => mimeType.trim()).map((mimeType) => mimeType.toLowerCase()).filter(Boolean)
|
|
1352
|
+
),
|
|
1353
|
+
acceptedExtensions: Object.freeze(
|
|
1354
|
+
definition.acceptedExtensions.map((extension) => extension.trim().replace(/^\./, "").toLowerCase()).filter(Boolean)
|
|
1355
|
+
),
|
|
1356
|
+
maxSize: typeof definition.maxFileSize === "number" ? Math.max(1, Math.floor(definition.maxFileSize)) : void 0
|
|
1357
|
+
});
|
|
1358
|
+
}));
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/definitions/conversions.ts
|
|
1362
|
+
function decorateConversion(definition) {
|
|
1363
|
+
const clone = (overrides) => decorateConversion(Object.freeze({
|
|
1364
|
+
...definition,
|
|
1365
|
+
...overrides
|
|
1366
|
+
}));
|
|
1367
|
+
return Object.freeze({
|
|
1368
|
+
kind: definition.kind,
|
|
1369
|
+
name: definition.name,
|
|
1370
|
+
definition,
|
|
1371
|
+
performOnCollections(...collections) {
|
|
1372
|
+
return clone({
|
|
1373
|
+
collections: Object.freeze(
|
|
1374
|
+
collections.filter(Boolean)
|
|
1375
|
+
)
|
|
1376
|
+
});
|
|
1377
|
+
},
|
|
1378
|
+
width(pixels) {
|
|
1379
|
+
return clone({
|
|
1380
|
+
width: Math.max(1, Math.floor(pixels))
|
|
1381
|
+
});
|
|
1382
|
+
},
|
|
1383
|
+
height(pixels) {
|
|
1384
|
+
return clone({
|
|
1385
|
+
height: Math.max(1, Math.floor(pixels))
|
|
1386
|
+
});
|
|
1387
|
+
},
|
|
1388
|
+
fit(mode) {
|
|
1389
|
+
return clone({ fit: mode });
|
|
1390
|
+
},
|
|
1391
|
+
format(format) {
|
|
1392
|
+
return clone({ format });
|
|
1393
|
+
},
|
|
1394
|
+
quality(value) {
|
|
1395
|
+
return clone({
|
|
1396
|
+
quality: Math.max(1, Math.min(100, Math.floor(value)))
|
|
1397
|
+
});
|
|
1398
|
+
},
|
|
1399
|
+
queued() {
|
|
1400
|
+
return clone({ queued: true });
|
|
1401
|
+
},
|
|
1402
|
+
nonQueued() {
|
|
1403
|
+
return clone({ queued: false });
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
function conversion(name) {
|
|
1408
|
+
return decorateConversion(Object.freeze({
|
|
1409
|
+
kind: "conversion",
|
|
1410
|
+
name,
|
|
1411
|
+
collections: Object.freeze([]),
|
|
1412
|
+
queued: false
|
|
1413
|
+
}));
|
|
1414
|
+
}
|
|
1415
|
+
function normalizeConversionDefinitions(definitions) {
|
|
1416
|
+
return Object.freeze(definitions.map((definition) => {
|
|
1417
|
+
return Object.freeze({
|
|
1418
|
+
...definition,
|
|
1419
|
+
collections: Object.freeze(
|
|
1420
|
+
definition.collections.filter(Boolean)
|
|
1421
|
+
),
|
|
1422
|
+
width: typeof definition.width === "number" ? Math.max(1, Math.floor(definition.width)) : void 0,
|
|
1423
|
+
height: typeof definition.height === "number" ? Math.max(1, Math.floor(definition.height)) : void 0,
|
|
1424
|
+
quality: typeof definition.quality === "number" ? Math.max(1, Math.min(100, Math.floor(definition.quality))) : void 0,
|
|
1425
|
+
queued: Boolean(definition.queued)
|
|
1426
|
+
});
|
|
1427
|
+
}));
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// src/definitions/config.ts
|
|
1431
|
+
function resolveMediaDefinition(input) {
|
|
1432
|
+
if (typeof input === "function") {
|
|
1433
|
+
return input({ collection, conversion });
|
|
1434
|
+
}
|
|
1435
|
+
return input;
|
|
1436
|
+
}
|
|
1437
|
+
function normalizeMediaDefinition(input) {
|
|
1438
|
+
const collections = normalizeCollectionDefinitions(
|
|
1439
|
+
(input.collections ?? []).map((item) => item.definition)
|
|
1440
|
+
);
|
|
1441
|
+
const conversions = normalizeConversionDefinitions(
|
|
1442
|
+
(input.conversions ?? []).map((item) => item.definition)
|
|
1443
|
+
);
|
|
1444
|
+
const collectionsByName = /* @__PURE__ */ Object.create(null);
|
|
1445
|
+
for (const definition of collections) {
|
|
1446
|
+
if (collectionsByName[definition.name]) {
|
|
1447
|
+
throw new Error(`[Holo Media] Duplicate media collection "${definition.name}".`);
|
|
1448
|
+
}
|
|
1449
|
+
collectionsByName[definition.name] = definition;
|
|
1450
|
+
}
|
|
1451
|
+
const conversionsByName = /* @__PURE__ */ Object.create(null);
|
|
1452
|
+
for (const definition of conversions) {
|
|
1453
|
+
if (conversionsByName[definition.name]) {
|
|
1454
|
+
throw new Error(`[Holo Media] Duplicate media conversion "${definition.name}".`);
|
|
1455
|
+
}
|
|
1456
|
+
for (const collectionName of definition.collections) {
|
|
1457
|
+
if (!collectionsByName[collectionName]) {
|
|
1458
|
+
throw new Error(
|
|
1459
|
+
`[Holo Media] Conversion "${definition.name}" references unknown collection "${collectionName}".`
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
conversionsByName[definition.name] = definition;
|
|
1464
|
+
}
|
|
1465
|
+
return Object.freeze({
|
|
1466
|
+
collections,
|
|
1467
|
+
conversions,
|
|
1468
|
+
collectionsByName: Object.freeze(collectionsByName),
|
|
1469
|
+
conversionsByName: Object.freeze(conversionsByName)
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/defineMediaModel.ts
|
|
1474
|
+
function defineMediaModel(model, definition) {
|
|
1475
|
+
installEntityMediaMethods();
|
|
1476
|
+
const resolvedDefinition = normalizeMediaDefinition(
|
|
1477
|
+
resolveMediaDefinition(definition)
|
|
1478
|
+
);
|
|
1479
|
+
registerMediaDefinition(model, resolvedDefinition);
|
|
1480
|
+
model.resolveRelationUsing("media", () => morphMany(
|
|
1481
|
+
() => Media,
|
|
1482
|
+
"model"
|
|
1483
|
+
));
|
|
1484
|
+
return model;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/index.ts
|
|
1488
|
+
installEntityMediaMethods();
|
|
1489
|
+
ensureMediaQueueJobRegistered();
|
|
1490
|
+
export {
|
|
1491
|
+
MEDIA_GENERATE_CONVERSIONS_JOB,
|
|
1492
|
+
Media,
|
|
1493
|
+
MediaAdder,
|
|
1494
|
+
MediaItem,
|
|
1495
|
+
collection,
|
|
1496
|
+
conversion,
|
|
1497
|
+
createDefaultMediaConversionExecutor,
|
|
1498
|
+
defaultMediaConversionExecutor,
|
|
1499
|
+
defineMediaModel,
|
|
1500
|
+
dispatchQueuedMediaConversions,
|
|
1501
|
+
ensureMediaQueueJobRegistered,
|
|
1502
|
+
getMediaConversionExecutor,
|
|
1503
|
+
getMediaDefinition,
|
|
1504
|
+
getMediaDefinitionForMorphClass,
|
|
1505
|
+
getMediaPathGenerator,
|
|
1506
|
+
normalizeCollectionDefinitions,
|
|
1507
|
+
normalizeConversionDefinitions,
|
|
1508
|
+
normalizeMediaDefinition,
|
|
1509
|
+
registerMediaDefinition,
|
|
1510
|
+
requireMediaDefinition,
|
|
1511
|
+
requireMediaDefinitionForMorphClass,
|
|
1512
|
+
resetMediaPathGenerator,
|
|
1513
|
+
resetMediaRuntime,
|
|
1514
|
+
resolveMediaCollection,
|
|
1515
|
+
resolveMediaConversion,
|
|
1516
|
+
resolveMediaDefinition,
|
|
1517
|
+
runMediaGenerateConversionsJob,
|
|
1518
|
+
setMediaConversionExecutor,
|
|
1519
|
+
setMediaPathGenerator
|
|
1520
|
+
};
|