一句话:每个 LingCode Cloud 后端都自带文件存储,底层是 S3 兼容的对象存储(可选配 CDN)。一共两个存储桶:public(由 CDN 分发,全世界可读)和 private(按用户隔离,需要已登录用户的令牌)。你用 lingcode.storage.from('public').upload(path, file) 上传,SDK 会自动把小文件走内联、把大文件通过预签名直传(presigned PUT)直接送到对象存储——所以 GB 级的视频和音频开箱即用。
大多数应用都需要一个放文件的地方:头像、照片上传、生成的 PDF、音频片段。存储就是后端里保管这些字节的那部分。设计上的问题几乎总是同一个——这个文件是全世界凭 URL 就能看到的,还是只属于某一个用户的?LingCode 用两个存储桶回答这个问题,本指南余下的内容就是教你选对那一个,并且上传时根本不用操心文件大小。
文件的可见性是你把它放在哪儿的属性,而不是你事后逐次请求才决定的事。这让规则保持简单,也不可能在不知不觉中弄错:
public——经 CDN 分发,全世界可读。任何拿到 URL 的人都能取到这个文件。它用于你打算凭 URL 嵌入的资源:头像、公开图片、营销素材,凡是你乐意丢进 <img> 标签的东西。公开对象有一个稳定 URL,你可以用 getPublicUrl(path) 同步算出来。private——按用户隔离。读取私有对象需要已登录用户的令牌;没有公开 URL。它用于按用户隔离的文档——上传的合同、私人照片,凡是绝不能靠猜 URL 泄露的东西。用用户令牌加上所有权来守门读取,方式和你为私有数据库行守门时一模一样。你通过文件在存储桶内的路径来寻址——例如 public 存储桶里的 avatars/me.png。对象键在服务端按后端、按存储桶各自加了命名空间,任何路径穿越(. / ..)都会被剥除,所以你够不到另一个后端的文件。
如果你会凭 URL 嵌入它、也不介意任何人取到它,就用 public 并渲染 getPublicUrl(path)。如果它属于某一个用户、读取必须经过认证,就用 private,并在指向它的那些行上配上 RLS 风格的所有权检查。拿不准时,默认选 private——你随时可以再复制一份到公开桶。
调用 lingcode.storage.from(bucket) 拿到一个存储桶句柄,其中 bucket 是 'public' 或 'private'。每个方法都返回和 SDK 其余部分相同的 { data, error } Result 结构(getPublicUrl 除外——它是同步的,返回一个普通字符串)。
const bucket = lingcode.storage.from('public');
// upload(path, file, { contentType? }) -> { data: { bucket, path, bytes, url }, error }
// file 可以是 Blob | File | ArrayBuffer | string
await bucket.upload('avatars/me.png', file, { contentType: 'image/png' });
// download(path) -> { data: Blob, error }
const { data: blob } = await bucket.download('avatars/me.png');
// getPublicUrl(path) -> string (同步;只对 public 存储桶有意义)
const url = bucket.getPublicUrl('avatars/me.png');
// remove(path) -> { data: { removed: boolean }, error }
await bucket.remove('avatars/me.png');
你永远只调用一个方法——upload()——SDK 会根据大小决定怎么搬这些字节。这是关于存储最重要的一点,因为正是它让同一行调用既能处理 40 KB 的头像,也能处理 2 GB 的屏幕录制。
SDK 把这个分流完全藏了起来。如果你自己调 REST,大文件这条路是刻意的两步走:create-upload-url → 向返回的 URL 发 PUT → finalize(下文有详述)。对超大媒体,只要调 upload(),让 SDK 自动走预签名那条路。
把一个 2 GB 文件推过你的应用服务器,意味着那台服务器得缓冲并流转 2 GB——又慢、又吃内存、还容易超时。预签名 PUT 把服务器整个绕开:浏览器通过一个只对你那个特定对象、且只在短窗口内有效的 URL,直接和对象存储对话。你不花一分气力就拿到 GB 级上传,网关连字节都碰不到。
存储在四个维度上计量,上限取决于你的套餐档位。超过任一上限会返回 HTTP 402 quota_exceeded。后端在你存储配额的 80% 处发出警告,在 95% 处标记为严重,所以你会在撞墙之前就看到压力。
| 限额 | 它限制的对象 | Free | Pro | Max Pro |
|---|---|---|---|---|
maxObjectBytes |
内联上传,单个对象 | 1 MB | 5 MB | 10 MB |
maxUploadBytes |
预签名直传 PUT,单个对象 | 50 MB | 1 GB | 5 GB |
maxStorageBytes |
每个后端的总存储字节数 | 500 MB | 5 GB | 20 GB |
maxObjects |
每个后端的对象数量 | 50 | 1,000 | 10,000 |
单次 PUT 的上限是 5 GB,与档位无关——这是一次预签名直传 PUT 能写入的最大对象。完整数字以及其余套餐限额都在定价页上。
upload() 返回 error.code === 'quota_exceeded',说明你撞上了四个上限之一——对象太多、总字节太多,或者单个对象超过了你档位的每对象限额。把它呈现给用户(或者用 remove() 清掉旧对象),而不是一味重试。
头像是 public 的教科书式场景——你想要一个能塞进图片标签的稳定 URL。上传,然后同步读回这个公开 URL(或者干脆用上传返回的那个 url)。
const file = document.querySelector('#avatar-input').files[0];
const { data, error } = await lingcode.storage
.from('public')
.upload(`avatars/${user.id}.png`, file, { contentType: file.type });
if (error) {
console.error('upload failed', error);
} else {
// data = { bucket, path, bytes, url }
document.querySelector('#avatar').src = data.url;
}
// 或者随后任意时刻算出这个稳定 URL,无需重新上传:
const url = lingcode.storage.from('public').getPublicUrl(`avatars/${user.id}.png`);
用户的私有文件放进 private 存储桶。没有公开 URL——读取需要已登录用户的令牌,所以确保你已登录(见认证指南)。用 download() 把它取回来,它返回一个 Blob。
// 上传——按已登录用户隔离
const { error } = await lingcode.storage
.from('private')
.upload(`docs/${user.id}/contract.pdf`, pdfFile, {
contentType: 'application/pdf',
});
// 随后读回来(需要用户令牌)
const { data: blob, error: dlErr } = await lingcode.storage
.from('private')
.download(`docs/${user.id}/contract.pdf`);
if (!dlErr) {
const objectUrl = URL.createObjectURL(blob);
window.open(objectUrl); // 在新标签页里打开 PDF
}
把 path 存到一个用户拥有的数据库行上,让 RLS 来守门谁能看到那一行——这就是你把「谁能读这个文件」变成你已经在数据上用的同一套所有权检查的方式。
对大文件你不用做任何特殊处理。SDK 看到大小后会自行切换到预签名那条路。
const video = document.querySelector('#video-input').files[0]; // 例如 1.4 GB
// 同一个调用。SDK 通过预签名 PUT 把它直传到对象存储。
const { data, error } = await lingcode.storage
.from('private')
.upload(`recordings/${user.id}/${Date.now()}.mp4`, video, {
contentType: 'video/mp4',
});
const { data, error } = await lingcode.storage
.from('public')
.remove(`avatars/${user.id}.png`);
// data = { removed: true }
SDK 只是一层薄薄的包装,底下是一小撮 HTTP 端点。只有当你在 JavaScript 之外工作,或想自己掌控大文件上传那几步时,才会用到它们。基址是 https://lingcode.dev/api/cloud/be/<backend-id>。用 anon key(匿名密钥,作为 Bearer 令牌或 ?apikey=)或用户令牌认证;bucket 省略时取默认值。对象键按后端、按存储桶加了命名空间,./.. 路径穿越会被剥除。
POST /storage/upload
Authorization: Bearer <anon-or-user-token>
Content-Type: application/json
{ "bucket": "public", "path": "avatars/me.png",
"content_type": "image/png", "data_b64": "<base64-bytes>" }
-> { "bucket": "public", "path": "avatars/me.png", "bytes": 40213, "url": "https://..." }
GET /storage/object?bucket=public&path=avatars/me.png
Authorization: Bearer <anon-or-user-token>
-> 文件字节(对公开对象会 302 重定向到 CDN URL)
POST /storage/remove
{ "bucket": "public", "path": "avatars/me.png" }
-> { "removed": true }
这就是 SDK 对超过内联上限的文件自动做的事。第一步,要一个预签名上传 URL:
POST /storage/create-upload-url
{ "bucket": "private", "path": "recordings/big.mp4",
"content_type": "video/mp4" }
-> { "uploadUrl": "https://...", "method": "PUT",
"headers": { ... }, "bucket": "private", "path": "recordings/big.mp4" }
第二步,用返回的 method 和 headers 把字节直接 PUT 到 uploadUrl——这些字节去往对象存储,而非你的应用服务器:
PUT <uploadUrl>
<headers from the previous response>
<raw file bytes>
第三步,做 finalize 登记这个对象,让它计入你的后端并变得可读:
POST /storage/finalize
{ "bucket": "private", "path": "recordings/big.mp4" }
-> { "bucket": "private", "path": "recordings/big.mp4", "bytes": 1490233856, "url": "https://..." }
如果你的对象小于你档位的内联每对象上限(maxObjectBytes——1 / 5 / 10 MB),一个 POST /storage/upload 就够了。再大一点就走三步走的预签名流程,受 maxUploadBytes(50 MB / 1 GB / 5 GB)和 5 GB 单次 PUT 上限的约束。SDK 的 upload() 替你挑选;只有你自己直接调 REST 时才需要亲自摆弄这几步。