Skip to content

文件上传

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

稍后,在控制器中,你可以访问文件、验证它们并将它们移动到持久位置或 S3 等云存储服务。

访问用户上传的文件

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

ts
import { HttpContext } from '@adonisjs/core/http'

export default class UserAvatarsController {
  update({ request }: HttpContext) {
    // highlight-start
    const avatar = request.file('avatar')
    console.log(avatar)
    // highlight-end
  }
}

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

ts
import { HttpContext } from '@adonisjs/core/http'

export default class InvoicesController {
  update({ request }: HttpContext) {
    // highlight-start
    const invoiceDocuments = request.files('documents')
    
    for (let document of invoiceDocuments) {
      console.log(document)
    }
    // highlight-end
  }
}

手动验证文件

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

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

ts
const avatar = request.file('avatar', {
  size: '2mb',
  extnames: ['jpg', 'png', 'jpeg']
})

if (!avatar.isValid) {
  return response.badRequest({
    errors: avatar.errors
  })
}

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

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

ts
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,
    })
  })
}

使用验证器验证文件

你可以使用验证器作为验证管道的一部分验证文件,而不是手动验证文件(如上一节所示)。使用验证器时,你不必手动检查错误;验证管道会处理这些。

ts
// app/validators/user_validator.ts
import vine from '@vinejs/vine'

export const updateAvatarValidator = vine.compile(
  vine.object({
    // highlight-start
    avatar: vine.file({
      size: '2mb',
      extnames: ['jpg', 'png', 'pdf']
    })
    // highlight-end
  })
)
ts
import { HttpContext } from '@adonisjs/core/http'
import { updateAvatarValidator } from '#validators/user_validator'

export default class UserAvatarsController {
  async update({ request }: HttpContext) {
    // highlight-start
    const { avatar } = await request.validateUsing(
      updateAvatarValidator
    )
    // highlight-end
  }
}

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

ts
import vine from '@vinejs/vine'

export const createInvoiceValidator = vine.compile(
  vine.object({
    // highlight-start
    documents: vine.array(
      vine.file({
        size: '2mb',
        extnames: ['jpg', 'png', 'pdf']
      })
    )
    // highlight-end
  })
)

将文件移动到持久位置

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

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

ts
import app from '@adonisjs/core/services/app'

const avatar = request.file('avatar', {
  size: '2mb',
  extnames: ['jpg', 'png', 'jpeg']
})

// highlight-start
/**
 * 将头像移动到 "storage/uploads" 目录
 */
await avatar.move(app.makePath('storage/uploads'))
// highlight-end

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

ts
// highlight-start
import { cuid } from '@adonisjs/core/helpers'
// highlight-end
import app from '@adonisjs/core/services/app'

await avatar.move(app.makePath('storage/uploads'), {
  // highlight-start
  name: `${cuid()}.${avatar.extname}`
  // highlight-end
})

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

ts
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 方法来提供文件服务。

ts
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('格式错误的路径')
  }

  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 文件并更改以下选项之一以禁用自动处理。

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

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

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

ts
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 也允许你验证流部分(即文件)。如果发生错误,error 事件将在 part 对象上触发。

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

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