@adonis-agora/media 0.8.2 → 0.9.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.
Files changed (104) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +2 -2
  3. package/dist/providers/media_provider.d.ts.map +1 -1
  4. package/dist/providers/media_provider.js +113 -0
  5. package/dist/providers/media_provider.js.map +1 -1
  6. package/dist/src/content_type.d.ts +6 -1
  7. package/dist/src/content_type.d.ts.map +1 -1
  8. package/dist/src/content_type.js +102 -7
  9. package/dist/src/content_type.js.map +1 -1
  10. package/dist/src/define_config.d.ts +88 -5
  11. package/dist/src/define_config.d.ts.map +1 -1
  12. package/dist/src/define_config.js +7 -2
  13. package/dist/src/define_config.js.map +1 -1
  14. package/dist/src/direct_upload.d.ts +177 -0
  15. package/dist/src/direct_upload.d.ts.map +1 -0
  16. package/dist/src/direct_upload.js +299 -0
  17. package/dist/src/direct_upload.js.map +1 -0
  18. package/dist/src/direct_upload_handler.d.ts +83 -0
  19. package/dist/src/direct_upload_handler.d.ts.map +1 -0
  20. package/dist/src/direct_upload_handler.js +180 -0
  21. package/dist/src/direct_upload_handler.js.map +1 -0
  22. package/dist/src/disks/factory.d.ts +11 -3
  23. package/dist/src/disks/factory.d.ts.map +1 -1
  24. package/dist/src/disks/factory.js +5 -3
  25. package/dist/src/disks/factory.js.map +1 -1
  26. package/dist/src/disks/s3.d.ts +10 -0
  27. package/dist/src/disks/s3.d.ts.map +1 -1
  28. package/dist/src/disks/s3.js +96 -22
  29. package/dist/src/disks/s3.js.map +1 -1
  30. package/dist/src/disks/sigv4.d.ts +58 -0
  31. package/dist/src/disks/sigv4.d.ts.map +1 -0
  32. package/dist/src/disks/sigv4.js +87 -0
  33. package/dist/src/disks/sigv4.js.map +1 -0
  34. package/dist/src/disks/xml_fallback.d.ts +3 -3
  35. package/dist/src/disks/xml_fallback.js +3 -3
  36. package/dist/src/errors.d.ts +104 -0
  37. package/dist/src/errors.d.ts.map +1 -1
  38. package/dist/src/errors.js +145 -0
  39. package/dist/src/errors.js.map +1 -1
  40. package/dist/src/hls/delivery.d.ts +112 -0
  41. package/dist/src/hls/delivery.d.ts.map +1 -0
  42. package/dist/src/hls/delivery.js +118 -0
  43. package/dist/src/hls/delivery.js.map +1 -0
  44. package/dist/src/hls/playlist.d.ts +67 -0
  45. package/dist/src/hls/playlist.d.ts.map +1 -0
  46. package/dist/src/hls/playlist.js +126 -0
  47. package/dist/src/hls/playlist.js.map +1 -0
  48. package/dist/src/index.d.ts +18 -3
  49. package/dist/src/index.d.ts.map +1 -1
  50. package/dist/src/index.js +13 -2
  51. package/dist/src/index.js.map +1 -1
  52. package/dist/src/media_collection.d.ts +8 -1
  53. package/dist/src/media_collection.d.ts.map +1 -1
  54. package/dist/src/media_collection.js +23 -1
  55. package/dist/src/media_collection.js.map +1 -1
  56. package/dist/src/media_library.d.ts +32 -2
  57. package/dist/src/media_library.d.ts.map +1 -1
  58. package/dist/src/media_library.js +218 -11
  59. package/dist/src/media_library.js.map +1 -1
  60. package/dist/src/media_manager.d.ts +37 -2
  61. package/dist/src/media_manager.d.ts.map +1 -1
  62. package/dist/src/media_manager.js +54 -1
  63. package/dist/src/media_manager.js.map +1 -1
  64. package/dist/src/media_record.d.ts +22 -2
  65. package/dist/src/media_record.d.ts.map +1 -1
  66. package/dist/src/testing/fake_transformer.d.ts +32 -0
  67. package/dist/src/testing/fake_transformer.d.ts.map +1 -0
  68. package/dist/src/testing/fake_transformer.js +32 -0
  69. package/dist/src/testing/fake_transformer.js.map +1 -0
  70. package/dist/src/testing/index.d.ts +2 -0
  71. package/dist/src/testing/index.d.ts.map +1 -1
  72. package/dist/src/testing/index.js +1 -0
  73. package/dist/src/testing/index.js.map +1 -1
  74. package/dist/src/transformer.d.ts +111 -0
  75. package/dist/src/transformer.d.ts.map +1 -0
  76. package/dist/src/transformer.js +2 -0
  77. package/dist/src/transformer.js.map +1 -0
  78. package/dist/src/transformers/factory.d.ts +30 -0
  79. package/dist/src/transformers/factory.d.ts.map +1 -0
  80. package/dist/src/transformers/factory.js +34 -0
  81. package/dist/src/transformers/factory.js.map +1 -0
  82. package/dist/src/transformers/hls.d.ts +110 -0
  83. package/dist/src/transformers/hls.d.ts.map +1 -0
  84. package/dist/src/transformers/hls.js +111 -0
  85. package/dist/src/transformers/hls.js.map +1 -0
  86. package/dist/src/transformers/mediabunny.d.ts +5 -0
  87. package/dist/src/transformers/mediabunny.d.ts.map +1 -0
  88. package/dist/src/transformers/mediabunny.js +150 -0
  89. package/dist/src/transformers/mediabunny.js.map +1 -0
  90. package/dist/src/transformers/mediabunny_loader.d.ts +15 -0
  91. package/dist/src/transformers/mediabunny_loader.d.ts.map +1 -0
  92. package/dist/src/transformers/mediabunny_loader.js +21 -0
  93. package/dist/src/transformers/mediabunny_loader.js.map +1 -0
  94. package/dist/src/transformers/probe.d.ts +56 -0
  95. package/dist/src/transformers/probe.d.ts.map +1 -0
  96. package/dist/src/transformers/probe.js +32 -0
  97. package/dist/src/transformers/probe.js.map +1 -0
  98. package/dist/src/transformers/source_file.d.ts +20 -0
  99. package/dist/src/transformers/source_file.d.ts.map +1 -0
  100. package/dist/src/transformers/source_file.js +30 -0
  101. package/dist/src/transformers/source_file.js.map +1 -0
  102. package/dist/stubs/config/media.stub +12 -0
  103. package/package.json +11 -7
  104. package/LICENSE +0 -21
