@holo-js/media 0.3.28 → 0.4.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/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
+ 90fe1fb20b9e79de3879f0aadd27cc72d3e9fdb5b7a2bd18449eddda2c91dd49
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, RelationConstraintDefinition, RelatedColumnNameForRelationPath, ModelCollection, EntityWithLoaded, PaginatedResult, SimplePaginatedResult, CursorPaginatedResult, ResolveEagerLoads } from '@holo-js/db';
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: MediaSourceInput, state?: {
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> ? MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded>, TCollectionName, TConversionName> : TValue extends ModelCollection<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TItem> ? TItem extends Entity<TTable, TRelations> ? ModelCollection<TTable, TRelations, MediaEnabledEntityResult<TItem, TCollectionName, TConversionName>> : 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>;
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 MediaRelationConstraint<TTable extends TableDefinition, TRelations extends RelationMap> = RelationConstraintDefinition & ((query: ModelQueryBuilder<TTable, TRelations>) => unknown);
324
- type MediaRelationConstraintMap<TRelations extends RelationMap> = Readonly<Partial<Record<MediaRelationPath<TRelations>, MediaRelationConstraint<TableDefinition, TRelations>>>>;
325
- type MediaEnabledQueryBuilderWith<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded> ? {
326
- <TPaths extends readonly MediaRelationPath<TRelations>[]>(...relations: TPaths): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded & ResolveEagerLoads<TRelations, TPaths>>, TCollectionName, TConversionName>;
327
- <TPath extends MediaRelationPath<TRelations>>(relation: TPath, constraint: MediaRelationConstraint<TTable, TRelations>): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded & ResolveEagerLoads<TRelations, readonly [TPath]>>, TCollectionName, TConversionName>;
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
- (...relations: readonly MediaRelationPath<TRelations>[]): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded>, TCollectionName, TConversionName>;
335
- (relations: MediaRelationConstraintMap<TRelations>): MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded>, TCollectionName, TConversionName>;
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 MediaEnabledRelationColumnAggregate<TBuilder, TCollectionName extends string, TConversionName extends string> = TBuilder extends ModelQueryBuilder<infer TTable extends TableDefinition, infer TRelations extends RelationMap, infer TLoaded> ? <TPath extends MediaRelationPath<TRelations>>(first: TPath | MediaRelationConstraintMap<TRelations>, column: RelatedColumnNameForRelationPath<TRelations, TPath>, ...rest: readonly MediaRelationPath<TRelations>[]) => MediaEnabledQueryBuilder<ModelQueryBuilder<TTable, TRelations, TLoaded>, TCollectionName, TConversionName> : never;
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 { type CollectionNamesOf, type ConversionNamesOf, MEDIA_GENERATE_CONVERSIONS_JOB, Media, type MediaAddResult, MediaAdder, type MediaCollectionBuilder, type MediaCollectionDefinition, type MediaConversionBuilder, type MediaConversionDefinition, type MediaConversionExecutor, type MediaConversionExecutorInput, type MediaConversionExecutorResult, type MediaConversionExecutorSource, type MediaConversionFit, type MediaConversionFormat, type MediaConversionPathInput, type MediaDefinitionFactory, type MediaDefinitionHelpers, type MediaDefinitionInput, type MediaEnabledEntity, type MediaEnabledModel, type MediaGenerateConversionsPayload, type MediaGenerateConversionsResult, MediaItem, type MediaOriginalPathInput, type MediaPathGenerator, type MediaSourceInput, type NormalizedMediaCollectionDefinition, type NormalizedMediaConversionDefinition, type NormalizedMediaDefinition, type StoredMediaConversion, 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 };
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
- let pipeline = sharp(await toBuffer(source.contents), {
106
+ const image = sharp(await toBuffer(source.contents), {
106
107
  animated: true,
107
- failOn: "warning"
108
- }).rotate();
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 { Storage as Storage4 } from "@holo-js/storage/runtime";
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
- written = [];
384
+ created = [];
370
385
  async put(disk, path, contents) {
371
- const previous = await Storage.disk(disk).getBytes(path);
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.written.splice(0).reverse()) {
391
+ for (const file of this.created.splice(0).reverse()) {
378
392
  try {
379
- if (file.previous) {
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
- return await DB.writeTransaction(async (transaction) => {
395
- transaction.afterRollback(async () => {
396
- const failures = [];
397
- try {
398
- await mutation.compensate();
399
- } catch (error) {
400
- failures.push(error);
401
- }
402
- try {
403
- await options.afterRollback?.();
404
- } catch (error) {
405
- failures.push(error);
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 effect(result);
425
+ await mutation.compensate();
416
426
  } catch (error) {
417
427
  failures.push(error);
418
428
  }
419
- }
420
- options.afterEffects?.();
421
- if (failures.length === 0) return;
422
- const cause = failures.length === 1 ? failures[0] : new AggregateError(failures, "[Holo Media] Post-commit effects failed.");
423
- throw new Error(options.committedMessage, { cause });
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
- return result;
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
- async function generateStoredConversions(options) {
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 conversionPath = getMediaPathGenerator().conversionPath({
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 targetDisk = generated.disk ?? options.conversionsDisk;
566
- const conversionMimeType = generated.mimeType?.trim() || mimeTypeFromFileName2(generatedFileName) || void 0;
567
- const conversionContents = await toBinaryContent(generated.contents);
568
- await options.mutation.put(targetDisk, conversionPath, conversionContents);
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: conversionPath,
571
- disk: targetDisk,
605
+ path,
606
+ disk,
572
607
  fileName: generatedFileName,
573
- mimeType: conversionMimeType,
574
- size: getContentSize(conversionContents)
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
- async function deleteObsoleteConversions(current, next, fallbackDisk, requested) {
580
- const obsolete = Object.entries(current).filter(([name, conversion2]) => {
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 nextBase = requested?.length ? Object.fromEntries(
606
- Object.entries(current).filter(([name]) => !requested.includes(name))
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
- operation: async (mutation) => {
615
- const regenerated = await generateStoredConversions({
616
- definition,
617
- collection: collection2,
618
- conversionsDisk,
619
- requestedConversions: requested,
620
- includeQueued: options.includeQueued === true,
621
- mutation,
622
- source: {
623
- uuid: String(media.get("uuid")),
624
- fileName: String(media.get("file_name")),
625
- extension: media.get("extension") ?? void 0,
626
- mimeType: media.get("mime_type") ?? void 0,
627
- size: sourceContents.byteLength,
628
- contents: sourceContents
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 generatedConversions;
671
+ return {
672
+ generatedConversions,
673
+ obsolete: obsoleteConversionFiles(current, generatedConversions, conversionsDisk)
674
+ };
636
675
  },
637
676
  afterCommit: [
638
- async (generatedConversions) => {
639
- await deleteObsoleteConversions(current, generatedConversions, conversionsDisk, requested);
677
+ async ({ obsolete }) => {
678
+ await removeStoredFiles(obsolete);
640
679
  },
641
680
  async () => {
642
681
  if (options.includeQueued) return;
643
682
  await dispatchQueuedMediaConversionsForModel({
644
- mediaId: media.get("id"),
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 resolveImplicitDiskName() {
897
- const defaultDisk = Storage4.disk();
898
- if (defaultDisk.visibility === "public") {
899
- return defaultDisk.name;
900
- }
964
+ function resolveMediaDefaultDisk() {
965
+ let configuredDisk;
901
966
  try {
902
- const publicDisk = Storage4.disk("public");
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 defaultDisk.name;
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 contents2 = new Uint8Array(await response.arrayBuffer());
954
- if (contents2.byteLength > resolvedMaxSize) {
955
- throw createMaxSizeError(fileName, collectionName, resolvedMaxSize, contents2.byteLength);
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 contents2;
1020
+ return contents;
958
1021
  }
959
- const reader = response.body.getReader();
960
- const chunks = [];
961
- let totalSize = 0;
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
- break;
1035
+ return;
967
1036
  }
968
- if (!value) {
969
- continue;
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 chunk of chunks) {
984
- contents.set(chunk, offset);
985
- offset += chunk.byteLength;
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 (typeof input === "string" && /^https?:\/\//i.test(input)) {
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 isUrlInput(input) {
1054
- return typeof input === "object" && input !== null && "url" in input;
1055
- }
1056
- async function resolveRemoteMediaSource(input, overrideFileName, overrideName, maxSize, collectionName) {
1057
- const remoteUrl = resolveRemoteMediaUrl(input.url);
1058
- const response = await remoteMediaDownloader(remoteUrl);
1059
- if (response.status >= 300 && response.status < 400) {
1060
- throw new Error("[@holo-js/media] Remote media downloads do not follow redirects.");
1061
- }
1062
- if (!response.ok) {
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
- `[Holo Media] Failed to download media from "${remoteUrl.toString()}" (${response.status} ${response.statusText}).`
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 ?? input.name)
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 (isBlockedRemoteMediaHost(url.hostname)) {
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
- return BLOCKED_REMOTE_MEDIA_ADDRESSES.check(address.address, address.family === 4 ? "ipv4" : "ipv6");
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 resolver(url.hostname);
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 requestRemoteMedia(url, address) {
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 isBlockedRemoteMediaHost(hostname) {
1482
+ function isLocalRemoteMediaName(hostname) {
1195
1483
  const lowerCaseHostname = hostname.toLowerCase();
1196
- const normalized = lowerCaseHostname.startsWith("[") && lowerCaseHostname.endsWith("]") ? lowerCaseHostname.slice(1, -1) : lowerCaseHostname;
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 ?? resolveImplicitDiskName()
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
- operation: async (mutation) => {
1316
- const existing = collection2.singleFile ? await owner.getMedia(options.collectionName) : [];
1601
+ write: async (mutation) => {
1317
1602
  await mutation.put(disk, path, source.contents);
1318
- const generatedConversions = await generateStoredConversions({
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, { url });
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);
@@ -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 { type HoloMediaConfig, defineMediaConfig, normalizeMediaConfig };
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.28",
3
+ "version": "0.4.0",
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.3.28",
32
- "@holo-js/db": "^0.3.28",
33
- "@holo-js/kernel": "^0.3.28",
34
- "@holo-js/queue": "^0.3.28",
35
- "@holo-js/storage": "^0.3.28",
36
- "file-type": "^22.2.0",
37
- "sharp": "^0.35.4"
37
+ "@holo-js/config": "^0.4.0",
38
+ "@holo-js/db": "^0.4.0",
39
+ "@holo-js/kernel": "^0.4.0",
40
+ "@holo-js/queue": "^0.4.0",
41
+ "@holo-js/storage": "^0.4.0",
42
+ "sharp": "^0.35.5"
38
43
  },
39
44
  "devDependencies": {
40
- "@holo-js/db-sqlite": "^0.3.28",
41
- "tsup": "^8.3.5",
42
- "typescript": "^5.7.2",
43
- "vitest": "^4.1.5"
45
+ "@holo-js/db-sqlite": "^0.4.0",
46
+ "tsup": "^9.0.0",
47
+ "typescript": "^6.0.3",
48
+ "vitest": "^5.0.5"
44
49
  },
45
50
  "engines": {
46
- "node": "^22.12.0 || ^24.0.0 || ^26.0.0"
51
+ "node": "^22.18.0 || ^24.0.0 || ^26.0.0"
47
52
  }
48
53
  }