@adonis-agora/media

中文文档
0.28.2发布于 2026/11/02MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 1万

Media-library for AdonisJS — owner collections (MIME whitelist, single-file replace, ordering), image conversions (eager/lazy), pluggable content transformers (HLS video via mediabunny, metadata probe, image blur-up placeholder), and column attachments, a

原始使用文档
文档目录13 节
  1. Subpath exports
  2. Install
  3. Configure
  4. Use
  5. Transformers and video streaming
  6. Responsive images ( next/image -style)
  7. Authorization ( media.access )
  8. Uploads, S3 and delivery
  9. Dashboard
  10. Diagnostics
  11. Testing
  12. Links
  13. License

@adonis-agora/media

npm version npm downloads CI license

Media-library for AdonisJS, part of the Agora ecosystem — the spatie/laravel-media-library feel, on top of @adonisjs/drive.

Attach files to entities, organize them into collections (MIME whitelist, single-file replace, ordering), and generate image conversions (eager or lazy) — plus content transformers, the same seam driven beyond images: the bundled transformers.video() packages a video for HLS/DASH streaming and transformers.probe() records duration/codecs as metadata, both persisted into the same record.conversions map. Storage is delegated entirely to Drive, so you reuse your existing local / s3 / gcs disks — this package never reimplements disk drivers.

This is the core library: MediaLibrary, AttachmentManager, the MediaStore SPI (in-memory + Lucid), the ImageProcessor SPI (sharp), the Transformer SPI (HLS video packaging + metadata probe), the bundled disks.s3() driver, proxy/direct/resumable uploads, configurable delivery, the provider + defineConfig, and a testing kit. The management console ships embedded — registering @adonis-agora/media/dashboard_provider is all a consuming app needs (see Dashboard below); no separate install. Pair it with @adonis-agora/media-react for a browser upload client.

Subpath exports

The package uses subpath exports (the Agora idiom), so heavy backends stay optional:

SubpathWhat
@adonis-agora/mediabarrel — defineConfig, stores, processors, disks, uploadSessions, MediaManager, MediaLibrary, AttachmentManager, UploadManager, ResumableUploadManager, MediaDeliveryHandler, SPIs, errors
@adonis-agora/media/services/mainthe app-bound MediaManager singleton — import media from '@adonis-agora/media/services/main', importable at any time (it resolves lazily on first use)
@adonis-agora/media/media_providerthe service provider (binds MediaManager, mounts optional upload/TUS routes)
@adonis-agora/media/dashboard_providerthe embedded management-console provider (React SPA + JSON API)
@adonis-agora/media/dashboarddefineConfig + types for config/media_dashboard.ts, DashboardService, the session-auth helpers, ObjectInsightProvider
@adonis-agora/media/configurenode ace configure hook
@adonis-agora/media/single-filestoreSingleFile / removeSingleFile / storeSingleFileWith / removeSingleFileWith / isSingleFileStoreAvailable — lets other packages delegate single-file uploads (e.g. avatars) to media without a hard dependency
@adonis-agora/media/stores/lucidthe Lucid MediaStore (@adonisjs/lucid peer)
@adonis-agora/media/upload_sessions/lucidthe Lucid resumable UploadSessionStore (@adonisjs/lucid peer)
@adonis-agora/media/disks/s3the bundled S3Disk (@aws-sdk/client-s3 peer; presigning is hand-rolled SigV4)
@adonis-agora/media/processors/sharpthe sharp ImageProcessor (sharp peer)
@adonis-agora/media/telescopemediaTelescopeExtension (@adonis-agora/telescope optional peer)
@adonis-agora/media/testingInMemoryMediaStore, InMemoryDisk, inMemoryDiskResolver, FakeImageProcessor, InMemoryUploadSessionStore
@adonis-agora/media/typesthe structural Disk / DiskResolver / ExtendedDisk / MultipartUploadDisk contracts

Install

node ace add @adonisjs/drive   # storage backend (required)
npm i @adonis-agora/media
node ace configure @adonis-agora/media

configure registers the media provider and the dashboard provider, publishes config/media.ts and config/media_dashboard.ts, and publishes two migrations — the media table for the lucid store and the session table for the lucid upload-session store. Delete the ones you don't need (the in-memory equivalents need no table); otherwise node ace migration:run. The management console is live at /media/dashboard the moment your app boots — see Dashboard.

