Drive

Drive

AdonisJS Drive(@adonisjs/drive)是一个构建于 flydrive.dev 之上的轻量封装。FlyDrive 是一个用于 Node.js 的文件存储库。它提供了一个统一的 API,用于与本地文件系统以及 S3、R2、GCS 等云存储解决方案交互。

使用 FlyDrive,你可以在各种云存储服务(包括本地文件系统)上管理用户上传的文件,而无需更改一行代码。

安装

使用以下命令安装并配置 @adonisjs/drive 包:

node ace add @adonisjs/drive
  1. 使用检测到的包管理器安装 @adonisjs/drive 包。

  2. adonisrc.ts 文件中注册以下服务提供者。

    {
    providers: [
    // ...other providers
    () => import('@adonisjs/drive/drive_provider'),
    ]
    }
  3. 创建 config/drive.ts 文件。

  4. 为所选的存储服务定义环境变量。

  5. 为所选的存储服务安装所需的对等依赖(peer dependencies)。

配置

@adonisjs/drive 包的配置存储在 config/drive.ts 文件中。你可以在单个配置文件中为多个服务定义配置。

另见:配置 stub

import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { defineConfig, services } from '@adonisjs/drive'
const driveConfig = defineConfig({
default: env.get('DRIVE_DISK'),
services: {
/**
* Persist files on the local filesystem
*/
fs: services.fs({
location: app.makePath('storage'),
serveFiles: true,
routeBasePath: '/uploads',
visibility: 'public',
}),
/**
* Persist files on Digital Ocean spaces
*/
spaces: services.s3({
credentials: {
accessKeyId: env.get('SPACES_KEY'),
secretAccessKey: env.get('SPACES_SECRET'),
},
region: env.get('SPACES_REGION'),
bucket: env.get('SPACES_BUCKET'),
endpoint: env.get('SPACES_ENDPOINT'),
visibility: 'public',
}),
},
})
export default driveConfig

环境变量

存储服务的凭据/设置以环境变量的形式存储在 .env 文件中。在使用 Drive 之前,请务必更新这些值。

此外,DRIVE_DISK 环境变量定义了用于管理文件的默认磁盘/服务。例如,你可能希望在开发环境中使用 fs 磁盘,在生产环境中使用 spaces 磁盘。

用法

配置好 Drive 后,你可以导入 drive 服务来与其 API 交互。在以下示例中,我们使用 Drive 处理一次文件上传操作。

由于 AdonisJS 集成只是 FlyDrive 之上的一层薄封装,你应该阅读 FlyDrive 文档以更好地理解其 API。

import { cuid } from '@adonisjs/core/helpers'
import drive from '@adonisjs/drive/services/main'
import router from '@adonisjs/core/services/router'
router.put('/me', async ({ request, response }) => {
/**
* Step 1: Grab the image from the request and perform basic
* validations
*/
const image = request.file('avatar', {
size: '2mb',
extnames: ['jpeg', 'jpg', 'png'],
})
if (!image) {
return response.badRequest({ error: 'Image missing' })
}
/**
* Step 2: Move the image with a unique name using Drive
*/
const key = `uploads/${cuid()}.${image.extname}`
await image.moveToDisk(key)
/**
* Respond with the file's public URL
*/
return {
message: 'Image uploaded',
url: image.meta.url,
}
})
  • Drive 包为 MultipartFile 添加了 moveToDisk 方法。此方法将文件从其 tmpPath 移动到已配置的存储提供者。

  • 移动文件后,文件对象上会设置 meta.url 属性。此属性包含文件的公开 URL。如果你的文件是私有的,那么你必须使用 drive.use().getSignedUrl() 方法。

Drive 服务

@adonisjs/drive/services/main 路径导出的 Drive 服务是使用从 config/drive.ts 文件导出的配置创建的 DriveManager 类的单例实例。

你可以导入此服务,以与 DriveManager 和已配置的文件存储服务交互。例如:

import drive from '@adonisjs/drive/services/main'
drive instanceof DriveManager // true
/**
* Returns instance of the default disk
*/
const disk = drive.use()
/**
* Returns instance of a disk named r2
*/
const disk = drive.use('r2')
/**
* Returns instance of a disk named spaces
*/
const disk = drive.use('spaces')

一旦你获得了某个 Disk 的实例,就可以用它来管理文件。

另见:Disk API

