文件上传

文件上传

AdonisJS 对使用 multipart/form-data 内容类型发送的用户上传文件提供一流的支持。文件通过 bodyparser 中间件 自动处理,并保存在操作系统的 tmp 目录中。

之后,在控制器中,你可以访问这些文件,对其进行验证,并将其移动到持久位置或像 S3 这样的云存储服务。

访问用户上传的文件

你可以使用 request.file 方法访问用户上传的文件。该方法接受字段名称并返回 MultipartFile 的实例。

import { HttpContext } from '@adonisjs/core/http'
export default class UserAvatarsController {
update({ request }: HttpContext) {
const avatar = request.file('avatar')
console.log(avatar)
}
}

如果单个输入字段用于上传多个文件,你可以使用 request.files 方法访问它们。该方法接受字段名称并返回 MultipartFile 实例的数组。

import { HttpContext } from '@adonisjs/core/http'
export default class InvoicesController {
update({ request }: HttpContext) {
const invoiceDocuments = request.files('documents')
for (let document of invoiceDocuments) {
console.log(document)
}
}
}

手动验证文件

你可以使用 validator 来验证文件,或通过 request.file 方法定义验证规则。

在以下示例中,我们将通过 request.file 方法内联定义验证规则,并使用 file.errors 属性访问验证错误。

const avatar = request.file('avatar', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
})
if (!avatar.isValid) {
return response.badRequest({
errors: avatar.errors
})
}

当处理文件数组时,你可以遍历文件并检查一个或多个文件是否未通过验证。

提供给 request.files 方法的验证选项适用于所有文件。在以下示例中,我们期望每个文件都小于 2mb,并且必须具有允许的文件扩展名之一。

