# 表单与文件上传

URL: https://caijiao.org/hono/06-practice/02-form-upload
Source: docs/hono/06-practice/02-form-upload.md
Description: 学习 Hono 中读取 multipart 表单、普通表单字段和上传文件，并了解文件大小与存储安全注意事项。

Hono 可以通过 Web 标准的 `formData()` 读取表单字段和上传文件。不同运行时对文件大小、临时存储和流式处理能力可能不同，生产环境要结合平台限制设计。

## 读取普通表单

```ts
app.post('/contact', async (c) => {
  const form = await c.req.formData()
  const email = String(form.get('email') ?? '')
  const message = String(form.get('message') ?? '')

  if (!email || !message) {
    return c.json({ message: 'email and message are required' }, 400)
  }

  return c.json({
    received: true,
    email,
  })
})
```

HTML 表单：

```html
<form action="/contact" method="post">
  <input name="email" type="email" />
  <textarea name="message"></textarea>
  <button type="submit">提交</button>
</form>
```

## 读取文件

```ts
app.post('/upload', async (c) => {
  const form = await c.req.formData()
  const file = form.get('file')

  if (!(file instanceof File)) {
    return c.json({ message: 'file is required' }, 400)
  }

  return c.json({
    name: file.name,
    type: file.type,
    size: file.size,
  })
})
```

测试：

```bash
curl -X POST http://localhost:3000/upload \
  -F "file=@./avatar.png"
```

## 保存文件

Node.js 中可以把文件转换为 `ArrayBuffer` 后写入磁盘：

```ts
import { writeFile } from 'node:fs/promises'
import { join } from 'node:path'

app.post('/upload', async (c) => {
  const form = await c.req.formData()
  const file = form.get('file')

  if (!(file instanceof File)) {
    return c.json({ message: 'file is required' }, 400)
  }

  const bytes = await file.arrayBuffer()
  const filename = `${crypto.randomUUID()}-${file.name}`
  const path = join(process.cwd(), 'uploads', filename)

  await writeFile(path, Buffer.from(bytes))

  return c.json({ filename })
})
```

Cloudflare Workers 中更常见的是写入 R2，而不是本地文件系统。

## 安全注意事项

- 限制上传大小，避免内存被大文件耗尽。
- 不要信任客户端传来的文件名。
- 检查 MIME 类型和文件内容，特别是图片、压缩包和可执行文件。
- 私有文件不要直接放在公开静态目录。
- 对上传结果做鉴权，避免任意用户读取其他用户文件。

如果文件上传是核心功能，建议使用对象存储，并将上传、扫描、缩略图、访问控制拆成清晰流程。