@@ -7,6 +7,7 @@ import { processors } from './processors/factory.js';
7
7
  import type { ImageProcessorFactory } from './processors/factory.js';
8
8
  import { stores } from './stores/factory.js';
9
9
  import type { LucidStoreConfig, StoreContext, StoreFactory } from './stores/factory.js';
10
+ import { transformers } from './transformers/factory.js';
10
11
  import type { UploadMode } from './upload_mode.js';
11
12
  import { uploadSessions } from './upload_sessions/factory.js';
12
13
  import type { LucidUploadSessionStoreConfig, UploadSessionStoreContext, UploadSessionStoreFactory } from './upload_sessions/factory.js';
@@ -42,6 +43,61 @@ export interface MediaUploadsConfig {
42
43
  * disabled and no session store is built.
43
44
  */
44
45
  resumable?: MediaResumableConfig;
46
+ /**
47
+ * Session-backed direct upload settings (`media.direct`) — browser→S3 multipart through presigned
48
+ * part URLs, with the session (uploadId, part size, confirmed ETags) persisted server-side so an
49
+ * interrupted upload resumes after a page reload. Present ⇒ `media.direct` is available; when
50
+ * `routes.enabled`, the provider mounts the JSON endpoints. Absent ⇒ disabled, no session store
51
+ * is built. See {@link MediaDirectUploadConfig} for how this relates to TUS.
52
+ */
53
+ direct?: MediaDirectUploadConfig;
54
+ }
55
+ /**
56
+ * Session-backed direct upload configuration. The session store is the SAME SPI (and, with
57
+ * `uploadSessions.lucid()`, the same tables) the resumable (TUS) subsystem uses — the two flows are
58
+ * complements, not rivals: TUS streams every chunk THROUGH the app (works on any disk, byte-exact
59
+ * resume), while direct hands the browser presigned URLs and the bytes never touch the app (S3-only,
60
+ * resumes at part granularity, needs bucket CORS exposing `ETag`). For video-sized files on S3,
61
+ * direct halves the total bandwidth.
62
+ */
63
+ export interface MediaDirectUploadConfig {
64
+ /**
65
+ * Name of the session store (a key of {@link stores} below). Omit to use the in-memory store
66
+ * (single-process, non-durable — sessions won't survive a restart, uploads in flight will).
67
+ */
68
+ store?: string;
69
+ /** Named session stores, built with the {@link uploadSessions} factory (lazy peers). */
70
+ stores?: Record<string, UploadSessionStoreFactory>;
71
+ /**
72
+ * Multipart part size in bytes. Falls back to `uploads.partSize`, then 20 MiB. S3 requires at
73
+ * least 5 MiB and at most 10,000 parts per upload.
74
+ */
75
+ partSize?: number;
76
+ /**
77
+ * Lifetime of presigned part URLs, in seconds. Falls back to `uploads.presignTtlSeconds`, then
78
+ * 3600. Expiry never strands an upload: `status()` re-issues fresh URLs for pending parts.
79
+ */
80
+ presignTtlSeconds?: number;
81
+ /** Session lifetime in seconds. Omit for sessions that never expire. */
82
+ sessionTtlSeconds?: number;
83
+ /** Mount the built-in direct-session HTTP routes. Opt-in. */
84
+ routes?: {
85
+ /** Register the routes. Default false. */
86
+ enabled?: boolean;
87
+ /** Path prefix for the routes. Default `/media/uploads/direct/sessions`. */
88
+ prefix?: string;
89
+ /** Disk uploads land on. Defaults to the media default disk (must be multipart-capable). */
90
+ disk?: string;
91
+ /** Reject initiations whose declared `size` exceeds this many bytes. */
92
+ maxSize?: number;
93
+ /**
94
+ * Collection these uploads are destined for. Set it to have `initiate` reject a declared
95
+ * `contentType` outside that collection's `acceptsMimeTypes` — before the multipart upload
96
+ * even opens. The collection config remains the single source of truth; nothing is restated
97
+ * here. Omit to accept any type (size limits still apply).
98
+ */
99
+ collection?: string;
100
+ };
45
101
  }
