上传文件:直传与 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;相同内容可按哈希复用已有对象。
接口接受 File 或 Blob。传入无文件名的 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。