await disk.put(key, value)
await disk.putStream(key, readableStream)
await disk.get(key)
await disk.getStream(key)
await disk.getArrayBuffer(key)
await disk.delete(key)
await disk.deleteAll(prefix)
await disk.copy(source, destination)
await disk.move(source, destination)
await disk.copyFromFs(source, destination)
await disk.moveFromFs(source, destination)

本地文件系统驱动

AdonisJS 集成增强了 FlyDrive 的本地文件系统驱动,并添加了对 URL 生成以及使用 AdonisJS HTTP 服务器提供文件的支持。

以下是你可以用来配置文件系统驱动的选项列表。

{
services: {
fs: services.fs({
location: app.makePath('storage'),
visibility: 'public',
appUrl: env.get('APP_URL'),
serveFiles: true,
routeBasePath: '/uploads',
}),
}
}

location

location 属性定义了文件应存储在其中的 store。该目录应添加到 .gitignore 中,这样你就不会将开发期间上传的文件推送到生产服务器。

visibility

visibility 属性用于将文件标记为公开或私有。私有文件只能使用签名 URL 访问。了解更多

serveFiles

serveFiles 选项会自动注册一个路由,用于从本地文件系统提供文件。你可以使用 list:routes ace 命令查看此路由。

routeBasePath

routeBasePath 选项定义了用于提供文件的路由的基础前缀。请确保该基础前缀是唯一的。

appUrl

你可以选择性地定义 appUrl 属性,以使用应用的完整域名创建 URL。否则将创建相对 URL。

Edge 辅助工具

在 Edge 模板中,你可以使用以下辅助方法之一来生成 URL。这两个方法都是异步的,因此请务必 await 它们。

<img src="{{ await driveUrl(user.avatar) }}" />
<!-- Generate URL for a named disk -->
<img src="{{ await driveUrl(user.avatar, 's3') }}" />
<img src="{{ await driveUrl(user.avatar, 'r2') }}" />
<a href="{{ await driveSignedUrl(invoice.key) }}">
Download Invoice
</a>
<!-- Generate URL for a named disk -->
<a href="{{ await driveSignedUrl(invoice.key, 's3') }}">
Download Invoice
</a>
<!-- Generate URL with signed options -->
<a href="{{ await driveSignedUrl(invoice.key, {
expiresIn: '30 mins',
}) }}">
Download Invoice
</a>

MultipartFile 辅助方法

Drive 扩展了 Bodyparser 的 MultipartFile 类,并添加了 moveToDisk 方法。此方法将文件从其 tmpPath 复制到已配置的存储提供者。

const image = request.file('image')!
const key = 'user-1-avatar.png'
/**
* Move file to the default disk
*/
await image.moveToDisk(key)
/**
* Move file to a named disk
*/
await image.moveToDisk(key, 's3')
/**
* Define additional properties during the
* move operation
*/
await image.moveToDisk(key, 's3', {
contentType: 'image/png',
})
/**
* Write file by first reading it as a buffer. You may use this
* option when your cloud storage provider results in broken
* files with the "stream" option
*/
await image.moveToDisk(key, 's3', {
moveAs: 'buffer'
})

在测试期间伪造磁盘

Drive 的 fakes API 可在测试期间使用,以防止与远程存储交互。在 fakes 模式下,drive.use() 方法将返回一个伪造磁盘(由本地文件系统支持),所有文件都将被写入你应用根目录下的 ./tmp/drive-fakes 目录中。

在你使用 drive.restore 方法恢复伪造后,这些文件会被自动删除。

另见:FlyDrive fakes 文档

tests/functional/users/update.spec.ts
import { test } from '@japa/runner'
import drive from '@adonisjs/drive/services/main'
import fileGenerator from '@poppinss/file-generator'
test.group('Users | update', () => {
test('should be able to update my avatar', async ({ client, cleanup }) => {
/**
* Fake the "spaces" disk and restore the fake
* after the test finishes
*/
const fakeDisk = drive.fake('spaces')
cleanup(() => drive.restore('spaces'))
/**
* Create user to perform the login and update
*/
const user = await UserFactory.create()
/**
* Generate a fake in-memory png file with size of
* 1mb
*/
const { contents, mime, name } = await fileGenerator.generatePng('1mb')
/**
* Make put request and send the file
*/
await client
.put('me')
.file('avatar', contents, {
filename: name,
contentType: mime,
})
.loginAs(user)
/**
* Assert the file exists
*/
fakeDisk.assertExists(user.avatar)
})
})