Required peers: @adonisjs/core (^7.3.0) and @adonisjs/drive — either major, ^3.0.0 or ^4.0.0.

Optional peers, loaded lazily only when selected:

  • @adonisjs/lucid (^22.4.0) — the lucid MediaStore / UploadSessionStore
  • sharp (^0.33.0) — image conversions
  • @aws-sdk/client-s3 (^3.0.0) — the bundled disks.s3() driver (presigned URLs are hand-rolled SigV4, no presigner package)
  • @adonis-agora/telescope (^0.4.0) — the Telescope watcher extension

Configure

// config/media.ts
import { defineConfig, stores, processors } from '@adonis-agora/media'

export default defineConfig({
  // disk: 's3',                 // omit to use Drive's default disk
  store: 'lucid',
  stores: {
    memory: stores.memory(),
    lucid: stores.lucid({ connection: 'pg' }),
  },
  imageProcessor: processors.sharp(),
  collections: [
    { name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
    {
      name: 'gallery',
      conversions: [
        { name: 'thumb', width: 200, height: 200 },
        { name: 'og', width: 1200, eager: true },
      ],
    },
  ],
})

Use

The provider binds a singleton MediaManager; reach it with the service import (or @inject() it). A real upload controller loads the owner, validates the file, and streams its tmp bytes into a collection:

// app/controllers/post_images_controller.ts
import { createReadStream } from 'node:fs'
import Post from '#models/post'
import media from '@adonis-agora/media/services/main'
import type { HttpContext } from '@adonisjs/core/http'

export default class PostImagesController {
  async store({ request, response, params }: HttpContext) {
    const post = await Post.findOrFail(params.id)

    const image = request.file('image', { size: '5mb', extnames: ['jpg', 'png', 'webp'] })
    if (!image) return response.badRequest({ error: 'An "image" file is required' })

    const record = await media.library.attach({
      ownerType: 'Post',
      ownerId: post.id, // string | number — coerced for you
      collection: 'gallery',
      fileName: image.clientName,
      mimeType: image.type!,
      contents: createReadStream(image.tmpPath!), // Buffer | Readable
    })

    return response.created({ id: record.id, url: await media.library.url(record.id) })
  }
}

Reading, listing and deleting go through the same media singleton:

await media.library.url(record.id)            // public url of the original
await media.library.url(record.id, 'thumb')   // generated lazily on first call, then cached
await media.library.list('Post', post.id, 'gallery')
await media.library.delete(record.id)

// Or bind an owner once for a bulk upload
const gallery = media.library.for('Post', post.id)
await gallery.attach({ collection: 'gallery', fileName: image.clientName, mimeType: image.type!, contents })

// Column attachments (adonis-attachment style) — store the value object on a JSON column
const att = await media.attachments.createFromFile(
  { fileName: image.clientName, mimeType: image.type!, contents },
  { variants: [{ name: 'thumb', width: 100 }] },
)
user.avatarData = att.toJSON()
await media.attachments.url(att, 'thumb')

A ConversionPreset is { name, width?, height?, fit?, format?, quality?, eager? }. Eager presets are generated synchronously on attach; the rest are generated lazily on the first url(id, name) and cached on the record. fit and format map straight onto sharp.

Transformers and video streaming

Image conversions answer "same image, different size". Transformers answer everything else: transform this content into that. A transformer takes a stored media's bytes and produces many files (a video package), one file (an extracted audio track), or none at all (a metadata probe) — persisted into the same record.conversions map, so presets and transformers share one namespace per collection.

Two ship with the package, both over the optional mediabunny peer (pure TypeScript — no ffmpeg binary), imported lazily inside the first transform() call:

// config/media.ts
import { defineConfig, transformers } from '@adonis-agora/media'

export default defineConfig({
  collections: [
    {
      name: 'videos',
      acceptsMimeTypes: ['video/mp4'],
      transformers: [
        transformers.video(),                // CMAF segments + an HLS master + a DASH manifest
        transformers.probe({ eager: true }),     // duration / resolution / codecs, no artifact
      ],
    },
  ],
})

Transformations are assumed heavy, so nothing runs on attach unless you mark it eager. Trigger one from your own job — transform(id, name) is idempotent, so a retry skips straight through:

await media.library.transform(mediaId, 'video')

transformers.video() writes CMAF segments by default, which HLS and DASH both play, so one copy of the video gets both manifests. segments: 'ts' writes HLS over MPEG-TS for old players, and engine: engines.ffmpeg({ ladder }) transcodes an ABR ladder with the system ffmpeg. Serve the package with one route that authorizes every manifest and segment request with the collection's access.read:

router.get('/media/:id/stream/:conversion/*', media.access.streamRoute())
const video = await media.access.stream(ctx, mediaId) // manifest URLs for the player

With delivery.signing, the URLs are signed for the whole package, so segment requests carry the authorization along, and the signature outlives the video. Writing your own transformer is one interface (name + transform(context)); the context hands you the record, the original's bytes/stream, and a sandboxed write() that keys every artifact under the conversion's own prefix.

Responsive images (next/image-style)

media.library.image(id) returns everything an <img> needs as plain JSON: src, a srcSet built from the collection's same-picture presets, the intrinsic width/height, the blur-up placeholder and the focalPoint. Render it with <MediaImage> from @adonis-agora/media-react. It is one <img> with lazy loading, priority, fill, a layout that computes sizes, and the blurred preview behind it until it loads.

// config/media.ts — presets at standard widths + the placeholder, from one flag
{ name: 'banner', responsive: true, stripMetadata: true }

// controller
return inertia.render('courses/show', { banner: await media.library.image(course.bannerMediaId) })
<MediaImage image={banner} alt={course.title} layout="constrained" priority />

More on the image side:

  • Focal points: media.library.setFocalPoint(id, { x, y }) makes every crop keep that part, and so does object-fit: cover in the browser. Presets also take position: 'attention'.
  • Art direction: image(id, { conversions: [...] }) returns a group, which <MediaPicture> swaps in on some screens.
  • On-demand resizing: the delivery route serves ?w=640, resized once and cached, as WebP (or AVIF, when you opt in) depending on Accept. Only whitelisted widths are served, unless the URL is signed.
  • Signed URLs: with delivery.signing, media.library.deliveryUrl() and image() hand out signed, expiring URLs, and the route serves nothing else.
  • stripMetadata: true: removes GPS, camera and timestamps from uploaded originals.

Authorization (media.access)

The library still never decides who may see a file, but it applies your rules for you. You declare who may read and upload per collection, tell it who the current user is (authkit, @adonisjs/auth or your own), and mount the ready delivery route:

import { authorizers } from '@adonis-agora/media'
import { authkitIdentity } from '@adonis-agora/media/authkit'
import { authz } from '@adonis-agora/media/authz'

defineConfig({
  access: { identity: authkitIdentity() },
  collections: [
    { name: 'avatar', access: { read: authorizers.owner(), write: authorizers.authenticated() } },
    { name: 'reports', access: { read: authz.can('reports.read') } },
  ],
})

const serveMedia = media.access.deliveryRoute()          // build once, at route definition
router.get('/media/:id', serveMedia)                      // 401/403/404/400 handled
const banner = await media.access.image(ctx, id)                       // authorized descriptor

@adonis-agora/media/authkit and @adonis-agora/media/authz load their packages only when used. Both are optional peers.

Uploads, S3 and delivery

Large-file uploads ship in three flavours — proxy (bytes stream through your app), direct-S3 multipart (the browser uploads straight to the bucket via presigned part URLs), and resumable tus sessions (pluggable in-memory + Lucid session stores). Opt in under uploads in config/media.ts; the provider mounts the routes under /media/uploads and /media/uploads/tus.

export default defineConfig({
  // ...
  uploads: {
    mode: 'auto', // 'direct' | 'proxy' | 'auto'
    routes: { enabled: true },
    resumable: { routes: { enabled: true } },
  },
  delivery: { mode: 'auto' }, // 'public' | 'signed' | 'proxy' | 'auto'
})

delivery is the read-side counterpart to uploads.mode: MediaLibrary.deliver(id, options?) resolves to a discriminated union — { kind: 'redirect', url } for public/signed, or { kind: 'stream', stream, mimeType, size, fileName } for proxy — so a private bucket doesn't need a hand-rolled streaming route. MediaDeliveryHandler and TusUploadHandler are the framework-agnostic route halves your app mounts with its own middleware; neither performs authorization — guard the route before calling handle.

The same delivery route resizes on demand (delivery.handle({ mediaId, width: qs.w, quality: qs.q, accept })). It serves only widths in delivery.images.widths unless the request is signed, and it negotiates AVIF/WebP from Accept. With delivery.signing ({ secret, expiresIn }), it serves only URLs built by media.library.deliveryUrl() or image(), until they expire.

The bundled disks.s3() driver adds extended operations (copy/move/deleteMany/list/size/stat) and native multipart over the optional AWS SDK peer. Drive it from the browser with @adonis-agora/media-react.

Dashboard

A management console — browse buckets, watch resumable uploads in progress, upload objects, and copy/move/delete across buckets — ships embedded in this package: registering @adonis-agora/media/dashboard_provider (done automatically by node ace configure) is all a consuming app needs. No separate package install, no separate provider registration.

// adonisrc.ts
providers: [
  () => import('@adonis-agora/media/media_provider'),
  () => import('@adonis-agora/media/dashboard_provider'),
]
// config/media_dashboard.ts
import { defineConfig } from '@adonis-agora/media/dashboard'
import { middleware } from '#start/kernel'

export default defineConfig({
  basePath: '/media/dashboard',   // where the SPA mounts (default)
  actions: true,                  // enable copy/move/delete (default: false — read-only)
  disks: ['s3', 'backups'],       // browsable disks (default: derived from media config)
  middleware: middleware.auth(),  // gate the whole console — SPA + API
})

The console reads/writes through the real @adonis-agora/media surface (disk list/stat/copy/move/deleteMany, the resumable session store, the MediaStore) — nothing about storage is reimplemented. Full config reference, routes, and folder-operations semantics are documented in @adonis-agora/media-dashboard, which is now the build-time source of the console's SPA bundle (embedded here at build time) and remains installable standalone — its own provider is a thin delegate to this one, kept for hosts that already register it directly. Don't register both providers in the same app.

Diagnostics

All nine lifecycle events — attach, delete, conversion, attachment.create, attachment.delete, upload.start, upload.progress, upload.complete, upload.abort — are emitted on the agora:media:* channel via the @agora/diagnostics:emit global slot — read structurally, with no hard dependency on @adonis-agora/diagnostics. When that package isn't installed, emitting is an inert no-op; when it is, the Telescope generic watcher (or @adonis-agora/media/telescope) captures every media event automatically.

Testing

@adonis-agora/media/testing ships in-memory doubles for the SPIs so you can exercise the library without a disk, database, or sharp:

import { MediaLibrary, StorageManager } from '@adonis-agora/media'
import { InMemoryMediaStore, inMemoryDiskResolver, FakeImageProcessor } from '@adonis-agora/media/testing'

const { resolve, disks } = inMemoryDiskResolver(['fs'])
const library = new MediaLibrary({
  storage: new StorageManager({ default: 'fs', resolve }),
  store: new InMemoryMediaStore(),
  imageProcessor: new FakeImageProcessor(),
})

In a real app you can also drive.fake() from @adonisjs/drive to back the disks with an in-memory fake while keeping the rest of the wiring intact.

License

MIT

版本与发布记录

@adonis-agora/media 共发布 49 个版本,首次发布于 2026/06/24;过去 12 个月发布了 49 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.28.2latest2026/11/023 天前
0.28.12026/10/289 天前
0.28.02026/10/289 天前
0.27.02026/10/2710 天前
0.26.22026/10/2412 天前
0.26.12026/10/2412 天前

关于 @adonis-agora/media 的常见问题

@adonis-agora/media 是什么?

Media-library for AdonisJS — owner collections (MIME whitelist, single-file replace, ordering), image conversions (eager/lazy), pluggable content transformers (HLS video via mediabunny, metadata probe, image blur-up placeholder), and column attachments, a

如何安装 @adonis-agora/media?

在项目目录运行 npm install @adonis-agora/media,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@adonisjs/core(^7.3.0)、@adonisjs/drive(^3.0.0 || ^4.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@adonis-agora/media 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@adonis-agora/media 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@adonis-agora/media 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@adonis-agora/media 可以商用吗?

@adonis-agora/media 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@adonis-agora/media 的下载量有多少?

根据 npm 官方下载统计,@adonis-agora/media 在 2026/10/05 – 2026/11/03 这 30 天内被下载 10,081 次,日均约 336 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@adonis-agora/media 还在维护吗?

最新版本 0.28.2 发布于 2026/11/02(3 天前);至今共发布 49 个版本,其中过去 12 个月发布了 49 个;首次发布于 2026/06/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。