const invoiceDocuments = request.files('documents', {
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
/**
* 创建无效文件集合
*/
let invalidDocuments = invoiceDocuments.filter((document) => {
return !document.isValid
})
if (invalidDocuments.length) {
/**
* 返回文件名及其对应的错误
*/
return response.badRequest({
errors: invalidDocuments.map((document) => {
name: document.clientName,
errors: document.errors,
})
})
}

使用验证器验证文件

无需手动验证文件(如在上一节中所见),你可以使用validator ,将文件验证作为验证流程的一部分。使用验证器时,无需手动检查错误;验证流程会自动处理这一部分。

// app/validators/user_validator.ts
import vine from '@vinejs/vine'
export const updateAvatarValidator = vine.compile(
vine.object({
avatar: vine.file({
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
})
)
import { HttpContext } from '@adonisjs/core/http'
import { updateAvatarValidator } from '#validators/user_validator'
export default class UserAvatarsController {
async update({ request }: HttpContext) {
const { avatar } = await request.validateUsing(
updateAvatarValidator
)
}
}

可以使用 vine.array 类型验证文件数组。例如:

import vine from '@vinejs/vine'
export const createInvoiceValidator = vine.compile(
vine.object({
documents: vine.array(
vine.file({
size: '2mb',
extnames: ['jpg', 'png', 'pdf']
})
)
})
)

将文件移动到持久位置

默认情况下,用户上传的文件保存在操作系统的 tmp 目录中,并且可能会在计算机清理 tmp 目录时被删除。

因此,建议将文件存储在一个持久化的位置。你可以使用 file.move 在同一文件系统中移动文件。该方法接受一个用于移动文件的目标目录的绝对路径。

import app from '@adonisjs/core/services/app'
const avatar = request.file('avatar', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
})
/**
* 将头像移动到 "storage/uploads" 目录
*/
await avatar.move(app.makePath('storage/uploads'))

建议为移动的文件提供一个唯一的随机名称。为此,你可以使用 cuid 辅助工具。

import { cuid } from '@adonisjs/core/helpers'
import app from '@adonisjs/core/services/app'
await avatar.move(app.makePath('storage/uploads'), {
name: `${cuid()}.${avatar.extname}`
})

文件移动后,你可以将其名称存储在数据库中以便后续引用。

await avatar.move(app.makePath('uploads'))
/**
* 示例代码,将文件名作为头像保存在
* 用户模型中,并将其持久化到数据库。
*/
auth.user!.avatar = avatar.fileName!
await auth.user.save()

文件属性

以下是你可以在 MultipartFile 实例上访问的属性列表。

属性描述
fieldNameHTML 输入字段的名称。
clientName用户计算机上的文件名。
size文件的大小(以字节为单位)。
extname文件的扩展名。
errors与给定文件关联的错误数组。
type文件的 MIME 类型
subtype文件的 MIME 子类型
filePathmove 操作后文件的绝对路径。
fileNamemove 操作后的文件名。
tmpPathtmp 目录中文件的绝对路径。
meta与文件关联的元数据,作为键值对。默认情况下,该对象为空。
validated布尔值,指示文件是否已验证。
isValid布尔值,指示文件是否通过了验证规则。
hasErrors布尔值,指示一个或多个错误是否与给定文件关联。

提供文件服务

如果你将用户上传的文件与应用程序代码保存在同一文件系统中,你可以通过创建路由并使用 response.download 方法来提供文件服务。

import { sep, normalize } from 'node:path'
import app from '@adonisjs/core/services/app'
import router from '@adonisjs/core/services/router'
const PATH_TRAVERSAL_REGEX = /(?:^|[\\/])\.\.(?:[\\/]|$)/
router.get('/uploads/*', ({ request, response }) => {
const filePath = request.param('*').join(sep)
const normalizedPath = normalize(filePath)
if (PATH_TRAVERSAL_REGEX.test(normalizedPath)) {
return response.badRequest('Malformed path')
}
const absolutePath = app.makePath('uploads', normalizedPath)
return response.download(absolutePath)
})
  • 我们使用通配符路由参数获取文件路径,并将数组转换为字符串。
  • 接下来,我们使用 Node.js 的 path 模块对路径进行规范化。
  • 使用 PATH_TRAVERSAL_REGEX 保护此路由免受目录穿越攻击。
  • 最后,我们将 normalizedPath 转换为 uploads 目录中的绝对路径,并使用 response.download 方法提供文件服务。

使用 Drive 上传和提供文件服务

Drive 是 AdonisJS 核心团队创建的文件系统抽象。你可以使用 Drive 管理用户上传的文件,并将它们存储在本地文件系统中,或将其移动到像 S3 或 GCS 这样的云存储服务。

我们建议使用 Drive 而不是手动上传和提供文件服务。Drive 处理了许多安全问题,如路径穿越,并为多个存储提供商提供了统一的 API。

了解更多关于 Drive 的信息

高级用法 - 自处理多部分流

你可以关闭多部分请求的自动处理,并在高级用例中自行处理流。打开 config/bodyparser.ts 文件,并更改以下选项之一以禁用自动处理。

{
multipart: {
/**
* 如果希望对所有 HTTP 请求手动自处理多部分流,
* 请将此设置为 false。
*/
autoProcess: false
}
}
{
multipart: {
/**
* 定义要手动处理多部分流的路由模式数组。
*/
processManually: ['/assets']
}
}

禁用自动处理后,你可以使用 request.multipart 对象处理单个文件。

在下面的示例中,我们使用 Node.js 的 stream.pipeline 方法处理多部分可读流,并将其写入磁盘上的文件。但是,你也可以将此文件流式传输到一些外部服务,如 s3

import { createWriteStream } from 'node:fs'
import app from '@adonisjs/core/services/app'
import { pipeline } from 'node:stream/promises'
import { HttpContext } from '@adonisjs/core/http'
export default class AssetsController {
async store({ request }: HttpContext) {
/**
* 步骤 1:定义文件监听器
*/
request.multipart.onFile('*', {}, async (part, reporter) => {
part.pause()
part.on('data', reporter)
const filePath = app.makePath(part.file.clientName)
await pipeline(part, createWriteStream(filePath))
return { filePath }
})
/**
* 步骤 2:处理流
*/
await request.multipart.process()
/**
* 步骤 3:访问处理后的文件
*/
return request.allFiles()
}
}
  • multipart.onFile 方法接受你希望处理的文件的输入字段名称。你可以使用通配符 * 处理所有文件。

  • onFile 监听器接收 part(可读流)作为第一个参数,接收 reporter 函数作为第二个参数。

  • reporter 函数用于跟踪流的处理进度,以便 AdonisJS 可以在流处理完成后,为你提供对处理后的字节、文件扩展名和其他元数据的访问。

  • 最后,你可以从 onFile 监听器返回一个属性对象,这些属性将与你使用 request.filerequest.allFiles() 方法访问的文件对象合并。

错误处理

你必须监听 part 对象上的 error 事件,并手动处理错误。通常,流读取器(可写流)会在内部监听此事件,并中止写入操作。

验证流部分

即使你手动处理多部分流,AdonisJS 也允许你验证流部分(即文件)。如果发生错误,将在 part 对象上发出 error 事件。

multipart.onFile 方法接受验证选项作为第二个参数。此外,请确保监听 data 事件,并将 reporter 方法绑定到它。否则,不会进行任何验证。

request.multipart.onFile('*', {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg']
}, async (part, reporter) => {
/**
* 以下两行是执行流验证所必需的
*/
part.pause()
part.on('data', reporter)
})