后端上传文件:直传与 Func 内生成

上传文件:直传与 Func 内生成

站点里的两条上传路径怎么选:浏览器选择的文件走 CDN 签名直传,Func 内生成的字节走 ctx.assets.upload(单次 20 MiB)。含为什么 base64 中转必然失败,以及文件在 JSON 表里该怎么存。

站点里的文件有两条上传路径,选错会直接撞上体积限制:用户在浏览器里选的文件走 CDN 签名直传Func 里生成的字节走 ctx.assets.upload。两条路都返回一个可直接使用的 URL,表里存的永远是 URL,不是文件内容。

先选对路径

浏览器里的文件 → 签名直传

用户选择或拖入的头像、附件、图片。文件字节由浏览器直接 PUT 到 CDN,不经过 Func,因此不受 Func 的执行超时和返回体积限制。

Func 里生成的文件 → ctx.assets.upload

AI 生成的图片、服务端拼的 PDF、第三方接口取回的字节。这些内容浏览器本来就没有,只能从 Func 传出去,单次上限 20 MiB

把浏览器文件编码成 base64 再 invoke() 给 Func 转发,是这里唯一真正的错误写法:base64 会让体积膨胀约三分之一,同时占满 Func 的入参、执行时间和返回体积三项预算,稍大的文件必然失败。

浏览器文件:CDN 签名直传

用户在网页中选择或拖入的 File/Blob 使用 talizen/assets

import { uploadAsset } from 'talizen/assets'

const asset = await uploadAsset(file, {
  onFileUploadProcess(fileName, progress) {
    console.log(fileName, progress)
  },
})

// asset.fileUrl === asset.url

uploadAsset() 会依次调用 POST /api/asset/file/preupload 获取短期签名地址、由浏览器直接 PUT 文件字节到该 CDN 存储地址,再调用 POST /api/asset/file/ack 确认上传。文件字节不经过 Func;相同内容可按哈希复用已有对象。

接口接受 FileBlob。传入无文件名的 Blob 时使用 uploadAsset(blob, { fileName: 'avatar.webp' })。返回值包含 { fileUrl, url, fileName, mimeType, size, hash },其中两个 URL 字段相同。签名上传同时支持预览域名和已发布站点域名。

上传完成后,把返回的 URL 交给 Func 落库,Func 侧只处理字符串:

import { invoke } from 'talizen/func'

const asset = await uploadAsset(file)
await invoke('profile.setAvatar', { url: asset.url, size: asset.size })

Func 收到的是浏览器给的 URL,所以要当成不可信输入校验:确认它指向平台 CDN 域名,再落库。

Func 内生成的文件

const asset = ctx.assets.upload({
  filename: 'report.pdf',
  mimeType: 'application/pdf',
  base64: input.base64,
})
await ctx.db.insert('reports', {
  userId: ctx.auth.requireUser().id,
  url: asset.url,
  size: asset.size,
})

ctx.assets.upload() 仅用于 Func 内生成、无法从浏览器直接上传的字节。它同步返回 { fileUrl, url, size },其中两个 URL 字段相同。保存 URL 和大小即可;不要保存内部路径,也不要把大段 base64 存入表或作为 Func JSON 结果返回。单次上传上限为 20 MiB

典型场景是把第三方返回的字节转存到自己的 CDN,避免依赖对方的临时链接:

export async function generate(input, ctx) {
  const user = ctx.auth.requireUser()

  const response = await fetch('https://api.example.com/v1/images', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.EXAMPLE_API_KEY },
    body: JSON.stringify({ prompt: input.prompt }),
  })
  if (!response.ok) {
    ctx.response.status(502)
    throw new Error('upstream request failed')
  }

  const { image_base64 } = await response.json()
  const asset = ctx.assets.upload({
    filename: 'generated.png',
    mimeType: 'image/png',
    base64: image_base64,
  })

  ctx.db.insert('generations', { userId: user.id, url: asset.url, prompt: input.prompt })
  return { url: asset.url }
}

这类调用通常远超默认 5 秒超时,调用端需要显式放宽,见超时配置与流式响应

把文件存进 JSON 表

表里存 URL 和元数据,不存文件本身。对应的 schema 字段用字符串加 format: "uri" 描述——表 schema 不支持 "format": "file"

{
  "url": { "type": "string", "format": "uri", "contentMediaType": "image/*" },
  "size": { "type": "number" },
  "fileName": { "type": "string" }
}

字段与查询规则见 JSON 表:定义、读写与查询

验收清单

  • 浏览器选择的文件走 uploadAsset(),没有经过 base64 和 invoke() 中转。
  • ctx.assets.upload() 只用于 Func 内生成的字节,且单次不超过 20 MiB。
  • 表里存的是 URL 和大小,没有 base64、没有内部存储路径。
  • 浏览器传上来的 URL 在落库前校验过来源。
  • 涉及生成类的长耗时调用,调用端设置了匹配的 timeoutMs

Render diagnostics