46
102
  /**
47
103
  * Resumable (TUS) upload configuration. The session store persists offset/length/metadata/expiry so
@@ -147,8 +203,8 @@ export interface MediaConfig {
147
203
  * so the heavy peer is imported lazily. Required only if any collection defines conversions.
148
204
  */
149
205
  imageProcessor?: ImageProcessor | ImageProcessorFactory;
150
- /** Collection definitions (MIME whitelist, single-file replace, conversions). */
151
- collections?: MediaCollectionConfig[];
206
+ /** Collection definitions (MIME whitelist, single-file replace, conversions, transformers). */
207
+ collections?: readonly MediaCollectionConfig[];
152
208
  /** Key prefix for column attachments created via the `AttachmentManager`. Default `attachments`. */
153
209
  attachmentKeyPrefix?: string;
154
210
  /** Emit `agora:media:*` diagnostics events (default true). */
@@ -158,8 +214,35 @@ export interface MediaConfig {
158
214
  /** How stored media is served back (`auto`/`public`/`signed`/`proxy` + signed-URL TTL). */
159
215
  delivery?: MediaDeliveryConfig;
160
216
  }
161
- /** Identity helper giving `config/media.ts` full type-checking. */
162
- export declare function defineConfig(config?: MediaConfig): MediaConfig;
163
- export { stores, processors, disks, uploadSessions };
217
+ /**
218
+ * Identity helper giving `config/media.ts` full type-checking. The `const` type parameter keeps
219
+ * collection/conversion/transformer names as literal types, so {@link InferConversions} /
220
+ * {@link InferTransformers} can extract them from `typeof config` — no `as const` needed.
221
+ */
222
+ export declare function defineConfig<const T extends MediaConfig>(config?: T): T;
223
+ /** The collection literal types of a config (helper for the `Infer*` utilities). */
224
+ type CollectionOf<T extends MediaConfig> = T['collections'] extends readonly (infer C extends MediaCollectionConfig)[] ? C : never;
225
+ /**
226
+ * Union of the transformer names a config declares — for typing job payloads, `transform()` call
227
+ * sites, and route params against the config instead of restating strings:
228
+ *
229
+ * ```ts
230
+ * const config = defineConfig({
231
+ * collections: [{ name: 'videos', transformers: [transformers.hls(), transformers.probe()] }],
232
+ * })
233
+ * type AppTransformer = InferTransformers<typeof config> // 'hls' | 'probe'
234
+ * ```
235
+ */
236
+ export type InferTransformers<T extends MediaConfig> = NonNullable<CollectionOf<T>['transformers']>[number]['name'];
237
+ /**
238
+ * Union of every conversion name a config declares — image presets and transformers alike, since
239
+ * both persist under `record.conversions`:
240
+ *
241
+ * ```ts
242
+ * type AppConversion = InferConversions<typeof config> // 'thumb' | 'og' | 'hls' | 'probe'
243
+ * ```
244
+ */
245
+ export type InferConversions<T extends MediaConfig> = NonNullable<CollectionOf<T>['conversions']>[number]['name'] | InferTransformers<T>;
246
+ export { stores, processors, disks, uploadSessions, transformers };
164
247
  export type { StoreContext, StoreFactory, LucidStoreConfig, ImageProcessorFactory, DiskFactory, S3DiskConfig, S3Credentials, UploadMode, DeliveryMode, UploadSessionStoreContext, UploadSessionStoreFactory, LucidUploadSessionStoreConfig, };
165
248
  //# sourceMappingURL=define_config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"define_config.d.ts","sourceRoot":"","sources":["../../src/define_config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"define_config.d.ts","sourceRoot":"","sources":["../../src/define_config.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,15 @@
1
1
  import { disks } from './disks/factory.js';
2
2
  import { processors } from './processors/factory.js';
3
3
  import { stores } from './stores/factory.js';
4
+ import { transformers } from './transformers/factory.js';
4
5
  import { uploadSessions } from './upload_sessions/factory.js';
5
- /** Identity helper giving `config/media.ts` full type-checking. */
6
+ /**
7
+ * Identity helper giving `config/media.ts` full type-checking. The `const` type parameter keeps
8
+ * collection/conversion/transformer names as literal types, so {@link InferConversions} /
9
+ * {@link InferTransformers} can extract them from `typeof config` — no `as const` needed.
10
+ */
6
11
  export function defineConfig(config = {}) {
7
12
  return config;
8
13
  }
9
- export { stores, processors, disks, uploadSessions };
14
+ export { stores, processors, disks, uploadSessions, transformers };
10
15
  //# sourceMappingURL=define_config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"define_config.js","sourceRoot":"","sources":["../../src/define_config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAI3C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAG7C,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AA+J9D,mEAAmE;AACnE,MAAM,UAAU,YAAY,CAAC,SAAsB,EAAE;IACnD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"define_config.js","sourceRoot":"","sources":["../../src/define_config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAI3C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAErD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAuN9D;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAA8B,SAAY,EAAO;IAC3E,OAAO,MAAM,CAAC;AAChB,CAAC;AAmCD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,177 @@
