@adonis-agora/media 0.12.0 → 0.12.2
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/CHANGELOG.md +28 -0
- package/package.json +11 -8
- package/skills/media-attachments-single-file/SKILL.md +224 -0
- package/skills/media-library-collections/SKILL.md +334 -0
- package/skills/media-setup/SKILL.md +253 -0
- package/skills/media-stores-delivery/SKILL.md +302 -0
- package/skills/media-uploads-resumable/SKILL.md +347 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: media-setup
|
|
3
|
+
description: >-
|
|
4
|
+
Install and configure @adonis-agora/media in an AdonisJS app. Covers node ace
|
|
5
|
+
configure (media_provider + dashboard_provider registration), config/media.ts via
|
|
6
|
+
defineConfig with stores/processors/disks/uploadSessions factory maps, the
|
|
7
|
+
media singleton from @adonis-agora/media/services/main, disk resolution precedence
|
|
8
|
+
(per-call > collection > top-level disk > Drive default), lazy optional peers
|
|
9
|
+
(@adonisjs/lucid, sharp, @aws-sdk/client-s3, mediabunny), StoreNotConfiguredError,
|
|
10
|
+
DriveNotReadyError, and the embedded console provider. Use when setting up media,
|
|
11
|
+
choosing a MediaStore, wiring disks, or fixing boot errors.
|
|
12
|
+
license: MIT
|
|
13
|
+
metadata:
|
|
14
|
+
type: core
|
|
15
|
+
library: "@adonis-agora/media"
|
|
16
|
+
library_version: "0.12.1"
|
|
17
|
+
framework: adonisjs
|
|
18
|
+
sources:
|
|
19
|
+
- "DavideCarvalho/adonis-media:README.md"
|
|
20
|
+
- "DavideCarvalho/adonis-media:docs/getting-started.mdx"
|
|
21
|
+
- "DavideCarvalho/adonis-media:docs/configuration.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-media:packages/adonis/src/define_config.ts"
|
|
23
|
+
- "DavideCarvalho/adonis-media:packages/adonis/src/media_manager.ts"
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Setting up @adonis-agora/media
|
|
27
|
+
|
|
28
|
+
`@adonis-agora/media` is a media library for AdonisJS on top of `@adonisjs/drive`: owner
|
|
29
|
+
collections with policy, column attachments, image conversions, content transformers,
|
|
30
|
+
large-file uploads, configurable delivery, and an embedded management console. Storage is
|
|
31
|
+
delegated entirely to Drive — this package never reimplements disk drivers.
|
|
32
|
+
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Install Drive (the storage backend) plus the package, then run the configure codemod:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node ace add @adonisjs/drive # required — media owns no disk drivers
|
|
39
|
+
npm i @adonis-agora/media
|
|
40
|
+
node ace configure @adonis-agora/media
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`configure` registers **both** providers in `adonisrc.ts`, publishes `config/media.ts`
|
|
44
|
+
and `config/media_dashboard.ts`, and publishes two Lucid migrations (one for the optional
|
|
45
|
+
`lucid` media store, one for the upload-session store). Delete the migrations you don't
|
|
46
|
+
need; the in-memory stores need none.
|
|
47
|
+
|
|
48
|
+
```ts title="config/media.ts"
|
|
49
|
+
import { defineConfig, stores, processors } from '@adonis-agora/media'
|
|
50
|
+
|
|
51
|
+
export default defineConfig({
|
|
52
|
+
// disk: 's3', // omit to use Drive's default disk
|
|
53
|
+
store: 'memory',
|
|
54
|
+
stores: {
|
|
55
|
+
memory: stores.memory(),
|
|
56
|
+
lucid: stores.lucid({ connection: 'pg' }),
|
|
57
|
+
},
|
|
58
|
+
imageProcessor: processors.sharp(),
|
|
59
|
+
collections: [
|
|
60
|
+
{ name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
|
|
61
|
+
{
|
|
62
|
+
name: 'gallery',
|
|
63
|
+
conversions: [
|
|
64
|
+
{ name: 'thumb', width: 200, height: 200 },
|
|
65
|
+
{ name: 'og', width: 1200, eager: true },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Reach the singleton manager anywhere:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import media from '@adonis-agora/media/services/main'
|
|
76
|
+
|
|
77
|
+
const url = await media.library.url(recordId)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Source: `docs/getting-started.mdx`, `docs/configuration.mdx`.
|
|
81
|
+
|
|
82
|
+
## Core patterns
|
|
83
|
+
|
|
84
|
+
### Pattern 1 — select drivers by name from lazy factory maps
|
|
85
|
+
|
|
86
|
+
The Agora idiom: drivers live in the core package (`stores`, `processors`, `disks`,
|
|
87
|
+
`uploadSessions`, `transformers`), you select them **by name**, and each heavy optional peer
|
|
88
|
+
(`@adonisjs/lucid`, `sharp`, `@aws-sdk/client-s3`, `mediabunny`) is imported only when its
|
|
89
|
+
driver is selected at boot.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { defineConfig, stores, processors, disks } from '@adonis-agora/media'
|
|
93
|
+
|
|
94
|
+
export default defineConfig({
|
|
95
|
+
store: 'lucid',
|
|
96
|
+
stores: { lucid: stores.lucid({ connection: 'pg' }) },
|
|
97
|
+
imageProcessor: processors.sharp(),
|
|
98
|
+
disks: { s3: disks.s3({ bucket: 'my-bucket', region: 'us-east-1' }) },
|
|
99
|
+
disk: 's3',
|
|
100
|
+
})
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Listing a driver you never select costs nothing — the thunk imports nothing until built.
|
|
104
|
+
An empty `defineConfig({})` gives the in-memory store on Drive's default disk, no
|
|
105
|
+
conversions, no upload routes, `auto` delivery, diagnostics on.
|
|
106
|
+
|
|
107
|
+
Source: `docs/configuration.mdx`.
|
|
108
|
+
|
|
109
|
+
### Pattern 2 — disk precedence
|
|
110
|
+
|
|
111
|
+
Which disk a write targets resolves per-call first, then collection, then top-level, then
|
|
112
|
+
Drive's default:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
defineConfig({
|
|
116
|
+
disk: 'public-s3', // 3rd choice
|
|
117
|
+
collections: [
|
|
118
|
+
{ name: 'invoices', disk: 'private-s3' }, // 2nd choice
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// per-call wins over everything:
|
|
123
|
+
await media.library.attach({ ..., disk: 'temp-fs' }) // 1st choice
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Where the NAME resolves from has separate precedence: the config's own `disks` map first,
|
|
127
|
+
then `config/drive.ts`. A record stores which disk it lives on, so moving a collection to a
|
|
128
|
+
new disk never strands existing files.
|
|
129
|
+
|
|
130
|
+
Source: `docs/configuration.mdx` (`disk` — where files live).
|
|
131
|
+
|
|
132
|
+
### Pattern 3 — DI instead of the service import
|
|
133
|
+
|
|
134
|
+
The provider binds one singleton `MediaManager`. Prefer it through the container when you
|
|
135
|
+
want testability:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { MediaManager } from '@adonis-agora/media'
|
|
139
|
+
|
|
140
|
+
export default class PostImagesController {
|
|
141
|
+
constructor(protected media: MediaManager) {}
|
|
142
|
+
}
|
|
143
|
+
// or: const media = await app.container.make(MediaManager)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Accessors: `media.library` (collections), `media.attachments` (column attachments),
|
|
147
|
+
`media.disk(name?)` (raw Drive disk escape hatch), `media.store` (cross-owner reads),
|
|
148
|
+
and the opt-in coordinators `media.uploads` / `media.resumable` / `media.direct`.
|
|
149
|
+
|
|
150
|
+
Source: `docs/getting-started.mdx` (Step 2), `packages/adonis/src/media_manager.ts`.
|
|
151
|
+
|
|
152
|
+
## Common mistakes
|
|
153
|
+
|
|
154
|
+
### [CRITICAL] Calling the media singleton at module scope before Drive boots
|
|
155
|
+
|
|
156
|
+
Wrong:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// module scope — runs before the app (and Drive) has booted
|
|
160
|
+
import media from '@adonis-agora/media/services/main'
|
|
161
|
+
export const DEFAULT_URL = await media.library.url('seed-image')
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Correct:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import media from '@adonis-agora/media/services/main'
|
|
168
|
+
|
|
169
|
+
export default class LogoController {
|
|
170
|
+
async show() {
|
|
171
|
+
const url = await media.library.url('seed-image') // inside request lifecycle
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Mechanism: a disk resolution reaching `@adonisjs/drive` before its manager has booted throws
|
|
177
|
+
`DriveNotReadyError` (`E_MEDIA_DRIVE_NOT_READY`) — almost always a media call at module scope
|
|
178
|
+
instead of inside a request or `app.booted`.
|
|
179
|
+
Source: `docs/errors.mdx` (Storage and configuration).
|
|
180
|
+
|
|
181
|
+
### [HIGH] Naming a `store` that has no matching factory entry
|
|
182
|
+
|
|
183
|
+
Wrong:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
defineConfig({
|
|
187
|
+
store: 'luicd', // typo
|
|
188
|
+
stores: { lucid: stores.lucid() },
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Correct:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
defineConfig({
|
|
196
|
+
store: 'lucid',
|
|
197
|
+
stores: { lucid: stores.lucid() },
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Mechanism: the provider throws `StoreNotConfiguredError` at boot — deliberately NOT a silent
|
|
202
|
+
fallback to in-memory, because a typo quietly swapping durable persistence for a process-local
|
|
203
|
+
Map is discovered only after data loss. Only the zero-config path (no `store` named at all)
|
|
204
|
+
resolves to in-memory. Same rule for upload-session stores.
|
|
205
|
+
Source: `docs/configuration.mdx` (How the provider builds it), `docs/errors.mdx`.
|
|
206
|
+
|
|
207
|
+
### [HIGH] Selecting lucid/sharp without installing the peer or running the migration
|
|
208
|
+
|
|
209
|
+
Wrong:
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
npm i @adonis-agora/media # no @adonisjs/lucid, no sharp, no migration:run
|
|
213
|
+
# config selects store: 'lucid' + conversions → fails at boot / first attach
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Correct:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
node ace add @adonisjs/lucid # or ensure it exists
|
|
220
|
+
npm i sharp
|
|
221
|
+
node ace migration:run # published by configure for the lucid stores
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Mechanism: the factory thunks import their peers lazily at boot-time selection; selecting
|
|
225
|
+
`lucid` without the published `media` table fails on the first store write, not at
|
|
226
|
+
`configure` time.
|
|
227
|
+
Source: `docs/getting-started.mdx` (Prerequisites, Callout on migrations).
|
|
228
|
+
|
|
229
|
+
### [MEDIUM] Importing the dashboard's defineConfig into config/media.ts
|
|
230
|
+
|
|
231
|
+
Wrong:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// config/media.ts
|
|
235
|
+
import { defineConfig } from '@adonis-agora/media/dashboard' // wrong file's helper
|
|
236
|
+
export default defineConfig({ collections: [{ name: 'gallery' }] })
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Correct:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
// config/media.ts
|
|
243
|
+
import { defineConfig } from '@adonis-agora/media'
|
|
244
|
+
// config/media_dashboard.ts uses:
|
|
245
|
+
// import { defineConfig } from '@adonis-agora/media/dashboard'
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Mechanism: both packages export a `defineConfig`; the wrong one types the wrong file and
|
|
249
|
+
silently drops unknown keys instead of failing.
|
|
250
|
+
Source: README.md (subpath exports table), `docs/dashboard/index.mdx`.
|
|
251
|
+
|
|
252
|
+
See also: `media-library-collections/SKILL.md` — attaching to collections after setup;
|
|
253
|
+
`media-stores-delivery/SKILL.md` — writing custom stores/processors/disks.
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: media-stores-delivery
|
|
3
|
+
description: >-
|
|
4
|
+
Pluggable storage seams and delivery in @adonis-agora/media — the MediaStore SPI
|
|
5
|
+
(stores.memory / stores.lucid with the published migration, custom stores via
|
|
6
|
+
StoreFactory, cross-owner keyset list() with encodeMediaCursor/clampMediaListLimit),
|
|
7
|
+
the ImageProcessor SPI (processors.sharp, custom processors), the structural Disk
|
|
8
|
+
contract (putStream, getVisibility), createDriveBackedResolver (module namespace,
|
|
9
|
+
not default export), the bundled disks.s3() with extended operations
|
|
10
|
+
(copy/move/deleteMany/list/size/stat) and declarative visibility, presignS3Url,
|
|
11
|
+
delivery.mode auto|public|signed|proxy, MediaDeliveryHandler and HlsDeliveryHandler
|
|
12
|
+
route mounting, and the @adonis-agora/media/testing doubles. Use when choosing or
|
|
13
|
+
writing a store, wiring S3, serving media, signing URLs, or testing without disks.
|
|
14
|
+
license: MIT
|
|
15
|
+
metadata:
|
|
16
|
+
type: core
|
|
17
|
+
library: "@adonis-agora/media"
|
|
18
|
+
library_version: "0.12.1"
|
|
19
|
+
framework: adonisjs
|
|
20
|
+
sources:
|
|
21
|
+
- "DavideCarvalho/adonis-media:docs/stores-and-processors.mdx"
|
|
22
|
+
- "DavideCarvalho/adonis-media:docs/storage/s3-disk.mdx"
|
|
23
|
+
- "DavideCarvalho/adonis-media:docs/delivery.mdx"
|
|
24
|
+
- "DavideCarvalho/adonis-media:docs/testing.mdx"
|
|
25
|
+
- "DavideCarvalho/adonis-media:packages/adonis/src/disks/drive.ts"
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Storage seams & delivery
|
|
29
|
+
|
|
30
|
+
The library delegates everything heavy to three pluggable seams — the `MediaStore`
|
|
31
|
+
(metadata persistence), the `ImageProcessor` (conversions), and a structural `Disk`
|
|
32
|
+
contract that real `@adonisjs/drive` disks satisfy directly. `delivery` is the mirror-image
|
|
33
|
+
strategy for **reads**.
|
|
34
|
+
|
|
35
|
+
## Setup
|
|
36
|
+
|
|
37
|
+
```ts title="config/media.ts"
|
|
38
|
+
import { defineConfig, stores, processors, disks } from '@adonis-agora/media'
|
|
39
|
+
|
|
40
|
+
export default defineConfig({
|
|
41
|
+
disk: 's3',
|
|
42
|
+
disks: { s3: disks.s3({ bucket: 'my-bucket', region: 'us-east-1' }) },
|
|
43
|
+
store: 'lucid',
|
|
44
|
+
stores: { lucid: stores.lucid({ connection: 'pg', table: 'media' }) },
|
|
45
|
+
imageProcessor: processors.sharp(),
|
|
46
|
+
delivery: { mode: 'auto', signedTtlSeconds: 300 },
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
node ace migration:run # publishes the media table for stores.lucid()
|
|
52
|
+
npm i @aws-sdk/client-s3 # optional peer, imported only because s3 is selected
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Source: `docs/configuration.mdx`, `docs/stores-and-processors.mdx`.
|
|
56
|
+
|
|
57
|
+
## Core patterns
|
|
58
|
+
|
|
59
|
+
### Pattern 1 — the cross-owner `MediaStore.list` read
|
|
60
|
+
|
|
61
|
+
`listByOwner` answers "what has this entity got"; `list` answers "everything, paginated,
|
|
62
|
+
newest first" — the read the dashboard's Collections view is built on:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import media from '@adonis-agora/media/services/main'
|
|
66
|
+
|
|
67
|
+
const page = await media.store.list({
|
|
68
|
+
collection: 'gallery',
|
|
69
|
+
ownerType: 'Post',
|
|
70
|
+
prefix: 'Post/42/',
|
|
71
|
+
limit: 50, // default 50, clamped to 200
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
page.items // ordered createdAt desc, then id desc (stable keyset under writes)
|
|
75
|
+
page.nextCursor // opaque string, or null on the last page
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Ordering is a stable keyset so paging stays consistent while rows are written underneath.
|
|
79
|
+
`encodeMediaCursor` / `decodeMediaCursor` / `clampMediaListLimit` and the
|
|
80
|
+
`DEFAULT_MEDIA_LIST_LIMIT` / `MAX_MEDIA_LIST_LIMIT` constants are exported for implementers.
|
|
81
|
+
|
|
82
|
+
Source: `docs/dashboard/collections.mdx`, `docs/stores-and-processors.mdx`.
|
|
83
|
+
|
|
84
|
+
### Pattern 2 — wire a custom store or processor
|
|
85
|
+
|
|
86
|
+
Both seams are tiny interfaces; wire them through a factory thunk so any peer loads lazily:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { defineConfig } from '@adonis-agora/media'
|
|
90
|
+
import type { ImageProcessor, MediaStore, StoreFactory } from '@adonis-agora/media'
|
|
91
|
+
|
|
92
|
+
class RedisMediaStore implements MediaStore { /* save/find/listByOwner/list/delete/nextOrder */ }
|
|
93
|
+
class PassthroughProcessor implements ImageProcessor {
|
|
94
|
+
async convert(input: Buffer, preset) {
|
|
95
|
+
return { data: input, format: preset.format ?? 'webp', contentType: 'image/webp' }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
defineConfig({
|
|
100
|
+
store: 'redis',
|
|
101
|
+
stores: {
|
|
102
|
+
redis: (() => async () => new RedisMediaStore()) as unknown as StoreFactory,
|
|
103
|
+
},
|
|
104
|
+
imageProcessor: new PassthroughProcessor(), // or a factory thunk
|
|
105
|
+
})
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A `StoreFactory` receives the booted app as `StoreContext`, so a driver can resolve a peer's
|
|
109
|
+
service when it builds.
|
|
110
|
+
|
|
111
|
+
Source: `docs/stores-and-processors.mdx` (Writing your own store / processor).
|
|
112
|
+
|
|
113
|
+
### Pattern 3 — the bundled S3 disk and its extended operations
|
|
114
|
+
|
|
115
|
+
`disks.s3()` implements the base `Disk` contract plus `ExtendedDisk` (detected structurally
|
|
116
|
+
with `isExtendedDisk`) and `MultipartUploadDisk` (`isMultipartCapable`):
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const s3 = media.disk('s3')
|
|
120
|
+
|
|
121
|
+
if (isExtendedDisk(s3)) {
|
|
122
|
+
await s3.copy('a/photo.jpg', 'b/photo.jpg', { toBucket: 'archive' })
|
|
123
|
+
await s3.deleteMany(['a/1.jpg', 'a/2.jpg'])
|
|
124
|
+
await s3.stat('a/photo.jpg') // { size, contentType?, lastModified? }
|
|
125
|
+
const page = await s3.list('uploads/', { delimiter: '/', limit: 100 })
|
|
126
|
+
page.folders // sub-prefixes from CommonPrefixes
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Key options: `endpoint` + `forcePathStyle` for MinIO/R2/Spaces, `publicEndpoint` for the
|
|
131
|
+
host presigned URLs are signed against (the browser's endpoint), `publicBaseUrl` for stable
|
|
132
|
+
`getUrl()` output, and `visibility: 'public' | 'private'` (default `private`) — declarative,
|
|
133
|
+
read by `delivery.mode: 'auto'`, never applied as an ACL and never probed.
|
|
134
|
+
|
|
135
|
+
Source: `docs/storage/s3-disk.mdx`.
|
|
136
|
+
|
|
137
|
+
### Pattern 4 — serve media through a delivery route you own
|
|
138
|
+
|
|
139
|
+
`delivery.mode` (`auto` default) decides what `deliver()` returns: `public`/`signed` yield
|
|
140
|
+
`{ kind: 'redirect', url }`; `proxy` yields `{ kind: 'stream', stream, mimeType, size,
|
|
141
|
+
fileName }`. `auto` asks the disk's `getVisibility` — public ⇒ public, private ⇒ signed,
|
|
142
|
+
unknown ⇒ signed. `MediaDeliveryHandler` is framework-agnostic: YOU mount the route behind
|
|
143
|
+
YOUR auth:
|
|
144
|
+
|
|
145
|
+
```ts title="start/routes.ts"
|
|
146
|
+
import { MediaDeliveryHandler, MediaManager } from '@adonis-agora/media'
|
|
147
|
+
|
|
148
|
+
const media = await app.container.make(MediaManager)
|
|
149
|
+
const delivery = new MediaDeliveryHandler({ library: media })
|
|
150
|
+
|
|
151
|
+
router.get('/media/:id', async ({ params, request, response, auth }) => {
|
|
152
|
+
await authorizeMediaAccess(auth.user, params.id) // ← your rule, before handle()
|
|
153
|
+
|
|
154
|
+
const result = await delivery.handle({ mediaId: params.id, conversion: request.input('conversion') })
|
|
155
|
+
if (result.kind === 'redirect') return response.redirect(result.url)
|
|
156
|
+
|
|
157
|
+
response.header('content-type', result.mimeType)
|
|
158
|
+
if (result.size !== undefined) response.header('content-length', String(result.size))
|
|
159
|
+
response.header('content-disposition', `inline; filename="${result.fileName}"`)
|
|
160
|
+
return response.stream(result.stream)
|
|
161
|
+
}).use(middleware.auth())
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The handler's own `mode` / `signedTtlSeconds` override the configured ones per call. For
|
|
165
|
+
transformer-generated HLS packages use `HlsDeliveryHandler` — playlists are rewritten per
|
|
166
|
+
request, segment files are validated against the persisted artifact list (no caller input
|
|
167
|
+
ever reaches the disk as a path).
|
|
168
|
+
|
|
169
|
+
Source: `docs/delivery.mdx`, `docs/transformers.mdx` (Serving HLS).
|
|
170
|
+
|
|
171
|
+
## Common mistakes
|
|
172
|
+
|
|
173
|
+
### [CRITICAL] Passing the drive service default export instead of the module namespace
|
|
174
|
+
|
|
175
|
+
Wrong:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import driveService from '@adonisjs/drive/services/main' // captured default
|
|
179
|
+
const resolve = createDriveBackedResolver({ driveService, configuredDisks: {}, defaultDisk: 's3' })
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Correct:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import * as driveService from '@adonisjs/drive/services/main' // the NAMESPACE
|
|
186
|
+
const resolve = createDriveBackedResolver({ driveService, configuredDisks: {}, defaultDisk: 's3' })
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Mechanism: Drive's service module assigns its manager inside an `app.booted()` callback, so
|
|
190
|
+
at import time the default export is still `undefined` — a captured value stays `undefined`
|
|
191
|
+
forever (writes fail with `DriveNotReadyError`), while the namespace's ESM live binding
|
|
192
|
+
reflects the later assignment.
|
|
193
|
+
Source: `docs/stores-and-processors.mdx` (createDriveBackedResolver).
|
|
194
|
+
|
|
195
|
+
### [CRITICAL] Exposing a delivery route without authorization
|
|
196
|
+
|
|
197
|
+
Wrong:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
router.get('/media/:id', async ({ params, response }) => {
|
|
201
|
+
const result = await delivery.handle({ mediaId: params.id })
|
|
202
|
+
if (result.kind === 'redirect') return response.redirect(result.url)
|
|
203
|
+
return response.stream(result.stream)
|
|
204
|
+
}) // no middleware — every record is public to anyone who can guess an id
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Correct:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
router.get('/media/:id', async ({ params, request, response, auth }) => {
|
|
211
|
+
await authorizeMediaAccess(auth.user, params.id)
|
|
212
|
+
const result = await delivery.handle({ mediaId: params.id })
|
|
213
|
+
/* ... */
|
|
214
|
+
}).use(middleware.auth())
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Mechanism: `MediaDeliveryHandler` resolves a media id to bytes or a URL and performs NO
|
|
218
|
+
authorization — the same split as `TusUploadHandler`. This is also why the provider mounts
|
|
219
|
+
no delivery route of its own: there is no safe default.
|
|
220
|
+
Source: `docs/delivery.mdx` (error callout: The handler performs NO authorization).
|
|
221
|
+
|
|
222
|
+
### [HIGH] Relying on auto delivery for storage the browser cannot reach
|
|
223
|
+
|
|
224
|
+
Wrong:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
disks.s3({ bucket: 'media', endpoint: 'http://minio.internal:9000' }) // internal host
|
|
228
|
+
// delivery auto signs URLs against the INTERNAL host → unresolvable in the browser
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Correct:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
disks.s3({
|
|
235
|
+
bucket: 'media',
|
|
236
|
+
endpoint: 'http://minio.internal:9000', // server-side operations
|
|
237
|
+
publicEndpoint: 'https://files.example.com', // what presigned URLs are signed for
|
|
238
|
+
forcePathStyle: true,
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Mechanism: `auto` resolves to `signed` (never `proxy`) when visibility is unknown, and
|
|
243
|
+
SigV4 bakes the host into the signature — presigned URLs must be signed for the endpoint
|
|
244
|
+
the BROWSER reaches. Proxying is a deployment decision and must be opted into explicitly.
|
|
245
|
+
Source: `docs/delivery.mdx` (auto never picks proxy), `docs/storage/s3-disk.mdx` (publicEndpoint).
|
|
246
|
+
|
|
247
|
+
### [MEDIUM] Implementing a custom MediaStore without a working list()
|
|
248
|
+
|
|
249
|
+
Wrong:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
class MyStore implements MediaStore {
|
|
253
|
+
async list() { return { items: [], nextCursor: null } } // stubbed "for now"
|
|
254
|
+
// save/find/listByOwner/delete/nextOrder implemented
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Correct:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
class MyStore implements MediaStore {
|
|
262
|
+
async list(options?: MediaListOptions): Promise<MediaListPage> {
|
|
263
|
+
// keyset page over { createdAt desc, id desc } with cursor + clamped limit
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Mechanism: nothing inside `attach`/`url`/`delete` calls `list()`, so a stubbed
|
|
269
|
+
implementation surfaces only later — the dashboard's Collections view renders empty and
|
|
270
|
+
admin tooling sees nothing. `list` is part of the `MediaStore` interface, not optional.
|
|
271
|
+
Source: `docs/stores-and-processors.mdx` (The MediaStore), `docs/dashboard/collections.mdx`.
|
|
272
|
+
|
|
273
|
+
### [MEDIUM] Setting s3 visibility public and expecting an ACL to be applied
|
|
274
|
+
|
|
275
|
+
Wrong:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
disks.s3({ bucket: 'media', visibility: 'public' }) // expecting objects to become readable
|
|
279
|
+
// → 403s from the bucket: no bucket policy grants anonymous reads
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Correct:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
// make the bucket readable via its policy/CDN, THEN declare it:
|
|
286
|
+
disks.s3({ bucket: 'media', visibility: 'public', publicBaseUrl: 'https://cdn.example.com' })
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Mechanism: `visibility` is declarative metadata read by `delivery.mode: 'auto'` — the disk
|
|
290
|
+
never applies an ACL and never probes the bucket, so the declaration must match reality the
|
|
291
|
+
infrastructure already enforces.
|
|
292
|
+
Source: `docs/storage/s3-disk.mdx` (visibility option, getVisibility).
|
|
293
|
+
|
|
294
|
+
## References
|
|
295
|
+
|
|
296
|
+
- Testing doubles for all three seams: use `@adonis-agora/media/testing` —
|
|
297
|
+
`InMemoryMediaStore`, `inMemoryDiskResolver` (with a `DiskVisibility` argument to test
|
|
298
|
+
`auto` delivery's unknown-visibility branch), `FakeImageProcessor`, `InMemoryUploadSessionStore`,
|
|
299
|
+
`FakeTransformer`. See `docs/testing.mdx`.
|
|
300
|
+
|
|
301
|
+
See also: `media-dashboard-console/SKILL.md` — the console browses through these same
|
|
302
|
+
ExtendedDisk operations and `MediaStore.list`.
|