@holo-js/media 0.3.28 → 0.4.1
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/LICENSE +21 -0
- package/dist/.holo-build-fingerprint +1 -0
- package/dist/index.d.ts +41 -21
- package/dist/index.mjs +466 -175
- package/dist/projectConfig.d.ts +3 -1
- package/package.json +19 -14
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Holo-JS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
da44bd2451fc664c7a2dd714cb1a1ff736befa82a1fd95a4912ec353f31b7b72
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _holo_js_db from '@holo-js/db';
|
|
2
|
-
import { Entity, TableDefinition, ModelRecord, RelationMap, DynamicRelationResolver, ModelQueryBuilder, ModelRelationPath,
|
|
2
|
+
import { Entity, TableDefinition, ModelRecord, RelationMap, DynamicRelationResolver, ModelQueryBuilder, ModelRelationPath, ModelRelationName, RelationNameConstraintMapGuarded, RelatedColumnNameForRelationPath, AggregateRelationConstraintMap, AggregateRelatedColumnNameOfKeys, RelationConstraintFor, ResolveEagerLoads, ModelCollection, EntityWithLoaded, PaginatedResult, SimplePaginatedResult, CursorPaginatedResult, RelationConstraintMap } from '@holo-js/db';
|
|
3
3
|
import { StorageContent } from '@holo-js/storage/runtime';
|
|
4
4
|
export { HoloMediaConfig, defineMediaConfig, normalizeMediaConfig } from './projectConfig.js';
|
|
5
5
|
import { QueueJobDefinition, QueueJsonValue } from '@holo-js/queue';
|
|
@@ -213,15 +213,21 @@ declare class MediaItem<TCollectionName extends string = string, TConversionName
|
|
|
213
213
|
|
|
214
214
|
type MediaSourceInput = string | BinaryContent | {
|
|
215
215
|
readonly path: string;
|
|
216
|
-
} | {
|
|
217
|
-
readonly url: string;
|
|
218
|
-
readonly fileName?: string;
|
|
219
|
-
readonly name?: string;
|
|
220
216
|
} | {
|
|
221
217
|
readonly contents: BinaryContent;
|
|
222
218
|
readonly fileName?: string;
|
|
223
219
|
readonly name?: string;
|
|
224
220
|
};
|
|
221
|
+
declare class RemoteMediaSource {
|
|
222
|
+
readonly url: string;
|
|
223
|
+
constructor(url: string);
|
|
224
|
+
}
|
|
225
|
+
declare class StoredMediaSource {
|
|
226
|
+
readonly disk: string;
|
|
227
|
+
readonly path: string;
|
|
228
|
+
constructor(disk: string, path: string);
|
|
229
|
+
}
|
|
230
|
+
type MediaAdderSource = MediaSourceInput | RemoteMediaSource | StoredMediaSource;
|
|
225
231
|
type MediaAddErrorCode = 'max_size_exceeded' | 'invalid_mime_type' | 'invalid_extension';
|
|
226
232
|
type MediaAddError = {
|
|
227
233
|
readonly code: MediaAddErrorCode;
|
|
@@ -244,7 +250,7 @@ declare class MediaAdder<TEntity extends Entity<TableDefinition> = Entity<TableD
|
|
|
244
250
|
private readonly entity;
|
|
245
251
|
private readonly source;
|
|
246
252
|
private readonly state;
|
|
247
|
-
constructor(entity: TEntity, source:
|
|
253
|
+
constructor(entity: TEntity, source: MediaAdderSource, state?: {
|
|
248
254
|
readonly fileName?: string;
|
|
249
255
|
readonly name?: string;
|
|
250
256
|
readonly disk?: string;
|
|
@@ -262,6 +268,7 @@ type MediaEnabledEntity<TTable extends TableDefinition = TableDefinition, TColle
|
|
|
262
268
|
type MediaEnabledEntityMethods<TCollectionName extends string, TConversionName extends string> = {
|
|
263
269
|
addMedia(source: MediaSourceInput): MediaAdder<Entity<TableDefinition>, TCollectionName, TConversionName>;
|
|
264
270
|
addMediaFromUrl(url: string): MediaAdder<Entity<TableDefinition>, TCollectionName, TConversionName>;
|
|
271
|
+
addMediaFromDisk(path: string, disk: string): MediaAdder<Entity<TableDefinition>, TCollectionName, TConversionName>;
|
|
265
272
|
getMedia(collectionName?: TCollectionName): Promise<MediaItem<TCollectionName, TConversionName, Entity<TableDefinition>>[]>;
|
|
266
273
|
getMediaUrls(collectionName?: TCollectionName, conversionName?: TConversionName): Promise<string[]>;
|
|
267
274
|
getMediaPaths(collectionName?: TCollectionName, conversionName?: TConversionName): Promise<string[]>;
|
|
@@ -302,15 +309,16 @@ type MediaEnabledFunction<TFunction, TCollectionName extends string, TConversion
|
|
|
302
309
|
(...args: TArgs1): MediaEnabledResult<TReturn1, TCollectionName, TConversionName>;
|
|
303
310
|
(...args: TArgs2): MediaEnabledResult<TReturn2, TCollectionName, TConversionName>;
|
|
304
311
|
} : TFunction extends (...args: infer TArgs) => infer TReturn ? (...args: TArgs) => MediaEnabledResult<TReturn, TCollectionName, TConversionName> : TFunction;
|
|
312
|
+
type MediaRelationCallbackMethod = 'whereHas' | 'orWhereHas' | 'whereDoesntHave' | 'orWhereDoesntHave' | 'whereRelation' | 'orWhereRelation' | 'withWhereHas' | 'withCount' | 'withExists' | 'withSum' | 'withAvg' | 'withMin' | 'withMax';
|
|
305
313
|
type MediaEnabledQueryBuilder<TBuilder, TCollectionName extends string, TConversionName extends string> = {
|
|
306
|
-
[K in keyof TBuilder]: K extends 'with' ? MediaEnabledQueryBuilderWith<TBuilder, TCollectionName, TConversionName> : K extends 'withCount' | 'withExists' ? MediaEnabledRelationAggregate<TBuilder, TCollectionName, TConversionName> : K extends 'withSum' | 'withAvg' | 'withMin' | 'withMax' ? MediaEnabledRelationColumnAggregate<TBuilder, TCollectionName, TConversionName> : MediaEnabledFunction<TBuilder[K], TCollectionName, TConversionName>;
|
|
314
|
+
[K in keyof TBuilder]: K extends 'with' ? MediaEnabledQueryBuilderWith<TBuilder, TCollectionName, TConversionName> : K extends 'withCount' | 'withExists' ? MediaEnabledRelationAggregate<TBuilder, TCollectionName, TConversionName> : K extends 'withSum' | 'withAvg' | 'withMin' | 'withMax' ? MediaEnabledRelationColumnAggregate<TBuilder, TCollectionName, TConversionName> : K extends 'whereHas' | 'orWhereHas' | 'whereDoesntHave' | 'orWhereDoesntHave' ? MediaEnabledRelationFilter<TBuilder, TCollectionName, TConversionName> : K extends 'whereRelation' | 'orWhereRelation' ? MediaEnabledRelationColumnFilter<TBuilder, TCollectionName, TConversionName> : K extends 'withWhereHas' ? MediaEnabledWithWhereHas<TBuilder, TCollectionName, TConversionName> : MediaEnabledFunction<TBuilder[K], TCollectionName, TConversionName>;
|
|
307
315
|
};
|
|
308
316
|
type MediaEnabledPaginatedResult<TResult, TPaginator, TCollectionName extends string, TConversionName extends string> = Omit<TResult, keyof TPaginator> & TPaginator & (TResult extends {
|
|
309
317
|
readonly data: infer TData;
|
|
310
318
|
} ? {
|
|
311
319
|
readonly data: MediaEnabledResult<TData, TCollectionName, TConversionName>;
|
|
312
320
|
} : Record<never, never>);
|
|
313
|
-
type MediaEnabledResult<TValue, TCollectionName extends string, TConversionName extends string> = TValue extends Promise<infer TResult> ? Promise<MediaEnabledResult<TResult, TCollectionName, TConversionName>> : TValue extends AsyncGenerator<infer TYield, infer TReturn, infer TNext> ? AsyncGenerator<MediaEnabledResult<TYield, TCollectionName, TConversionName>, TReturn, TNext> : TValue extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded
|
|
321
|
+
type MediaEnabledResult<TValue, TCollectionName extends string, TConversionName extends string> = TValue extends Promise<infer TResult> ? Promise<MediaEnabledResult<TResult, TCollectionName, TConversionName>> : TValue extends AsyncGenerator<infer TYield, infer TReturn, infer TNext> ? AsyncGenerator<MediaEnabledResult<TYield, TCollectionName, TConversionName>, TReturn, TNext> : TValue extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded, infer TScopes extends Readonly<Record<string, unknown>>> ? MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName> : TValue extends ModelCollection<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TItem, infer TScopes extends Readonly<Record<string, unknown>>> ? TItem extends Entity<TTable, TRelations> ? ModelCollection<TTable, TRelations, MediaEnabledEntityResult<TItem, TCollectionName, TConversionName>, TScopes> : TValue : TValue extends PaginatedResult<infer TItem> ? MediaEnabledPaginatedResult<TValue, PaginatedResult<MediaEnabledResult<TItem, TCollectionName, TConversionName>>, TCollectionName, TConversionName> : TValue extends SimplePaginatedResult<infer TItem> ? MediaEnabledPaginatedResult<TValue, SimplePaginatedResult<MediaEnabledResult<TItem, TCollectionName, TConversionName>>, TCollectionName, TConversionName> : TValue extends CursorPaginatedResult<infer TItem> ? MediaEnabledPaginatedResult<TValue, CursorPaginatedResult<MediaEnabledResult<TItem, TCollectionName, TConversionName>>, TCollectionName, TConversionName> : MediaEnabledEntityResult<TValue, TCollectionName, TConversionName>;
|
|
314
322
|
type MediaModelStatic = {
|
|
315
323
|
readonly definition: {
|
|
316
324
|
readonly name: string;
|
|
@@ -320,28 +328,39 @@ type MediaModelStatic = {
|
|
|
320
328
|
readonly resolveRelationUsing: (name: string, resolver: DynamicRelationResolver) => unknown;
|
|
321
329
|
};
|
|
322
330
|
type MediaRelationPath<TRelations extends RelationMap> = ModelRelationPath<TRelations>;
|
|
323
|
-
type
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
<
|
|
328
|
-
(relations: MediaRelationConstraintMap<TRelations>): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded>, TCollectionName, TConversionName>;
|
|
329
|
-
<TPaths extends readonly MediaRelationPath<TRelations>[]>(relations: TPaths): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded & ResolveEagerLoads<TRelations, TPaths>>, TCollectionName, TConversionName>;
|
|
331
|
+
type MediaEnabledQueryBuilderWith<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded, infer TScopes extends Readonly<Record<string, unknown>>> ? {
|
|
332
|
+
<TPaths extends readonly MediaRelationPath<TRelations>[]>(...relations: TPaths): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded & ResolveEagerLoads<TRelations, TPaths>, TScopes>, TCollectionName, TConversionName>;
|
|
333
|
+
<TPath extends MediaRelationPath<TRelations>>(relation: TPath, constraint: RelationConstraintFor<TRelations, TPath>): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded & ResolveEagerLoads<TRelations, readonly [TPath]>, TScopes>, TCollectionName, TConversionName>;
|
|
334
|
+
(relations: RelationConstraintMap<TRelations>): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName>;
|
|
335
|
+
<TPaths extends readonly MediaRelationPath<TRelations>[]>(relations: TPaths): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded & ResolveEagerLoads<TRelations, TPaths>, TScopes>, TCollectionName, TConversionName>;
|
|
330
336
|
} : TBuilder extends {
|
|
331
337
|
with: infer TWith;
|
|
332
338
|
} ? MediaEnabledFunction<TWith, TCollectionName, TConversionName> : never;
|
|
333
|
-
type MediaEnabledRelationAggregate<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded
|
|
334
|
-
(...
|
|
335
|
-
(relations:
|
|
339
|
+
type MediaEnabledRelationAggregate<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded, infer TScopes extends Readonly<Record<string, unknown>>> ? {
|
|
340
|
+
(first: ModelRelationName<TRelations>, ...rest: readonly ModelRelationName<TRelations>[]): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName>;
|
|
341
|
+
<TConstraints extends RelationNameConstraintMapGuarded<TRelations, TConstraints>>(relations: TConstraints): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName>;
|
|
336
342
|
} : never;
|
|
337
|
-
type
|
|
343
|
+
type DistributiveKeysOfObject<TConstraints> = TConstraints extends unknown ? keyof TConstraints : never;
|
|
344
|
+
type AggregateKeysOfObject<TConstraints> = TConstraints extends unknown ? `${Extract<keyof TConstraints, string | number>}` : never;
|
|
345
|
+
type MediaEnabledRelationColumnAggregate<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded, infer TScopes extends Readonly<Record<string, unknown>>> ? {
|
|
346
|
+
<TRelationName extends ModelRelationName<TRelations>>(first: TRelationName, column: RelatedColumnNameForRelationPath<TRelations, TRelationName>, ...rest: readonly ModelRelationName<TRelations>[]): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName>;
|
|
347
|
+
<TConstraints extends AggregateRelationConstraintMap<TRelations, Extract<keyof TConstraints, string>>, _TNoExplicitArgument extends never>(first: TConstraints, column: AggregateRelatedColumnNameOfKeys<TRelations, AggregateKeysOfObject<TConstraints>>, ...rest: readonly ModelRelationName<TRelations>[]): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName>;
|
|
348
|
+
<TKey extends string>(first: NoInfer<TKey> | AggregateRelationConstraintMap<TRelations, TKey>, column: AggregateRelatedColumnNameOfKeys<TRelations, TKey>, ...rest: readonly ModelRelationName<TRelations>[]): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName>;
|
|
349
|
+
<TConstraints extends RelationNameConstraintMapGuarded<TRelations, TConstraints> & ([DistributiveKeysOfObject<TConstraints>] extends [ModelRelationName<TRelations>] ? unknown : never), _TNoExplicitArgument extends never>(first: TConstraints, column: RelatedColumnNameForRelationPath<TRelations, ModelRelationName<TRelations>>, ...rest: readonly ModelRelationName<TRelations>[]): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName>;
|
|
350
|
+
} : never;
|
|
351
|
+
type MediaEnabledRelationFilter<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded, infer TScopes extends Readonly<Record<string, unknown>>> ? <TPath extends MediaRelationPath<TRelations>>(relation: TPath, constraint?: RelationConstraintFor<TRelations, TPath>) => MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName> : never;
|
|
352
|
+
type MediaEnabledRelationColumnFilter<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded, infer TScopes extends Readonly<Record<string, unknown>>> ? <TRelationPath extends MediaRelationPath<TRelations>>(relation: TRelationPath, column: RelatedColumnNameForRelationPath<TRelations, TRelationPath>, operator: unknown, value?: unknown) => MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded, TScopes>, TCollectionName, TConversionName> : never;
|
|
353
|
+
type MediaEnabledWithWhereHas<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded, infer TScopes extends Readonly<Record<string, unknown>>> ? <TPath extends MediaRelationPath<TRelations>>(relation: TPath, constraint?: RelationConstraintFor<TRelations, TPath>) => MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded & ResolveEagerLoads<TRelations, readonly [TPath]>, TScopes>, TCollectionName, TConversionName> : never;
|
|
338
354
|
type MediaEnabledStaticWith<TModel extends MediaModelStatic, TCollectionName extends string, TConversionName extends string> = TModel extends {
|
|
339
355
|
query(): infer TBuilder;
|
|
340
356
|
} ? MediaEnabledQueryBuilderWith<TBuilder, TCollectionName, TConversionName> : TModel extends {
|
|
341
357
|
with: infer TWith;
|
|
342
358
|
} ? MediaEnabledFunction<TWith, TCollectionName, TConversionName> : never;
|
|
359
|
+
type MediaEnabledStaticRelationCallbackMethod<TModel, TKey extends keyof TModel, TCollectionName extends string, TConversionName extends string> = TModel extends {
|
|
360
|
+
query(): infer TBuilder;
|
|
361
|
+
} ? TKey extends keyof TBuilder ? MediaEnabledQueryBuilder<TBuilder, TCollectionName, TConversionName>[TKey] : never : never;
|
|
343
362
|
type MediaEnabledModel<TModel extends MediaModelStatic, TDefinition extends MediaDefinitionInput> = {
|
|
344
|
-
[K in keyof TModel]: K extends 'with' ? MediaEnabledStaticWith<TModel, MediaCollectionName<TDefinition>, MediaConversionName<TDefinition>> : MediaEnabledFunction<TModel[K], MediaCollectionName<TDefinition>, MediaConversionName<TDefinition>>;
|
|
363
|
+
[K in keyof TModel]: K extends 'with' ? MediaEnabledStaticWith<TModel, MediaCollectionName<TDefinition>, MediaConversionName<TDefinition>> : K extends MediaRelationCallbackMethod ? MediaEnabledStaticRelationCallbackMethod<TModel, K, MediaCollectionName<TDefinition>, MediaConversionName<TDefinition>> : MediaEnabledFunction<TModel[K], MediaCollectionName<TDefinition>, MediaConversionName<TDefinition>>;
|
|
345
364
|
};
|
|
346
365
|
declare function defineMediaModel<TModel extends MediaModelStatic, const TDefinition extends MediaDefinitionInput>(model: TModel, definition: TDefinition | MediaDefinitionFactory<TDefinition>): MediaEnabledModel<TModel, TDefinition>;
|
|
347
366
|
|
|
@@ -366,4 +385,5 @@ declare function dispatchQueuedMediaConversions(payload: MediaGenerateConversion
|
|
|
366
385
|
declare function createDefaultMediaConversionExecutor(): MediaConversionExecutor;
|
|
367
386
|
declare const defaultMediaConversionExecutor: MediaConversionExecutor;
|
|
368
387
|
|
|
369
|
-
export {
|
|
388
|
+
export { MEDIA_GENERATE_CONVERSIONS_JOB, Media, MediaAdder, MediaItem, collection, conversion, createDefaultMediaConversionExecutor, defaultMediaConversionExecutor, defineMediaModel, dispatchQueuedMediaConversions, ensureMediaQueueJobRegistered, getMediaConversionExecutor, getMediaDefinition, getMediaDefinitionForMorphClass, getMediaPathGenerator, normalizeCollectionDefinitions, normalizeConversionDefinitions, normalizeMediaDefinition, registerMediaDefinition, requireMediaDefinition, requireMediaDefinitionForMorphClass, resetMediaPathGenerator, resetMediaRuntime, resolveMediaCollection, resolveMediaConversion, resolveMediaDefinition, runMediaGenerateConversionsJob, setMediaConversionExecutor, setMediaPathGenerator };
|
|
389
|
+
export type { CollectionNamesOf, ConversionNamesOf, MediaAddResult, MediaCollectionBuilder, MediaCollectionDefinition, MediaConversionBuilder, MediaConversionDefinition, MediaConversionExecutor, MediaConversionExecutorInput, MediaConversionExecutorResult, MediaConversionExecutorSource, MediaConversionFit, MediaConversionFormat, MediaConversionPathInput, MediaDefinitionFactory, MediaDefinitionHelpers, MediaDefinitionInput, MediaEnabledEntity, MediaEnabledModel, MediaGenerateConversionsPayload, MediaGenerateConversionsResult, MediaOriginalPathInput, MediaPathGenerator, MediaSourceInput, NormalizedMediaCollectionDefinition, NormalizedMediaConversionDefinition, NormalizedMediaDefinition, StoredMediaConversion };
|
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { Entity } from "@holo-js/db";
|
|
|
11
11
|
|
|
12
12
|
// src/runtime/image.ts
|
|
13
13
|
import { mimeTypeFromFileName } from "@holo-js/kernel/file-content";
|
|
14
|
+
import { imageDecodingLimits } from "@holo-js/kernel/image-decoding";
|
|
14
15
|
|
|
15
16
|
// src/runtime/binary.ts
|
|
16
17
|
import { basename, extname } from "path";
|
|
@@ -102,10 +103,16 @@ function createDefaultMediaConversionExecutor() {
|
|
|
102
103
|
const outputFormat = normalizeOutputFormat(source.extension, conversion2.format);
|
|
103
104
|
try {
|
|
104
105
|
const sharp = await loadSharp();
|
|
105
|
-
|
|
106
|
+
const image = sharp(await toBuffer(source.contents), {
|
|
106
107
|
animated: true,
|
|
107
|
-
failOn: "warning"
|
|
108
|
-
|
|
108
|
+
failOn: "warning",
|
|
109
|
+
limitInputPixels: imageDecodingLimits.maximumDecodedPixels
|
|
110
|
+
});
|
|
111
|
+
const metadata = await image.metadata();
|
|
112
|
+
if ((metadata.pages ?? 1) > imageDecodingLimits.maximumFrames) {
|
|
113
|
+
throw new Error(`The image has more than ${imageDecodingLimits.maximumFrames} frames.`);
|
|
114
|
+
}
|
|
115
|
+
let pipeline = image.rotate();
|
|
109
116
|
if (conversion2.width || conversion2.height) {
|
|
110
117
|
pipeline = pipeline.resize({
|
|
111
118
|
width: conversion2.width,
|
|
@@ -150,7 +157,8 @@ function createDefaultMediaConversionExecutor() {
|
|
|
150
157
|
};
|
|
151
158
|
} catch (error) {
|
|
152
159
|
throw new Error(
|
|
153
|
-
`[Holo Media] Failed to generate conversion "${conversion2.name}" for "${source.fileName}": ${error.message}
|
|
160
|
+
`[Holo Media] Failed to generate conversion "${conversion2.name}" for "${source.fileName}": ${error.message}`,
|
|
161
|
+
{ cause: error }
|
|
154
162
|
);
|
|
155
163
|
}
|
|
156
164
|
}
|
|
@@ -346,8 +354,14 @@ import { BlockList, isIP } from "net";
|
|
|
346
354
|
import { tmpdir } from "os";
|
|
347
355
|
import { basename as basename2, resolve, sep } from "path";
|
|
348
356
|
import { Readable } from "stream";
|
|
357
|
+
import { config } from "@holo-js/config";
|
|
358
|
+
import { localStoragePathInternals } from "@holo-js/kernel";
|
|
349
359
|
import { inspectFileContent } from "@holo-js/kernel/file-content";
|
|
350
|
-
import {
|
|
360
|
+
import {
|
|
361
|
+
Storage as Storage4,
|
|
362
|
+
StorageMetadataUnsupportedError,
|
|
363
|
+
StorageStreamingUnsupportedError
|
|
364
|
+
} from "@holo-js/storage/runtime";
|
|
351
365
|
|
|
352
366
|
// src/queue.ts
|
|
353
367
|
import {
|
|
@@ -359,6 +373,7 @@ import {
|
|
|
359
373
|
import { connectionAsyncContext } from "@holo-js/db";
|
|
360
374
|
|
|
361
375
|
// src/model/conversions.ts
|
|
376
|
+
import { randomBytes } from "crypto";
|
|
362
377
|
import { mimeTypeFromFileName as mimeTypeFromFileName2 } from "@holo-js/kernel/file-content";
|
|
363
378
|
import { Storage as Storage2 } from "@holo-js/storage/runtime";
|
|
364
379
|
|
|
@@ -366,21 +381,16 @@ import { Storage as Storage2 } from "@holo-js/storage/runtime";
|
|
|
366
381
|
import { DB } from "@holo-js/db";
|
|
367
382
|
import { Storage } from "@holo-js/storage/runtime";
|
|
368
383
|
var MediaMutation = class {
|
|
369
|
-
|
|
384
|
+
created = [];
|
|
370
385
|
async put(disk, path, contents) {
|
|
371
|
-
|
|
372
|
-
this.written.push({ disk, path, previous });
|
|
386
|
+
this.created.push({ disk, path });
|
|
373
387
|
await Storage.disk(disk).put(path, contents);
|
|
374
388
|
}
|
|
375
389
|
async compensate() {
|
|
376
390
|
const failures = [];
|
|
377
|
-
for (const file of this.
|
|
391
|
+
for (const file of this.created.splice(0).reverse()) {
|
|
378
392
|
try {
|
|
379
|
-
|
|
380
|
-
await Storage.disk(file.disk).put(file.path, file.previous);
|
|
381
|
-
} else {
|
|
382
|
-
await Storage.disk(file.disk).delete(file.path);
|
|
383
|
-
}
|
|
393
|
+
await Storage.disk(file.disk).delete(file.path);
|
|
384
394
|
} catch (error) {
|
|
385
395
|
failures.push(error);
|
|
386
396
|
}
|
|
@@ -389,41 +399,62 @@ var MediaMutation = class {
|
|
|
389
399
|
if (failures.length > 1) throw new AggregateError(failures, "[Holo Media] File compensation failed.");
|
|
390
400
|
}
|
|
391
401
|
};
|
|
402
|
+
async function compensateAfterFailure(mutation, error) {
|
|
403
|
+
try {
|
|
404
|
+
await mutation.compensate();
|
|
405
|
+
} catch (cleanupError) {
|
|
406
|
+
return new AggregateError([error, cleanupError], "[Holo Media] File compensation failed.");
|
|
407
|
+
}
|
|
408
|
+
return error;
|
|
409
|
+
}
|
|
392
410
|
async function runMediaMutation(options) {
|
|
393
411
|
const mutation = new MediaMutation();
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
failures
|
|
406
|
-
}
|
|
407
|
-
if (failures.length === 1) throw failures[0];
|
|
408
|
-
if (failures.length > 1) throw new AggregateError(failures, "[Holo Media] Rollback restoration failed.");
|
|
409
|
-
});
|
|
410
|
-
const result = await options.operation(mutation);
|
|
411
|
-
transaction.afterCommit(async () => {
|
|
412
|
-
const failures = [];
|
|
413
|
-
for (const effect of options.afterCommit ?? []) {
|
|
412
|
+
let written;
|
|
413
|
+
try {
|
|
414
|
+
written = await options.write(mutation);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
throw await compensateAfterFailure(mutation, error);
|
|
417
|
+
}
|
|
418
|
+
let transactionStarted = false;
|
|
419
|
+
try {
|
|
420
|
+
return await DB.writeTransaction(async (transaction) => {
|
|
421
|
+
transactionStarted = true;
|
|
422
|
+
transaction.afterRollback(async () => {
|
|
423
|
+
const failures = [];
|
|
414
424
|
try {
|
|
415
|
-
await
|
|
425
|
+
await mutation.compensate();
|
|
416
426
|
} catch (error) {
|
|
417
427
|
failures.push(error);
|
|
418
428
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
429
|
+
try {
|
|
430
|
+
await options.afterRollback?.();
|
|
431
|
+
} catch (error) {
|
|
432
|
+
failures.push(error);
|
|
433
|
+
}
|
|
434
|
+
if (failures.length === 1) throw failures[0];
|
|
435
|
+
if (failures.length > 1) throw new AggregateError(failures, "[Holo Media] Rollback restoration failed.");
|
|
436
|
+
});
|
|
437
|
+
const result = await options.operation(mutation, written);
|
|
438
|
+
transaction.afterCommit(async () => {
|
|
439
|
+
const failures = [];
|
|
440
|
+
for (const effect of options.afterCommit ?? []) {
|
|
441
|
+
try {
|
|
442
|
+
await effect(result);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
failures.push(error);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
options.afterEffects?.();
|
|
448
|
+
if (failures.length === 0) return;
|
|
449
|
+
const cause = failures.length === 1 ? failures[0] : new AggregateError(failures, "[Holo Media] Post-commit effects failed.");
|
|
450
|
+
throw new Error(options.committedMessage, { cause });
|
|
451
|
+
});
|
|
452
|
+
return result;
|
|
424
453
|
});
|
|
425
|
-
|
|
426
|
-
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (transactionStarted) throw error;
|
|
456
|
+
throw await compensateAfterFailure(mutation, error);
|
|
457
|
+
}
|
|
427
458
|
}
|
|
428
459
|
async function removeStoredFiles(files, retained = []) {
|
|
429
460
|
const failures = [];
|
|
@@ -451,6 +482,7 @@ async function deleteMediaItems(items, owner) {
|
|
|
451
482
|
if (items.length === 0) return;
|
|
452
483
|
await runMediaMutation({
|
|
453
484
|
committedMessage: "[Holo Media] Deletion remains committed; post-commit file cleanup failed.",
|
|
485
|
+
write: async () => void 0,
|
|
454
486
|
operation: async () => {
|
|
455
487
|
for (const item of items) await item.getEntity().delete();
|
|
456
488
|
owner?.forgetRelation("media");
|
|
@@ -531,7 +563,13 @@ function resolveQueuedConversionNames(options) {
|
|
|
531
563
|
).filter((conversion2) => conversion2.queued).map((conversion2) => conversion2.name)
|
|
532
564
|
);
|
|
533
565
|
}
|
|
534
|
-
|
|
566
|
+
function withUniqueSuffix(path) {
|
|
567
|
+
const token = randomBytes(8).toString("hex");
|
|
568
|
+
const slash = path.lastIndexOf("/");
|
|
569
|
+
const dot = path.lastIndexOf(".");
|
|
570
|
+
return dot > slash + 1 ? `${path.slice(0, dot)}.${token}${path.slice(dot)}` : `${path}.${token}`;
|
|
571
|
+
}
|
|
572
|
+
async function writeStoredConversions(options) {
|
|
535
573
|
const requested = normalizeRequestedConversions(options.requestedConversions);
|
|
536
574
|
const matchingConversions = resolveMatchingConversions(
|
|
537
575
|
options.definition,
|
|
@@ -539,9 +577,6 @@ async function generateStoredConversions(options) {
|
|
|
539
577
|
requested,
|
|
540
578
|
{ includeQueued: options.includeQueued === true }
|
|
541
579
|
);
|
|
542
|
-
if (matchingConversions.length === 0) {
|
|
543
|
-
return Object.freeze({});
|
|
544
|
-
}
|
|
545
580
|
const generatedConversions = /* @__PURE__ */ Object.create(null);
|
|
546
581
|
const executor = getMediaConversionExecutor();
|
|
547
582
|
for (const conversion2 of matchingConversions) {
|
|
@@ -554,7 +589,7 @@ async function generateStoredConversions(options) {
|
|
|
554
589
|
continue;
|
|
555
590
|
}
|
|
556
591
|
const generatedFileName = sanitizeFileName(generated.fileName ?? options.source.fileName);
|
|
557
|
-
const
|
|
592
|
+
const plannedPath = getMediaPathGenerator().conversionPath({
|
|
558
593
|
uuid: options.source.uuid,
|
|
559
594
|
fileName: options.source.fileName,
|
|
560
595
|
extension: options.source.extension,
|
|
@@ -562,27 +597,22 @@ async function generateStoredConversions(options) {
|
|
|
562
597
|
conversion: conversion2,
|
|
563
598
|
generatedFileName
|
|
564
599
|
});
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
const
|
|
568
|
-
await options.mutation.put(
|
|
600
|
+
const path = options.unique ? withUniqueSuffix(plannedPath) : plannedPath;
|
|
601
|
+
const disk = generated.disk ?? options.conversionsDisk;
|
|
602
|
+
const contents = await toBinaryContent(generated.contents);
|
|
603
|
+
await options.mutation.put(disk, path, contents);
|
|
569
604
|
generatedConversions[conversion2.name] = Object.freeze({
|
|
570
|
-
path
|
|
571
|
-
disk
|
|
605
|
+
path,
|
|
606
|
+
disk,
|
|
572
607
|
fileName: generatedFileName,
|
|
573
|
-
mimeType:
|
|
574
|
-
size: getContentSize(
|
|
608
|
+
mimeType: generated.mimeType?.trim() || mimeTypeFromFileName2(generatedFileName) || void 0,
|
|
609
|
+
size: getContentSize(contents)
|
|
575
610
|
});
|
|
576
611
|
}
|
|
577
612
|
return Object.freeze(generatedConversions);
|
|
578
613
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (requested?.length && !requested.includes(name) || !conversion2?.path) return false;
|
|
582
|
-
const nextConversion = next[name];
|
|
583
|
-
return nextConversion?.path !== conversion2.path || (nextConversion.disk ?? fallbackDisk) !== (conversion2.disk ?? fallbackDisk);
|
|
584
|
-
}).map(([, conversion2]) => ({ disk: conversion2.disk ?? fallbackDisk, path: conversion2.path }));
|
|
585
|
-
await removeStoredFiles(obsolete);
|
|
614
|
+
function obsoleteConversionFiles(current, next, fallbackDisk) {
|
|
615
|
+
return Object.entries(current).filter(([name, conversion2]) => conversion2?.path && next[name]?.path !== conversion2.path).map(([, conversion2]) => ({ disk: conversion2.disk ?? fallbackDisk, path: conversion2.path }));
|
|
586
616
|
}
|
|
587
617
|
async function regenerateMediaEntityConversions(options) {
|
|
588
618
|
const media = options.media;
|
|
@@ -591,7 +621,6 @@ async function regenerateMediaEntityConversions(options) {
|
|
|
591
621
|
const collectionName = String(media.get("collection_name"));
|
|
592
622
|
const collection2 = resolveCollectionDefinition(definition, collectionName);
|
|
593
623
|
const requested = normalizeRequestedConversions(options.conversions);
|
|
594
|
-
const current = media.get("generated_conversions") ?? {};
|
|
595
624
|
const conversionsDisk = String(media.get("conversions_disk") ?? media.get("disk"));
|
|
596
625
|
resolveMatchingConversions(definition, collectionName, requested, {
|
|
597
626
|
includeQueued: options.includeQueued === true
|
|
@@ -602,46 +631,56 @@ async function regenerateMediaEntityConversions(options) {
|
|
|
602
631
|
`[Holo Media] Cannot regenerate conversions for media "${media.get("uuid")}" because the original file is missing.`
|
|
603
632
|
);
|
|
604
633
|
}
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
) : {};
|
|
608
|
-
return await runMediaMutation({
|
|
634
|
+
const mediaId = media.get("id");
|
|
635
|
+
const result = await runMediaMutation({
|
|
609
636
|
committedMessage: "[Holo Media] Regeneration remains committed; post-commit cleanup or conversion dispatch failed.",
|
|
610
637
|
afterRollback: async () => {
|
|
611
638
|
options.owner?.forgetRelation("media");
|
|
612
639
|
await media.refresh();
|
|
613
640
|
},
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
641
|
+
write: async (mutation) => await writeStoredConversions({
|
|
642
|
+
definition,
|
|
643
|
+
collection: collection2,
|
|
644
|
+
conversionsDisk,
|
|
645
|
+
requestedConversions: requested,
|
|
646
|
+
includeQueued: options.includeQueued === true,
|
|
647
|
+
unique: true,
|
|
648
|
+
mutation,
|
|
649
|
+
source: {
|
|
650
|
+
uuid: String(media.get("uuid")),
|
|
651
|
+
fileName: String(media.get("file_name")),
|
|
652
|
+
extension: media.get("extension") ?? void 0,
|
|
653
|
+
mimeType: media.get("mime_type") ?? void 0,
|
|
654
|
+
size: sourceContents.byteLength,
|
|
655
|
+
contents: sourceContents
|
|
656
|
+
}
|
|
657
|
+
}),
|
|
658
|
+
operation: async (_mutation, regenerated) => {
|
|
659
|
+
const locked = await Media.query().where("id", mediaId).lockForUpdate().first();
|
|
660
|
+
if (!locked) {
|
|
661
|
+
throw new Error(`[Holo Media] Cannot regenerate conversions for media "${media.get("uuid")}" because the media row no longer exists.`);
|
|
662
|
+
}
|
|
663
|
+
const current = locked.get("generated_conversions") ?? {};
|
|
664
|
+
const nextBase = requested?.length ? Object.fromEntries(
|
|
665
|
+
Object.entries(current).filter(([name]) => !requested.includes(name))
|
|
666
|
+
) : {};
|
|
631
667
|
const generatedConversions = Object.freeze({ ...nextBase, ...regenerated });
|
|
632
668
|
media.forceFill({ generated_conversions: generatedConversions });
|
|
633
669
|
await media.save();
|
|
634
670
|
options.owner?.forgetRelation("media");
|
|
635
|
-
return
|
|
671
|
+
return {
|
|
672
|
+
generatedConversions,
|
|
673
|
+
obsolete: obsoleteConversionFiles(current, generatedConversions, conversionsDisk)
|
|
674
|
+
};
|
|
636
675
|
},
|
|
637
676
|
afterCommit: [
|
|
638
|
-
async (
|
|
639
|
-
await
|
|
677
|
+
async ({ obsolete }) => {
|
|
678
|
+
await removeStoredFiles(obsolete);
|
|
640
679
|
},
|
|
641
680
|
async () => {
|
|
642
681
|
if (options.includeQueued) return;
|
|
643
682
|
await dispatchQueuedMediaConversionsForModel({
|
|
644
|
-
mediaId
|
|
683
|
+
mediaId,
|
|
645
684
|
conversionNames: resolveQueuedConversionNames({ definition, collectionName, requestedConversions: requested })
|
|
646
685
|
}, async () => {
|
|
647
686
|
await media.refresh();
|
|
@@ -652,6 +691,7 @@ async function regenerateMediaEntityConversions(options) {
|
|
|
652
691
|
options.owner?.forgetRelation("media");
|
|
653
692
|
}
|
|
654
693
|
});
|
|
694
|
+
return result.generatedConversions;
|
|
655
695
|
}
|
|
656
696
|
|
|
657
697
|
// src/queue.ts
|
|
@@ -782,6 +822,8 @@ var MediaItem = class {
|
|
|
782
822
|
this.entity = entity;
|
|
783
823
|
this.owner = owner;
|
|
784
824
|
}
|
|
825
|
+
entity;
|
|
826
|
+
owner;
|
|
785
827
|
get record() {
|
|
786
828
|
return this.entity.toAttributes();
|
|
787
829
|
}
|
|
@@ -866,8 +908,34 @@ var MediaItem = class {
|
|
|
866
908
|
};
|
|
867
909
|
|
|
868
910
|
// src/model/adder.ts
|
|
911
|
+
var RemoteMediaSource = class {
|
|
912
|
+
constructor(url) {
|
|
913
|
+
this.url = url;
|
|
914
|
+
}
|
|
915
|
+
url;
|
|
916
|
+
};
|
|
917
|
+
var StoredMediaSource = class {
|
|
918
|
+
constructor(disk, path) {
|
|
919
|
+
this.disk = disk;
|
|
920
|
+
this.path = path;
|
|
921
|
+
}
|
|
922
|
+
disk;
|
|
923
|
+
path;
|
|
924
|
+
};
|
|
869
925
|
var DEFAULT_REMOTE_MEDIA_MAX_SIZE = 10 * 1024 * 1024;
|
|
926
|
+
var REMOTE_MEDIA_TIMEOUT_MS = 3e4;
|
|
927
|
+
var DEFAULT_MEDIA_DISK = "public";
|
|
870
928
|
var BLOCKED_REMOTE_MEDIA_ADDRESSES = createBlockedRemoteMediaAddresses();
|
|
929
|
+
var GLOBALLY_REACHABLE_BLOCKED_IPV4_ADDRESSES = createGloballyReachableBlockedIPv4Addresses();
|
|
930
|
+
var GLOBAL_UNICAST_IPV6_ADDRESSES = createGlobalUnicastIPv6Addresses();
|
|
931
|
+
var IETF_PROTOCOL_ASSIGNMENTS = createIETFProtocolAssignmentsBlockList();
|
|
932
|
+
var GLOBALLY_REACHABLE_IETF_PROTOCOL_ASSIGNMENTS = createGloballyReachableIETFProtocolAssignments();
|
|
933
|
+
var NAT64_WELL_KNOWN_PREFIX = Buffer.concat([Buffer.from([0, 100, 255, 155]), Buffer.alloc(8)]);
|
|
934
|
+
var SIX_TO_FOUR_PREFIX = Buffer.from([32, 2]);
|
|
935
|
+
var IPV4_COMPATIBLE_PREFIX = Buffer.alloc(12);
|
|
936
|
+
var TRANSLATED_IPV4_PREFIX = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0]);
|
|
937
|
+
var IPV4_MAPPED_PREFIX = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255]);
|
|
938
|
+
var TEREDO_PREFIX = Buffer.from([32, 1, 0, 0]);
|
|
871
939
|
var LOCAL_MEDIA_SOURCE_ROOTS = Object.freeze([
|
|
872
940
|
tmpdir(),
|
|
873
941
|
resolve(process.cwd(), "storage")
|
|
@@ -877,9 +945,9 @@ var defaultRemoteMediaAddressResolver = async (hostname) => await lookup(hostnam
|
|
|
877
945
|
verbatim: true
|
|
878
946
|
});
|
|
879
947
|
function createRemoteMediaDownloader(resolver, requester) {
|
|
880
|
-
return async (url) => {
|
|
948
|
+
return async (url, signal) => {
|
|
881
949
|
const address = await resolveRemoteMediaAddress(url, resolver);
|
|
882
|
-
return await requester(url, address);
|
|
950
|
+
return await requester(url, address, signal);
|
|
883
951
|
};
|
|
884
952
|
}
|
|
885
953
|
var defaultRemoteMediaDownloader = createRemoteMediaDownloader(defaultRemoteMediaAddressResolver, requestRemoteMedia);
|
|
@@ -893,19 +961,14 @@ var MediaAddValidationException = class extends Error {
|
|
|
893
961
|
this.error = error;
|
|
894
962
|
}
|
|
895
963
|
};
|
|
896
|
-
function
|
|
897
|
-
|
|
898
|
-
if (defaultDisk.visibility === "public") {
|
|
899
|
-
return defaultDisk.name;
|
|
900
|
-
}
|
|
964
|
+
function resolveMediaDefaultDisk() {
|
|
965
|
+
let configuredDisk;
|
|
901
966
|
try {
|
|
902
|
-
|
|
903
|
-
if (publicDisk.visibility === "public") {
|
|
904
|
-
return publicDisk.name;
|
|
905
|
-
}
|
|
967
|
+
configuredDisk = config("media.disk");
|
|
906
968
|
} catch {
|
|
969
|
+
return DEFAULT_MEDIA_DISK;
|
|
907
970
|
}
|
|
908
|
-
return
|
|
971
|
+
return typeof configuredDisk === "string" && configuredDisk.trim() ? configuredDisk.trim() : DEFAULT_MEDIA_DISK;
|
|
909
972
|
}
|
|
910
973
|
function parseRemoteFileName(url) {
|
|
911
974
|
const parsedUrl = new URL(url);
|
|
@@ -950,39 +1013,51 @@ function createMaxSizeError(fileName, collectionName, maxSize, actualSize) {
|
|
|
950
1013
|
async function readRemoteMediaContents(response, fileName, maxSize, collectionName) {
|
|
951
1014
|
const resolvedMaxSize = typeof maxSize === "number" ? maxSize : DEFAULT_REMOTE_MEDIA_MAX_SIZE;
|
|
952
1015
|
if (!response.body) {
|
|
953
|
-
const
|
|
954
|
-
if (
|
|
955
|
-
throw createMaxSizeError(fileName, collectionName, resolvedMaxSize,
|
|
1016
|
+
const contents = new Uint8Array(await response.arrayBuffer());
|
|
1017
|
+
if (contents.byteLength > resolvedMaxSize) {
|
|
1018
|
+
throw createMaxSizeError(fileName, collectionName, resolvedMaxSize, contents.byteLength);
|
|
956
1019
|
}
|
|
957
|
-
return
|
|
1020
|
+
return contents;
|
|
958
1021
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1022
|
+
return await readBoundedMediaContents(
|
|
1023
|
+
readWebStreamChunks(response.body),
|
|
1024
|
+
fileName,
|
|
1025
|
+
resolvedMaxSize,
|
|
1026
|
+
collectionName
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
async function* readWebStreamChunks(stream) {
|
|
1030
|
+
const reader = stream.getReader();
|
|
962
1031
|
try {
|
|
963
1032
|
while (true) {
|
|
964
1033
|
const { done, value } = await reader.read();
|
|
965
1034
|
if (done) {
|
|
966
|
-
|
|
1035
|
+
return;
|
|
967
1036
|
}
|
|
968
|
-
if (
|
|
969
|
-
|
|
1037
|
+
if (value) {
|
|
1038
|
+
yield value;
|
|
970
1039
|
}
|
|
971
|
-
totalSize += value.byteLength;
|
|
972
|
-
if (totalSize > resolvedMaxSize) {
|
|
973
|
-
await reader.cancel().catch(() => void 0);
|
|
974
|
-
throw createMaxSizeError(fileName, collectionName, resolvedMaxSize, totalSize);
|
|
975
|
-
}
|
|
976
|
-
chunks.push(value);
|
|
977
1040
|
}
|
|
978
1041
|
} finally {
|
|
1042
|
+
await reader.cancel().catch(() => void 0);
|
|
979
1043
|
reader.releaseLock();
|
|
980
1044
|
}
|
|
1045
|
+
}
|
|
1046
|
+
async function readBoundedMediaContents(chunks, fileName, maxSize, collectionName) {
|
|
1047
|
+
const parts = [];
|
|
1048
|
+
let totalSize = 0;
|
|
1049
|
+
for await (const chunk of chunks) {
|
|
1050
|
+
totalSize += chunk.byteLength;
|
|
1051
|
+
if (totalSize > maxSize) {
|
|
1052
|
+
throw createMaxSizeError(fileName, collectionName, maxSize, totalSize);
|
|
1053
|
+
}
|
|
1054
|
+
parts.push(chunk);
|
|
1055
|
+
}
|
|
981
1056
|
const contents = new Uint8Array(totalSize);
|
|
982
1057
|
let offset = 0;
|
|
983
|
-
for (const
|
|
984
|
-
contents.set(
|
|
985
|
-
offset +=
|
|
1058
|
+
for (const part of parts) {
|
|
1059
|
+
contents.set(part, offset);
|
|
1060
|
+
offset += part.byteLength;
|
|
986
1061
|
}
|
|
987
1062
|
return contents;
|
|
988
1063
|
}
|
|
@@ -992,16 +1067,7 @@ async function resolveMediaSource(input, overrideFileName, overrideName, options
|
|
|
992
1067
|
return mimeType ? { ...source, mimeType } : source;
|
|
993
1068
|
}
|
|
994
1069
|
async function readMediaSource(input, overrideFileName, overrideName, options) {
|
|
995
|
-
if (
|
|
996
|
-
return resolveRemoteMediaSource(
|
|
997
|
-
{ url: input },
|
|
998
|
-
overrideFileName,
|
|
999
|
-
overrideName,
|
|
1000
|
-
options.maxSize,
|
|
1001
|
-
options.collectionName
|
|
1002
|
-
);
|
|
1003
|
-
}
|
|
1004
|
-
if (typeof input !== "string" && isUrlInput(input)) {
|
|
1070
|
+
if (input instanceof RemoteMediaSource) {
|
|
1005
1071
|
return resolveRemoteMediaSource(
|
|
1006
1072
|
input,
|
|
1007
1073
|
overrideFileName,
|
|
@@ -1010,6 +1076,12 @@ async function readMediaSource(input, overrideFileName, overrideName, options) {
|
|
|
1010
1076
|
options.collectionName
|
|
1011
1077
|
);
|
|
1012
1078
|
}
|
|
1079
|
+
if (input instanceof StoredMediaSource) {
|
|
1080
|
+
return resolveStorageMediaSource(input, overrideFileName, overrideName, options);
|
|
1081
|
+
}
|
|
1082
|
+
if (typeof input === "string" && /^https?:\/\//i.test(input) || typeof input === "object" && input !== null && "url" in input) {
|
|
1083
|
+
throw new Error("[@holo-js/media] addMedia() reads file paths and binary contents. Use addMediaFromUrl() to download a URL.");
|
|
1084
|
+
}
|
|
1013
1085
|
if (typeof input === "string" || isPathInput(input)) {
|
|
1014
1086
|
const path = await resolveLocalMediaSourcePath(typeof input === "string" ? input : input.path);
|
|
1015
1087
|
const contents2 = await readFile(path);
|
|
@@ -1050,43 +1122,130 @@ function getBinaryFileName(input) {
|
|
|
1050
1122
|
function isPathInput(input) {
|
|
1051
1123
|
return typeof input === "object" && input !== null && "path" in input;
|
|
1052
1124
|
}
|
|
1053
|
-
function
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
const
|
|
1059
|
-
if (
|
|
1060
|
-
throw
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1125
|
+
async function resolveStorageMediaSource(input, overrideFileName, overrideName, options) {
|
|
1126
|
+
const disk = Storage4.disk(input.disk);
|
|
1127
|
+
const fileName = sanitizeFileName(overrideFileName ?? localStoragePathInternals.splitLocalStoragePath(input.path).at(-1) ?? "");
|
|
1128
|
+
const maxSize = options.maxSize;
|
|
1129
|
+
const metadataAvailable = typeof maxSize === "number" && await checkStoredFileSize(disk, input, fileName, maxSize, options.collectionName);
|
|
1130
|
+
const chunks = await openStoredFileStream(disk, input);
|
|
1131
|
+
if (chunks === null) {
|
|
1132
|
+
throw missingStoredFileError(input);
|
|
1133
|
+
}
|
|
1134
|
+
let contents;
|
|
1135
|
+
if (chunks) {
|
|
1136
|
+
contents = await readBoundedMediaContents(chunks, fileName, maxSize ?? Number.POSITIVE_INFINITY, options.collectionName);
|
|
1137
|
+
} else if (typeof maxSize !== "number") {
|
|
1138
|
+
contents = await readWholeStoredFile(disk, input);
|
|
1139
|
+
} else if (!metadataAvailable) {
|
|
1063
1140
|
throw new Error(
|
|
1064
|
-
`[
|
|
1141
|
+
`[@holo-js/media] Collection "${options.collectionName}" has a maxSize, so "${input.path}" on disk "${input.disk}" needs a disk that supports file metadata or streaming.`
|
|
1065
1142
|
);
|
|
1143
|
+
} else {
|
|
1144
|
+
contents = await readWholeStoredFile(disk, input);
|
|
1145
|
+
if (contents.byteLength > maxSize) {
|
|
1146
|
+
throw createMaxSizeError(fileName, options.collectionName, maxSize, contents.byteLength);
|
|
1147
|
+
}
|
|
1066
1148
|
}
|
|
1067
|
-
const fileName = sanitizeFileName(
|
|
1068
|
-
overrideFileName ?? input.fileName ?? parseRemoteFileName(remoteUrl.toString())
|
|
1069
|
-
);
|
|
1070
|
-
const contentLengthHeader = response.headers.get("content-length");
|
|
1071
|
-
const contentLength = contentLengthHeader?.trim() ? Number(contentLengthHeader) : Number.NaN;
|
|
1072
|
-
const resolvedMaxSize = typeof maxSize === "number" ? maxSize : DEFAULT_REMOTE_MEDIA_MAX_SIZE;
|
|
1073
|
-
if (Number.isFinite(contentLength) && contentLength > resolvedMaxSize) {
|
|
1074
|
-
throw createMaxSizeError(fileName, collectionName, resolvedMaxSize, contentLength);
|
|
1075
|
-
}
|
|
1076
|
-
const contents = await readRemoteMediaContents(
|
|
1077
|
-
response,
|
|
1078
|
-
fileName,
|
|
1079
|
-
maxSize,
|
|
1080
|
-
collectionName
|
|
1081
|
-
);
|
|
1082
1149
|
return {
|
|
1083
1150
|
contents,
|
|
1084
1151
|
fileName,
|
|
1085
1152
|
extension: getExtension(fileName),
|
|
1086
1153
|
size: contents.byteLength,
|
|
1087
|
-
name: getDisplayName(fileName, overrideName
|
|
1154
|
+
name: getDisplayName(fileName, overrideName)
|
|
1088
1155
|
};
|
|
1089
1156
|
}
|
|
1157
|
+
async function checkStoredFileSize(disk, input, fileName, maxSize, collectionName) {
|
|
1158
|
+
let metadata;
|
|
1159
|
+
try {
|
|
1160
|
+
metadata = await disk.stat(input.path);
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
if (error instanceof StorageMetadataUnsupportedError) {
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1167
|
+
if (!metadata) {
|
|
1168
|
+
throw missingStoredFileError(input);
|
|
1169
|
+
}
|
|
1170
|
+
if (metadata.size > maxSize) {
|
|
1171
|
+
throw createMaxSizeError(fileName, collectionName, maxSize, metadata.size);
|
|
1172
|
+
}
|
|
1173
|
+
return true;
|
|
1174
|
+
}
|
|
1175
|
+
async function openStoredFileStream(disk, input) {
|
|
1176
|
+
try {
|
|
1177
|
+
return await disk.readStream(input.path);
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
if (error instanceof StorageStreamingUnsupportedError) {
|
|
1180
|
+
return void 0;
|
|
1181
|
+
}
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async function readWholeStoredFile(disk, input) {
|
|
1186
|
+
const contents = await disk.getBytes(input.path);
|
|
1187
|
+
if (!contents) {
|
|
1188
|
+
throw missingStoredFileError(input);
|
|
1189
|
+
}
|
|
1190
|
+
return contents;
|
|
1191
|
+
}
|
|
1192
|
+
function missingStoredFileError(input) {
|
|
1193
|
+
return new Error(`[@holo-js/media] File "${input.path}" was not found on disk "${input.disk}".`);
|
|
1194
|
+
}
|
|
1195
|
+
async function withRemoteMediaDeadline(download) {
|
|
1196
|
+
const controller = new AbortController();
|
|
1197
|
+
let timer;
|
|
1198
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
1199
|
+
timer = setTimeout(() => {
|
|
1200
|
+
const error = new Error(
|
|
1201
|
+
`[@holo-js/media] Remote media downloads must finish within ${REMOTE_MEDIA_TIMEOUT_MS / 1e3} seconds.`
|
|
1202
|
+
);
|
|
1203
|
+
controller.abort(error);
|
|
1204
|
+
reject(error);
|
|
1205
|
+
}, REMOTE_MEDIA_TIMEOUT_MS);
|
|
1206
|
+
});
|
|
1207
|
+
try {
|
|
1208
|
+
return await Promise.race([download(controller.signal), deadline]);
|
|
1209
|
+
} finally {
|
|
1210
|
+
clearTimeout(timer);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
async function resolveRemoteMediaSource(input, overrideFileName, overrideName, maxSize, collectionName) {
|
|
1214
|
+
const remoteUrl = resolveRemoteMediaUrl(input.url);
|
|
1215
|
+
return await withRemoteMediaDeadline(async (signal) => {
|
|
1216
|
+
const response = await remoteMediaDownloader(remoteUrl, signal);
|
|
1217
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1218
|
+
throw new Error("[@holo-js/media] Remote media downloads do not follow redirects.");
|
|
1219
|
+
}
|
|
1220
|
+
if (!response.ok) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`[Holo Media] Failed to download media from "${remoteUrl.toString()}" (${response.status} ${response.statusText}).`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
const fileName = sanitizeFileName(
|
|
1226
|
+
overrideFileName ?? parseRemoteFileName(remoteUrl.toString())
|
|
1227
|
+
);
|
|
1228
|
+
const contentLengthHeader = response.headers.get("content-length");
|
|
1229
|
+
const contentLength = contentLengthHeader?.trim() ? Number(contentLengthHeader) : Number.NaN;
|
|
1230
|
+
const resolvedMaxSize = typeof maxSize === "number" ? maxSize : DEFAULT_REMOTE_MEDIA_MAX_SIZE;
|
|
1231
|
+
if (Number.isFinite(contentLength) && contentLength > resolvedMaxSize) {
|
|
1232
|
+
throw createMaxSizeError(fileName, collectionName, resolvedMaxSize, contentLength);
|
|
1233
|
+
}
|
|
1234
|
+
const contents = await readRemoteMediaContents(
|
|
1235
|
+
response,
|
|
1236
|
+
fileName,
|
|
1237
|
+
maxSize,
|
|
1238
|
+
collectionName
|
|
1239
|
+
);
|
|
1240
|
+
return {
|
|
1241
|
+
contents,
|
|
1242
|
+
fileName,
|
|
1243
|
+
extension: getExtension(fileName),
|
|
1244
|
+
size: contents.byteLength,
|
|
1245
|
+
name: getDisplayName(fileName, overrideName)
|
|
1246
|
+
};
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1090
1249
|
async function resolveLocalMediaSourcePath(path) {
|
|
1091
1250
|
const resolvedPath = await realpath(path);
|
|
1092
1251
|
const allowedRoots = await Promise.all(LOCAL_MEDIA_SOURCE_ROOTS.map(async (root) => await realpath(root).catch(() => null)));
|
|
@@ -1111,9 +1270,13 @@ function resolveRemoteMediaUrl(value) {
|
|
|
1111
1270
|
if (url.username || url.password) {
|
|
1112
1271
|
throw new Error("[@holo-js/media] Remote media URLs must not include credentials.");
|
|
1113
1272
|
}
|
|
1114
|
-
if (
|
|
1273
|
+
if (isLocalRemoteMediaName(url.hostname)) {
|
|
1115
1274
|
throw new Error("[@holo-js/media] Remote media URLs must not target local or private hosts.");
|
|
1116
1275
|
}
|
|
1276
|
+
const literal = parseRemoteMediaIPLiteral(url.hostname);
|
|
1277
|
+
if (literal) {
|
|
1278
|
+
assertRemoteMediaAddressesAllowed([literal]);
|
|
1279
|
+
}
|
|
1117
1280
|
return url;
|
|
1118
1281
|
}
|
|
1119
1282
|
function createBlockedRemoteMediaAddresses() {
|
|
@@ -1127,42 +1290,167 @@ function createBlockedRemoteMediaAddresses() {
|
|
|
1127
1290
|
["172.16.0.0", 12],
|
|
1128
1291
|
["192.0.0.0", 24],
|
|
1129
1292
|
["192.0.2.0", 24],
|
|
1293
|
+
["192.88.99.2", 32],
|
|
1130
1294
|
["192.168.0.0", 16],
|
|
1131
1295
|
["198.18.0.0", 15],
|
|
1132
1296
|
["198.51.100.0", 24],
|
|
1133
1297
|
["203.0.113.0", 24],
|
|
1134
|
-
["224.0.0.0", 4]
|
|
1298
|
+
["224.0.0.0", 4],
|
|
1299
|
+
["240.0.0.0", 4],
|
|
1300
|
+
["255.255.255.255", 32]
|
|
1135
1301
|
]) {
|
|
1136
1302
|
blockList.addSubnet(network, prefix, "ipv4");
|
|
1137
1303
|
}
|
|
1138
1304
|
for (const [network, prefix] of [
|
|
1139
1305
|
["::", 128],
|
|
1140
1306
|
["::1", 128],
|
|
1307
|
+
["100::", 64],
|
|
1308
|
+
["100:0:0:1::", 64],
|
|
1309
|
+
["2001:db8::", 32],
|
|
1310
|
+
["3fff::", 20],
|
|
1311
|
+
["5f00::", 16],
|
|
1141
1312
|
["fc00::", 7],
|
|
1142
1313
|
["fe80::", 10],
|
|
1314
|
+
["fec0::", 10],
|
|
1143
1315
|
["ff00::", 8]
|
|
1144
1316
|
]) {
|
|
1145
1317
|
blockList.addSubnet(network, prefix, "ipv6");
|
|
1146
1318
|
}
|
|
1147
1319
|
return blockList;
|
|
1148
1320
|
}
|
|
1321
|
+
function createGloballyReachableBlockedIPv4Addresses() {
|
|
1322
|
+
const blockList = new BlockList();
|
|
1323
|
+
for (const [network, prefix] of [
|
|
1324
|
+
["192.0.0.9", 32],
|
|
1325
|
+
["192.0.0.10", 32]
|
|
1326
|
+
]) {
|
|
1327
|
+
blockList.addSubnet(network, prefix, "ipv4");
|
|
1328
|
+
}
|
|
1329
|
+
return blockList;
|
|
1330
|
+
}
|
|
1331
|
+
function createGlobalUnicastIPv6Addresses() {
|
|
1332
|
+
const blockList = new BlockList();
|
|
1333
|
+
blockList.addSubnet("2000::", 3, "ipv6");
|
|
1334
|
+
return blockList;
|
|
1335
|
+
}
|
|
1336
|
+
function createIETFProtocolAssignmentsBlockList() {
|
|
1337
|
+
const blockList = new BlockList();
|
|
1338
|
+
blockList.addSubnet("2001::", 23, "ipv6");
|
|
1339
|
+
return blockList;
|
|
1340
|
+
}
|
|
1341
|
+
function createGloballyReachableIETFProtocolAssignments() {
|
|
1342
|
+
const blockList = new BlockList();
|
|
1343
|
+
for (const [network, prefix] of [
|
|
1344
|
+
["2001::", 32],
|
|
1345
|
+
["2001:1::1", 128],
|
|
1346
|
+
["2001:1::2", 128],
|
|
1347
|
+
["2001:1::3", 128],
|
|
1348
|
+
["2001:3::", 32],
|
|
1349
|
+
["2001:4:112::", 48],
|
|
1350
|
+
["2001:20::", 28],
|
|
1351
|
+
["2001:30::", 28]
|
|
1352
|
+
]) {
|
|
1353
|
+
blockList.addSubnet(network, prefix, "ipv6");
|
|
1354
|
+
}
|
|
1355
|
+
return blockList;
|
|
1356
|
+
}
|
|
1357
|
+
function isBlockedRemoteMediaIPv4Address(address) {
|
|
1358
|
+
return BLOCKED_REMOTE_MEDIA_ADDRESSES.check(address, "ipv4") && !GLOBALLY_REACHABLE_BLOCKED_IPV4_ADDRESSES.check(address, "ipv4");
|
|
1359
|
+
}
|
|
1149
1360
|
function isBlockedRemoteMediaAddress(address) {
|
|
1150
|
-
|
|
1361
|
+
if (address.family === 4) {
|
|
1362
|
+
return isBlockedRemoteMediaIPv4Address(address.address);
|
|
1363
|
+
}
|
|
1364
|
+
return isBlockedRemoteMediaIPv6Address(address.address);
|
|
1365
|
+
}
|
|
1366
|
+
function isBlockedRemoteMediaIPv6Address(address) {
|
|
1367
|
+
const bytes = findIPv6Bytes(address);
|
|
1368
|
+
if (!bytes) {
|
|
1369
|
+
return true;
|
|
1370
|
+
}
|
|
1371
|
+
if (!GLOBAL_UNICAST_IPV6_ADDRESSES.check(address, "ipv6")) {
|
|
1372
|
+
const mappedAddress = findMappedIPv4Address(bytes);
|
|
1373
|
+
return mappedAddress === null || isBlockedRemoteMediaIPv4Address(mappedAddress);
|
|
1374
|
+
}
|
|
1375
|
+
if (BLOCKED_REMOTE_MEDIA_ADDRESSES.check(address, "ipv6")) {
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
if (IETF_PROTOCOL_ASSIGNMENTS.check(address, "ipv6") && !GLOBALLY_REACHABLE_IETF_PROTOCOL_ASSIGNMENTS.check(address, "ipv6")) {
|
|
1379
|
+
return true;
|
|
1380
|
+
}
|
|
1381
|
+
return findEmbeddedIPv4Addresses(bytes).some(isBlockedRemoteMediaIPv4Address);
|
|
1382
|
+
}
|
|
1383
|
+
function findMappedIPv4Address(bytes) {
|
|
1384
|
+
const mappedPrefixes = [NAT64_WELL_KNOWN_PREFIX, IPV4_COMPATIBLE_PREFIX, IPV4_MAPPED_PREFIX, TRANSLATED_IPV4_PREFIX];
|
|
1385
|
+
if (!mappedPrefixes.some((prefix) => bytes.subarray(0, 12).equals(prefix))) {
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
return formatIPv4Octets(bytes.subarray(12, 16));
|
|
1389
|
+
}
|
|
1390
|
+
function findEmbeddedIPv4Addresses(bytes) {
|
|
1391
|
+
const embeddedAddresses = [];
|
|
1392
|
+
if (bytes.subarray(0, 2).equals(SIX_TO_FOUR_PREFIX)) {
|
|
1393
|
+
embeddedAddresses.push(formatIPv4Octets(bytes.subarray(2, 6)));
|
|
1394
|
+
}
|
|
1395
|
+
if (bytes.subarray(0, 4).equals(TEREDO_PREFIX)) {
|
|
1396
|
+
embeddedAddresses.push(formatIPv4Octets(bytes.subarray(12, 16).map((octet) => octet ^ 255)));
|
|
1397
|
+
}
|
|
1398
|
+
const interfaceIdentifierPrefix = bytes.readUInt16BE(8);
|
|
1399
|
+
if ((interfaceIdentifierPrefix === 0 || interfaceIdentifierPrefix === 512) && bytes.readUInt16BE(10) === 24318) {
|
|
1400
|
+
embeddedAddresses.push(formatIPv4Octets(bytes.subarray(12, 16)));
|
|
1401
|
+
}
|
|
1402
|
+
return embeddedAddresses;
|
|
1403
|
+
}
|
|
1404
|
+
function findIPv6Bytes(address) {
|
|
1405
|
+
let canonical;
|
|
1406
|
+
try {
|
|
1407
|
+
canonical = new URL(`http://[${address}]`).hostname.slice(1, -1);
|
|
1408
|
+
} catch {
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
const halves = canonical.split("::");
|
|
1412
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
1413
|
+
const tail = halves[1] ? halves[1].split(":") : [];
|
|
1414
|
+
const zeroGroups = halves.length === 2 ? 8 - head.length - tail.length : 0;
|
|
1415
|
+
const groups = [...head, ...Array(zeroGroups).fill("0"), ...tail];
|
|
1416
|
+
if (groups.length !== 8) {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
return Buffer.from(groups.flatMap((group) => {
|
|
1420
|
+
const value = Number.parseInt(group, 16);
|
|
1421
|
+
return [value >> 8, value & 255];
|
|
1422
|
+
}));
|
|
1423
|
+
}
|
|
1424
|
+
function formatIPv4Octets(octets) {
|
|
1425
|
+
return Array.from(octets).join(".");
|
|
1151
1426
|
}
|
|
1152
1427
|
async function resolveRemoteMediaAddress(url, resolver) {
|
|
1153
|
-
const addresses = await
|
|
1428
|
+
const addresses = await resolveRemoteMediaHostAddresses(url.hostname, resolver);
|
|
1429
|
+
assertRemoteMediaAddressesAllowed(addresses);
|
|
1430
|
+
return addresses[0];
|
|
1431
|
+
}
|
|
1432
|
+
function assertRemoteMediaAddressesAllowed(addresses) {
|
|
1154
1433
|
if (addresses.length === 0 || addresses.some(isBlockedRemoteMediaAddress)) {
|
|
1155
1434
|
throw new Error("[@holo-js/media] Remote media URLs must not resolve to local or private hosts.");
|
|
1156
1435
|
}
|
|
1157
|
-
return addresses[0];
|
|
1158
1436
|
}
|
|
1159
|
-
function
|
|
1437
|
+
async function resolveRemoteMediaHostAddresses(hostname, resolver) {
|
|
1438
|
+
const literal = parseRemoteMediaIPLiteral(hostname);
|
|
1439
|
+
return literal ? [literal] : await resolver(hostname);
|
|
1440
|
+
}
|
|
1441
|
+
function parseRemoteMediaIPLiteral(hostname) {
|
|
1442
|
+
const address = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
1443
|
+
const family = isIP(address);
|
|
1444
|
+
return family === 0 ? null : { address, family };
|
|
1445
|
+
}
|
|
1446
|
+
function requestRemoteMedia(url, address, signal) {
|
|
1160
1447
|
return new Promise((resolveResponse, reject) => {
|
|
1161
1448
|
const request = resolveRemoteMediaRequest(url.protocol)(url, {
|
|
1162
1449
|
headers: {
|
|
1163
1450
|
host: url.host
|
|
1164
1451
|
},
|
|
1165
|
-
lookup: createPinnedRemoteMediaLookup(address)
|
|
1452
|
+
lookup: createPinnedRemoteMediaLookup(address),
|
|
1453
|
+
signal
|
|
1166
1454
|
}, (response) => {
|
|
1167
1455
|
const headers = new Headers();
|
|
1168
1456
|
appendRemoteMediaHeaders(headers, response.headers);
|
|
@@ -1191,14 +1479,9 @@ function appendRemoteMediaHeaders(headers, source) {
|
|
|
1191
1479
|
function resolveRemoteMediaRequest(protocol) {
|
|
1192
1480
|
return protocol === "https:" ? requestHttps : requestHttp;
|
|
1193
1481
|
}
|
|
1194
|
-
function
|
|
1482
|
+
function isLocalRemoteMediaName(hostname) {
|
|
1195
1483
|
const lowerCaseHostname = hostname.toLowerCase();
|
|
1196
|
-
|
|
1197
|
-
if (normalized === "localhost" || normalized.endsWith(".localhost")) {
|
|
1198
|
-
return true;
|
|
1199
|
-
}
|
|
1200
|
-
const family = isIP(normalized);
|
|
1201
|
-
return family !== 0 && isBlockedRemoteMediaAddress({ address: normalized, family });
|
|
1484
|
+
return lowerCaseHostname === "localhost" || lowerCaseHostname.endsWith(".localhost");
|
|
1202
1485
|
}
|
|
1203
1486
|
function validateSource(collection2, source) {
|
|
1204
1487
|
if (typeof collection2.maxSize === "number" && source.size > collection2.maxSize) {
|
|
@@ -1234,6 +1517,9 @@ var MediaAdder = class _MediaAdder {
|
|
|
1234
1517
|
this.source = source;
|
|
1235
1518
|
this.state = state;
|
|
1236
1519
|
}
|
|
1520
|
+
entity;
|
|
1521
|
+
source;
|
|
1522
|
+
state;
|
|
1237
1523
|
usingName(name) {
|
|
1238
1524
|
return new _MediaAdder(this.entity, this.source, {
|
|
1239
1525
|
...this.state,
|
|
@@ -1295,7 +1581,7 @@ var MediaAdder = class _MediaAdder {
|
|
|
1295
1581
|
collection: collection2,
|
|
1296
1582
|
definition: mediaDefinition,
|
|
1297
1583
|
source,
|
|
1298
|
-
disk: this.state.disk ?? collection2.disk ??
|
|
1584
|
+
disk: this.state.disk ?? collection2.disk ?? resolveMediaDefaultDisk()
|
|
1299
1585
|
});
|
|
1300
1586
|
}
|
|
1301
1587
|
};
|
|
@@ -1312,16 +1598,18 @@ async function attachMedia(options) {
|
|
|
1312
1598
|
afterRollback: () => {
|
|
1313
1599
|
owner.forgetRelation("media");
|
|
1314
1600
|
},
|
|
1315
|
-
|
|
1316
|
-
const existing = collection2.singleFile ? await owner.getMedia(options.collectionName) : [];
|
|
1601
|
+
write: async (mutation) => {
|
|
1317
1602
|
await mutation.put(disk, path, source.contents);
|
|
1318
|
-
|
|
1603
|
+
return await writeStoredConversions({
|
|
1319
1604
|
definition: options.definition,
|
|
1320
1605
|
collection: collection2,
|
|
1321
1606
|
conversionsDisk,
|
|
1322
1607
|
source: { ...source, uuid },
|
|
1323
1608
|
mutation
|
|
1324
1609
|
});
|
|
1610
|
+
},
|
|
1611
|
+
operation: async (_mutation, generatedConversions) => {
|
|
1612
|
+
const existing = collection2.singleFile ? await owner.getMedia(options.collectionName) : [];
|
|
1325
1613
|
const max = await Media.query().where("model_type", ownerDefinition.morphClass).where("model_id", ownerId).where("collection_name", options.collectionName).max("order_column");
|
|
1326
1614
|
const media = await Media.create({
|
|
1327
1615
|
uuid,
|
|
@@ -1420,7 +1708,10 @@ function installEntityMediaMethods() {
|
|
|
1420
1708
|
return new MediaAdder(this, source);
|
|
1421
1709
|
};
|
|
1422
1710
|
prototype.addMediaFromUrl = function addMediaFromUrl(url) {
|
|
1423
|
-
return new MediaAdder(this,
|
|
1711
|
+
return new MediaAdder(this, new RemoteMediaSource(url));
|
|
1712
|
+
};
|
|
1713
|
+
prototype.addMediaFromDisk = function addMediaFromDisk(path, disk) {
|
|
1714
|
+
return new MediaAdder(this, new StoredMediaSource(disk, path));
|
|
1424
1715
|
};
|
|
1425
1716
|
prototype.getMedia = async function getMedia(collectionName) {
|
|
1426
1717
|
const query = createQuery(this, collectionName);
|
package/dist/projectConfig.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
interface HoloMediaConfig {
|
|
2
|
+
readonly disk?: string;
|
|
2
3
|
readonly [key: string]: unknown;
|
|
3
4
|
}
|
|
4
5
|
declare module '@holo-js/config' {
|
|
@@ -9,4 +10,5 @@ declare module '@holo-js/config' {
|
|
|
9
10
|
declare function normalizeMediaConfig(config?: HoloMediaConfig): Readonly<HoloMediaConfig>;
|
|
10
11
|
declare function defineMediaConfig<TConfig extends HoloMediaConfig>(config: TConfig): Readonly<TConfig>;
|
|
11
12
|
|
|
12
|
-
export {
|
|
13
|
+
export { defineMediaConfig, normalizeMediaConfig };
|
|
14
|
+
export type { HoloMediaConfig };
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holo-js/media",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Holo-JS Framework - media collections and conversions for Holo-JS models",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/cobraprojects/holo-js.git",
|
|
10
|
+
"directory": "packages/media"
|
|
11
|
+
},
|
|
7
12
|
"exports": {
|
|
8
13
|
".": {
|
|
9
14
|
"types": "./dist/index.d.ts",
|
|
@@ -19,7 +24,8 @@
|
|
|
19
24
|
"main": "./dist/index.mjs",
|
|
20
25
|
"types": "./dist/index.d.ts",
|
|
21
26
|
"files": [
|
|
22
|
-
"dist"
|
|
27
|
+
"dist",
|
|
28
|
+
"LICENSE"
|
|
23
29
|
],
|
|
24
30
|
"scripts": {
|
|
25
31
|
"build": "tsup",
|
|
@@ -28,21 +34,20 @@
|
|
|
28
34
|
"test": "vitest --run"
|
|
29
35
|
},
|
|
30
36
|
"dependencies": {
|
|
31
|
-
"@holo-js/config": "^0.
|
|
32
|
-
"@holo-js/db": "^0.
|
|
33
|
-
"@holo-js/kernel": "^0.
|
|
34
|
-
"@holo-js/queue": "^0.
|
|
35
|
-
"@holo-js/storage": "^0.
|
|
36
|
-
"
|
|
37
|
-
"sharp": "^0.35.4"
|
|
37
|
+
"@holo-js/config": "^0.4.1",
|
|
38
|
+
"@holo-js/db": "^0.4.1",
|
|
39
|
+
"@holo-js/kernel": "^0.4.1",
|
|
40
|
+
"@holo-js/queue": "^0.4.1",
|
|
41
|
+
"@holo-js/storage": "^0.4.1",
|
|
42
|
+
"sharp": "^0.35.5"
|
|
38
43
|
},
|
|
39
44
|
"devDependencies": {
|
|
40
|
-
"@holo-js/db-sqlite": "^0.
|
|
41
|
-
"tsup": "^
|
|
42
|
-
"typescript": "^
|
|
43
|
-
"vitest": "^
|
|
45
|
+
"@holo-js/db-sqlite": "^0.4.1",
|
|
46
|
+
"tsup": "^9.0.0",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vitest": "^5.0.5"
|
|
44
49
|
},
|
|
45
50
|
"engines": {
|
|
46
|
-
"node": "^22.
|
|
51
|
+
"node": "^22.18.0 || ^24.0.0 || ^26.0.0"
|
|
47
52
|
}
|
|
48
53
|
}
|