1
+ import type { MediaCollectionRegistry } from './media_collection.js';
2
+ import type { UploadSession, UploadSessionListFilter, UploadSessionStore } from './resumable_upload.js';
3
+ import type { StorageManager } from './storage_manager.js';
4
+ import type { MultipartPart } from './types.js';
5
+ /**
6
+ * Default part size: 20 MiB. Between S3's 5 MiB minimum and a reasonable payload per browser `PUT`;
7
+ * with S3's 10,000-part cap it covers files up to ~195 GiB. Larger than the proxy default (8 MiB)
8
+ * on purpose — direct parts don't occupy app memory, so the only cost of a bigger part is a bigger
9
+ * retry unit.
10
+ */
11
+ export declare const DEFAULT_DIRECT_PART_SIZE: number;
12
+ /** S3's minimum part size (every part but the last). Enforced at `initiate` — see {@link UploadPartSizeError}. */
13
+ export declare const MIN_DIRECT_PART_SIZE: number;
14
+ /** S3's cap on parts per multipart upload. */
15
+ export declare const MAX_DIRECT_PARTS = 10000;
16
+ /** A part the client should `PUT`: its number and the presigned URL that authorizes exactly that. */
17
+ export interface DirectUploadPartUrl {
18
+ partNumber: number;
19
+ url: string;
20
+ }
21
+ export interface DirectUploadInitiateInput {
22
+ /** Disk name; defaults to the manager's default disk. Must be multipart-capable. */
23
+ disk?: string | undefined;
24
+ /** Final object key. Resolve it server-side (per user/tenant) — never trust a client-sent key. */
25
+ key: string;
26
+ /** Content-Type stamped on the final object, and checked against `collection`'s whitelist. */
27
+ contentType?: string | undefined;
28
+ /** Total file size in bytes. Required: it fixes the part count both sides slice by. */
29
+ size: number;
30
+ /**
31
+ * Collection this upload is destined for. When the manager holds a collection registry, the
32
+ * declared `contentType` is checked against that collection's `acceptsMimeTypes` HERE — before
33
+ * the multipart upload is opened, before a single byte moves. Mirrors the TUS `POST` gate: fast
34
+ * feedback and bandwidth economy, not the security boundary — `attachExisting` re-validates the
35
+ * real bytes at attach time.
36
+ */
37
+ collection?: string | undefined;
38
+ /** Public/private visibility for the final object. */
39
+ visibility?: 'public' | 'private' | undefined;
40
+ /** Override the manager's part size for this upload. Same S3 bounds apply. */
41
+ partSize?: number | undefined;
42
+ /** Extra metadata persisted on the session (keys under `direct:` are reserved). */
43
+ metadata?: Record<string, string> | undefined;
44
+ }
45
+ /** What {@link DirectUploadManager.initiate} hands back — everything the client needs to start `PUT`ing. */
46
+ export interface DirectUploadCreatedSession {
47
+ /** Session id — the handle for `status`/`confirmPart`/`complete`/`abort`. */
48
+ id: string;
49
+ key: string;
50
+ disk: string;
51
+ /** The agreed slice size. The client MUST cut the file with exactly this. */
52
+ partSize: number;
53
+ size: number;
54
+ totalParts: number;
55
+ /** One presigned URL per part, 1-based, in order. */
56
+ parts: DirectUploadPartUrl[];
57
+ expiresAt?: Date | undefined;
58
+ }
59
+ /**
60
+ * The resume answer: what is already confirmed (ETags the server holds) and what is still pending
61
+ * (with FRESH presigned URLs). A client that lost its state — page reload, new tab, new device —
62
+ * asks this and continues from the parts that are missing, re-uploading nothing.
63
+ */
64
+ export interface DirectUploadStatus {
65
+ id: string;
66
+ key: string;
67
+ disk: string;
68
+ partSize: number;
69
+ size: number;
70
+ totalParts: number;
71
+ contentType?: string | undefined;
72
+ /** Parts whose ETags the session holds — already uploaded and confirmed. */
73
+ completedParts: MultipartPart[];
74
+ /** Parts still missing, each with a freshly presigned URL (expiry never strands an upload). */
75
+ pendingParts: DirectUploadPartUrl[];
76
+ expiresAt?: Date | undefined;
77
+ }
78
+ export interface DirectUploadManagerOptions {
79
+ storage: StorageManager;
80
+ /**
81
+ * Session persistence — the SAME SPI (and, with the Lucid driver, the same tables) the resumable
82
+ * (TUS) subsystem uses. The store must implement `addPart`/`listParts`; both bundled drivers do.
83
+ */
84
+ sessions: UploadSessionStore;
85
+ /** Collection registry (`media.collections`) for the `initiate` MIME gate. Omit ⇒ no gate here. */
86
+ collections?: MediaCollectionRegistry | undefined;
87
+ /** Default part size in bytes. Default 20 MiB ({@link DEFAULT_DIRECT_PART_SIZE}). */
88
+ partSize?: number | undefined;
89
+ /** Lifetime of presigned part URLs, in seconds. Default 3600. */
90
+ presignTtlSeconds?: number | undefined;
91
+ /** Session lifetime in seconds. Omit for sessions that never expire. */
92
+ sessionTtlSeconds?: number | undefined;
93
+ idGenerator?: (() => string) | undefined;
94
+ /** Injectable clock (tests drive expiry deterministically). Default `() => new Date()`. */
95
+ clock?: (() => Date) | undefined;
96
+ /** Emit `upload.*` diagnostics events (default true). */
97
+ emitDiagnostics?: boolean | undefined;
98
+ }
99
+ /**
100
+ * Session-backed direct-to-S3 multipart uploads: the browser `PUT`s every byte straight to storage
101
+ * through presigned part URLs, and the app only coordinates — open, presign, collect ETags, close.
102
+ * The app's bandwidth cost is a handful of JSON round-trips regardless of file size, which is the
103
+ * entire point for video-sized files: the TUS path streams every chunk THROUGH the app (2× the
104
+ * bytes: client→app, app→S3), where this path streams none.
105
+ *
106
+ * What this adds over the raw primitives on {@link UploadManager} (`initiateDirect` & friends) is
107
+ * the **persisted session**: `uploadId`, the agreed `partSize` and every confirmed part ETag live
108
+ * in an {@link UploadSessionStore} — the same SPI the TUS engine persists its offsets in — instead
109
+ * of in the client's memory. A page reload no longer orphans the upload: the client asks
110
+ * {@link status} and gets back the confirmed ETags plus fresh URLs for what is missing.
111
+ *
112
+ * Framework-free, like {@link ResumableUploadManager}: the `DirectUploadHandler` maps it onto HTTP
113
+ * and the AdonisJS provider mounts that. `MediaManager.completeDirectUploadToLibrary` bridges a
114
+ * finished session into the media library through `attachExisting` — where the collection's
115
+ * whitelist checks the REAL bytes, exactly like every other upload path.
116
+ */
117
+ export declare class DirectUploadManager {
118
+ #private;
119
+ private readonly storage;
120
+ private readonly sessions;
121
+ private readonly collections;
122
+ private readonly partSize;
123
+ private readonly presignTtlSeconds;
124
+ private readonly ttlSeconds;
125
+ private readonly newId;
126
+ private readonly now;
127
+ private readonly emitDiagnostics;
128
+ constructor(options: DirectUploadManagerOptions);
129
+ /**
130
+ * Open a direct upload: validate everything that can be validated without bytes, start the native
131
+ * multipart upload, persist the session, and presign every part URL in one batch (presigning is
132
+ * local computation — see `sigv4.ts` — so a 10,000-part batch costs no S3 round-trips).
133
+ */
134
+ initiate(input: DirectUploadInitiateInput): Promise<DirectUploadCreatedSession>;
135
+ /**
136
+ * Record one part the client finished `PUT`ing (its S3 `ETag` response header). This is what makes
137
+ * the session resumable: an ETag the server holds survives any client-side loss of state. Safe to
138
+ * retry — confirming the same part again overwrites its ETag.
139
+ */
140
+ confirmPart(id: string, part: MultipartPart): Promise<{
141
+ offset: number;
142
+ completedParts: number;
143
+ }>;
144
+ /**
145
+ * Where the upload stands, framed for resumption: the ETags already confirmed and a FRESH
146
+ * presigned URL for every part still missing. After a page reload the client calls this, slices
147
+ * the file with the returned `partSize`, uploads only `pendingParts`, and completes.
148
+ */
149
+ status(id: string): Promise<DirectUploadStatus>;
150
+ /**
151
+ * Assemble the parts into the final object and close the session. `parts` supplements what the
152
+ * session already holds (a client may hand over ETags it never got to confirm — e.g. it raced
153
+ * completion against its last confirms); on overlap the caller's ETag wins. Every part number in
154
+ * `1..totalParts` must be accounted for, or this throws {@link UploadPartsIncompleteError} naming
155
+ * the gaps — BEFORE the S3 round-trip that would report an opaque `InvalidPart`.
156
+ *
157
+ * This is the raw primitive (the object exists, no media record). To land in the library, use
158
+ * `MediaManager.completeDirectUploadToLibrary`, which chains `attachExisting` — collection
159
+ * whitelist against the real bytes included.
160
+ */
161
+ complete(id: string, parts?: MultipartPart[]): Promise<{
162
+ key: string;
163
+ disk: string;
164
+ size: number;
165
+ }>;
166
+ /**
167
+ * Discard an in-flight upload: abort the native multipart upload (S3 frees the stored parts and
168
+ * stops charging for them) and drop the session. The S3 abort is best-effort — a bucket lifecycle
169
+ * rule may have already reaped the upload (`NoSuchUpload`), and a session that can no longer be
170
+ * aborted remotely must still be deletable locally.
171
+ */
172
+ abort(id: string): Promise<void>;
173
+ /** In-progress direct sessions, optionally filtered. Empty when the store cannot enumerate. */
174
+ list(filter?: UploadSessionListFilter): Promise<UploadSession[]>;
175
+ private emit;
176
+ }
177
+ //# sourceMappingURL=direct_upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"direct_upload.d.ts","sourceRoot":"","sources":["../../src/direct_upload.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,299 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { publishMedia } from './diagnostics.js';
3
+ import { MimeNotAllowedError, UploadNotSupportedError, UploadPartOutOfRangeError, UploadPartSizeError, UploadPartsIncompleteError, UploadSessionExpiredError, UploadSessionNotFoundError, } from './errors.js';
4
+ import { isMultipartCapable } from './multipart.js';
5
+ /**
6
+ * Default part size: 20 MiB. Between S3's 5 MiB minimum and a reasonable payload per browser `PUT`;
7
+ * with S3's 10,000-part cap it covers files up to ~195 GiB. Larger than the proxy default (8 MiB)
8
+ * on purpose — direct parts don't occupy app memory, so the only cost of a bigger part is a bigger
9
+ * retry unit.
10
+ */
11
+ export const DEFAULT_DIRECT_PART_SIZE = 20 * 1024 * 1024;
12
+ /** S3's minimum part size (every part but the last). Enforced at `initiate` — see {@link UploadPartSizeError}. */
13
+ export const MIN_DIRECT_PART_SIZE = 5 * 1024 * 1024;
14
+ /** S3's cap on parts per multipart upload. */
15
+ export const MAX_DIRECT_PARTS = 10_000;
16
+ /**
17
+ * Default presigned part-URL lifetime (1 hour). Deliberately not sized to "the whole upload on a
18
+ * slow connection": {@link DirectUploadManager.status} re-issues fresh URLs for whatever is still
19
+ * pending, so an expired URL costs one status round-trip, not the upload.
20
+ */
21
+ const DEFAULT_PRESIGN_TTL_SECONDS = 3600;
22
+ /** Session-metadata key marking a session as a direct upload and carrying its agreed part size. */
23
+ const PART_SIZE_METADATA_KEY = 'direct:partSize';
24
+ /**
25
+ * Session-backed direct-to-S3 multipart uploads: the browser `PUT`s every byte straight to storage
26
+ * through presigned part URLs, and the app only coordinates — open, presign, collect ETags, close.
27
+ * The app's bandwidth cost is a handful of JSON round-trips regardless of file size, which is the
28
+ * entire point for video-sized files: the TUS path streams every chunk THROUGH the app (2× the
29
+ * bytes: client→app, app→S3), where this path streams none.
30
+ *
31
+ * What this adds over the raw primitives on {@link UploadManager} (`initiateDirect` & friends) is
32
+ * the **persisted session**: `uploadId`, the agreed `partSize` and every confirmed part ETag live
33
+ * in an {@link UploadSessionStore} — the same SPI the TUS engine persists its offsets in — instead
34
+ * of in the client's memory. A page reload no longer orphans the upload: the client asks
35
+ * {@link status} and gets back the confirmed ETags plus fresh URLs for what is missing.
36
+ *
37
+ * Framework-free, like {@link ResumableUploadManager}: the `DirectUploadHandler` maps it onto HTTP
38
+ * and the AdonisJS provider mounts that. `MediaManager.completeDirectUploadToLibrary` bridges a
39
+ * finished session into the media library through `attachExisting` — where the collection's
40
+ * whitelist checks the REAL bytes, exactly like every other upload path.
41
+ */
42
+ export class DirectUploadManager {
43
+ storage;
44
+ sessions;
45
+ collections;
46
+ partSize;
47
+ presignTtlSeconds;
48
+ ttlSeconds;
49
+ newId;
50
+ now;
51
+ emitDiagnostics;
52
+ constructor(options) {
53
+ this.storage = options.storage;
54
+ this.sessions = options.sessions;
55
+ this.collections = options.collections;
56
+ this.partSize = options.partSize ?? DEFAULT_DIRECT_PART_SIZE;
57
+ this.presignTtlSeconds = options.presignTtlSeconds ?? DEFAULT_PRESIGN_TTL_SECONDS;
58
+ this.ttlSeconds = options.sessionTtlSeconds;
59
+ this.newId = options.idGenerator ?? (() => randomUUID());
60
+ this.now = options.clock ?? (() => new Date());
61
+ this.emitDiagnostics = options.emitDiagnostics ?? true;
62
+ }
63
+ /**
64
+ * Open a direct upload: validate everything that can be validated without bytes, start the native
65
+ * multipart upload, persist the session, and presign every part URL in one batch (presigning is
66
+ * local computation — see `sigv4.ts` — so a 10,000-part batch costs no S3 round-trips).
67
+ */
68
+ async initiate(input) {
69
+ const diskName = input.disk ?? this.storage.defaultDisk;
70
+ const disk = this.storage.disk(diskName);
71
+ if (!isMultipartCapable(disk))
72
+ throw new UploadNotSupportedError(diskName);
73
+ if (typeof this.sessions.addPart !== 'function' ||
74
+ typeof this.sessions.listParts !== 'function') {
75
+ throw new UploadNotSupportedError(diskName, 'direct upload part recording');
76
+ }
77
+ if (!Number.isInteger(input.size) || input.size <= 0) {
78
+ throw new RangeError(`Direct upload size must be a positive integer, got ${input.size}`);
79
+ }
80
+ const partSize = input.partSize ?? this.partSize;
81
+ if (!Number.isInteger(partSize) || partSize < MIN_DIRECT_PART_SIZE) {
82
+ throw new UploadPartSizeError(`${partSize} bytes is below S3's ${MIN_DIRECT_PART_SIZE}-byte minimum`);
83
+ }
84
+ const totalParts = Math.max(1, Math.ceil(input.size / partSize));
85
+ if (totalParts > MAX_DIRECT_PARTS) {
86
+ throw new UploadPartSizeError(`${input.size} bytes at ${partSize} bytes/part needs ${totalParts} parts, over S3's cap of ${MAX_DIRECT_PARTS} — increase partSize`);
87
+ }
88
+ // The cheapest possible rejection, before the multipart upload even opens. Only the DECLARED
89
+ // type can be checked here (there are no bytes yet); the collection config stays the single
90
+ // source of truth, and attach-time validation of the real content remains the final barrier.
91
+ if (input.collection !== undefined && this.collections !== undefined) {
92
+ const accepts = this.collections.get(input.collection).acceptsMimeTypes;
93
+ if (accepts && input.contentType !== undefined && !accepts.includes(input.contentType)) {
94
+ throw new MimeNotAllowedError(input.collection, input.contentType);
95
+ }
96
+ }
97
+ const putOptions = input.contentType !== undefined || input.visibility !== undefined
98
+ ? {
99
+ ...(input.contentType !== undefined ? { contentType: input.contentType } : {}),
100
+ ...(input.visibility !== undefined ? { visibility: input.visibility } : {}),
101
+ }
102
+ : undefined;
103
+ const { uploadId } = await disk.createMultipartUpload(input.key, putOptions);
104
+ const createdAt = this.now();
105
+ const session = await this.sessions.create({
106
+ id: this.newId(),
107
+ disk: diskName,
108
+ key: input.key,
109
+ contentType: input.contentType,
110
+ size: input.size,
111
+ offset: 0,
112
+ parts: 0,
113
+ multipartUploadId: uploadId,
114
+ createdAt,
115
+ ...(this.ttlSeconds !== undefined
116
+ ? { expiresAt: new Date(createdAt.getTime() + this.ttlSeconds * 1000) }
117
+ : {}),
118
+ metadata: { ...input.metadata, [PART_SIZE_METADATA_KEY]: String(partSize) },
119
+ });
120
+ const parts = await this.#presignParts(disk, session, Array.from({ length: totalParts }, (_, index) => index + 1));
121
+ this.emit('upload.start', {
122
+ id: session.id,
123
+ disk: diskName,
124
+ key: input.key,
125
+ mode: 'direct',
126
+ size: input.size,
127
+ contentType: input.contentType,
128
+ });
129
+ return {
130
+ id: session.id,
131
+ key: session.key,
132
+ disk: diskName,
133
+ partSize,
134
+ size: input.size,
135
+ totalParts,
136
+ parts,
137
+ expiresAt: session.expiresAt,
138
+ };
139
+ }
140
+ /**
141
+ * Record one part the client finished `PUT`ing (its S3 `ETag` response header). This is what makes
142
+ * the session resumable: an ETag the server holds survives any client-side loss of state. Safe to
143
+ * retry — confirming the same part again overwrites its ETag.
144
+ */
145
+ async confirmPart(id, part) {
146
+ const session = await this.#require(id);
147
+ const { partSize, totalParts } = this.#geometry(session);
148
+ if (!Number.isInteger(part.partNumber) || part.partNumber < 1 || part.partNumber > totalParts) {
149
+ throw new UploadPartOutOfRangeError(part.partNumber, totalParts);
150
+ }
151
+ await this.sessions.addPart?.(id, { partNumber: part.partNumber, etag: part.etag });
152
+ // Recompute progress from what is actually recorded (parts confirm in any order and in
153
+ // parallel), so offset means "bytes safely on S3" rather than a linear cursor.
154
+ const confirmed = (await this.sessions.listParts?.(id)) ?? [];
155
+ session.parts = confirmed.length;
156
+ session.offset = confirmed.reduce((sum, recorded) => sum + this.#partBytes(session, partSize, totalParts, recorded.partNumber), 0);
157
+ await this.sessions.update(session);
158
+ this.emit('upload.progress', {
159
+ id: session.id,
160
+ offset: session.offset,
161
+ parts: session.parts,
162
+ size: session.size,
163
+ });
164
+ return { offset: session.offset, completedParts: confirmed.length };
165
+ }
166
+ /**
167
+ * Where the upload stands, framed for resumption: the ETags already confirmed and a FRESH
168
+ * presigned URL for every part still missing. After a page reload the client calls this, slices
169
+ * the file with the returned `partSize`, uploads only `pendingParts`, and completes.
170
+ */
171
+ async status(id) {
172
+ const session = await this.#require(id);
173
+ const disk = this.storage.disk(session.disk);
174
+ if (!isMultipartCapable(disk))
175
+ throw new UploadNotSupportedError(session.disk);
176
+ const { partSize, totalParts } = this.#geometry(session);
177
+ const completedParts = [...((await this.sessions.listParts?.(id)) ?? [])].sort((a, b) => a.partNumber - b.partNumber);
178
+ const confirmedNumbers = new Set(completedParts.map((part) => part.partNumber));
179
+ const pendingNumbers = Array.from({ length: totalParts }, (_, index) => index + 1).filter((partNumber) => !confirmedNumbers.has(partNumber));
180
+ return {
181
+ id: session.id,
182
+ key: session.key,
183
+ disk: session.disk,
184
+ partSize,
185
+ size: session.size,
186
+ totalParts,
187
+ contentType: session.contentType,
188
+ completedParts,
189
+ pendingParts: await this.#presignParts(disk, session, pendingNumbers),
190
+ expiresAt: session.expiresAt,
191
+ };
192
+ }
193
+ /**
194
+ * Assemble the parts into the final object and close the session. `parts` supplements what the
195
+ * session already holds (a client may hand over ETags it never got to confirm — e.g. it raced
196
+ * completion against its last confirms); on overlap the caller's ETag wins. Every part number in
197
+ * `1..totalParts` must be accounted for, or this throws {@link UploadPartsIncompleteError} naming
198
+ * the gaps — BEFORE the S3 round-trip that would report an opaque `InvalidPart`.
199
+ *
200
+ * This is the raw primitive (the object exists, no media record). To land in the library, use
201
+ * `MediaManager.completeDirectUploadToLibrary`, which chains `attachExisting` — collection
202
+ * whitelist against the real bytes included.
203
+ */
204
+ async complete(id, parts = []) {
205
+ const session = await this.#require(id);
206
+ const disk = this.storage.disk(session.disk);
207
+ if (!isMultipartCapable(disk))
208
+ throw new UploadNotSupportedError(session.disk);
209
+ const { totalParts } = this.#geometry(session);
210
+ const byNumber = new Map();
211
+ for (const recorded of (await this.sessions.listParts?.(id)) ?? []) {
212
+ byNumber.set(recorded.partNumber, recorded);
213
+ }
214
+ for (const given of parts)
215
+ byNumber.set(given.partNumber, given);
216
+ const missing = Array.from({ length: totalParts }, (_, index) => index + 1).filter((partNumber) => !byNumber.has(partNumber));
217
+ if (missing.length > 0)
218
+ throw new UploadPartsIncompleteError(id, missing);
219
+ const assembled = [...byNumber.values()].sort((a, b) => a.partNumber - b.partNumber);
220
+ await disk.completeMultipartUpload(session.key, session.multipartUploadId, assembled);
221
+ await this.sessions.delete(id);
222
+ this.emit('upload.complete', { id: session.id, disk: session.disk, key: session.key });
223
+ return { key: session.key, disk: session.disk, size: session.size };
224
+ }
225
+ /**
226
+ * Discard an in-flight upload: abort the native multipart upload (S3 frees the stored parts and
227
+ * stops charging for them) and drop the session. The S3 abort is best-effort — a bucket lifecycle
228
+ * rule may have already reaped the upload (`NoSuchUpload`), and a session that can no longer be
229
+ * aborted remotely must still be deletable locally.
230
+ */
231
+ async abort(id) {
232
+ const session = await this.sessions.get(id);
233
+ if (!session)
234
+ return;
235
+ const disk = this.storage.disk(session.disk);
236
+ if (session.multipartUploadId && isMultipartCapable(disk)) {
237
+ await disk.abortMultipartUpload(session.key, session.multipartUploadId).catch(() => { });
238
+ }
239
+ await this.sessions.delete(id);
240
+ this.emit('upload.abort', { id: session.id, disk: session.disk, key: session.key });
241
+ }
242
+ /** In-progress direct sessions, optionally filtered. Empty when the store cannot enumerate. */
243
+ async list(filter) {
244
+ const sessions = (await this.sessions.list?.(filter)) ?? [];
245
+ return sessions.filter((session) => session.metadata?.[PART_SIZE_METADATA_KEY] !== undefined);
246
+ }
247
+ /** Presign a `PUT` URL for each given part number, in one local batch. */
248
+ async #presignParts(disk, session, partNumbers) {
249
+ const urls = [];
250
+ for (const partNumber of partNumbers) {
251
+ urls.push({
252
+ partNumber,
253
+ url: await disk.presignUploadPart(session.key, session.multipartUploadId, partNumber, this.presignTtlSeconds),
254
+ });
255
+ }
256
+ return urls;
257
+ }
258
+ /** The agreed slicing for a session, recovered from its persisted metadata. */
259
+ #geometry(session) {
260
+ const partSize = Number(session.metadata?.[PART_SIZE_METADATA_KEY]);
261
+ const size = session.size;
262
+ return { partSize, totalParts: Math.max(1, Math.ceil(size / partSize)) };
263
+ }
264
+ /** Byte length of one part: `partSize`, except the last part which takes the remainder. */
265
+ #partBytes(session, partSize, totalParts, partNumber) {
266
+ const size = session.size;
267
+ return partNumber === totalParts ? size - (totalParts - 1) * partSize : partSize;
268
+ }
269
+ /**
270
+ * Load a session, requiring it to be a DIRECT one: the store may be shared with the TUS engine
271
+ * (the Lucid driver even shares tables), so a session lacking the direct part-size marker — or a
272
+ * declared size, or a native upload id — is "not found" here, never misinterpreted. An expired
273
+ * session is treated as gone: native upload aborted, record deleted,
274
+ * {@link UploadSessionExpiredError} thrown.
275
+ */
276
+ async #require(id) {
277
+ const session = await this.sessions.get(id);
278
+ if (!session ||
279
+ session.multipartUploadId === undefined ||
280
+ session.size === undefined ||
281
+ !Number.isFinite(Number(session.metadata?.[PART_SIZE_METADATA_KEY]))) {
282
+ throw new UploadSessionNotFoundError(id);
283
+ }
284
+ if (session.expiresAt && session.expiresAt.getTime() <= this.now().getTime()) {
285
+ const disk = this.storage.disk(session.disk);
286
+ if (isMultipartCapable(disk)) {
287
+ await disk.abortMultipartUpload(session.key, session.multipartUploadId).catch(() => { });
288
+ }
289
+ await this.sessions.delete(id);
290
+ throw new UploadSessionExpiredError(id);
291
+ }
292
+ return session;
293
+ }
294
+ emit(event, payload) {
295
+ if (this.emitDiagnostics)
296
+ publishMedia(event, payload);
297
+ }
298
+ }
299
+ //# sourceMappingURL=direct_upload.js.map