文档 / 搜索 / LingCode Cloud / 存储
📘 参考 ● 文件 更新于 2026-06-11

存储

一句话:每个 LingCode Cloud 后端都自带文件存储,底层是 S3 兼容的对象存储(可选配 CDN)。一共两个存储桶:public(由 CDN 分发,全世界可读)和 private(按用户隔离,需要已登录用户的令牌)。你用 lingcode.storage.from('public').upload(path, file) 上传,SDK 会自动把小文件走内联、把大文件通过预签名直传(presigned PUT)直接送到对象存储——所以 GB 级的视频和音频开箱即用。

大多数应用都需要一个放文件的地方:头像、照片上传、生成的 PDF、音频片段。存储就是后端里保管这些字节的那部分。设计上的问题几乎总是同一个——这个文件是全世界凭 URL 就能看到的,还是只属于某一个用户的?LingCode 用两个存储桶回答这个问题,本指南余下的内容就是教你选对那一个,并且上传时根本不用操心文件大小。

为什么是两个存储桶

文件的可见性是你把它放在哪儿的属性,而不是你事后逐次请求才决定的事。这让规则保持简单,也不可能在不知不觉中弄错:

你通过文件在存储桶内的路径来寻址——例如 public 存储桶里的 avatars/me.png。对象键在服务端按后端、按存储桶各自加了命名空间,任何路径穿越(. / ..)都会被剥除,所以你够不到另一个后端的文件。

公开 vs. 私有——一行判断法则

如果你会凭 URL 嵌入它、也不介意任何人取到它,就用 public 并渲染 getPublicUrl(path)。如果它属于某一个用户、读取必须经过认证,就用 private,并在指向它的那些行上配上 RLS 风格的所有权检查。拿不准时,默认选 private——你随时可以再复制一份到公开桶。

SDK 接口

调用 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');

上传的故事:小文件 vs. 大文件

你永远只调用一个方法——upload()——SDK 会根据大小决定怎么搬这些字节。这是关于存储最重要的一点,因为正是它让同一行调用既能处理 40 KB 的头像,也能处理 2 GB 的屏幕录制。

SDK 把这个分流完全藏了起来。如果你自己调 REST,大文件这条路是刻意的两步走:create-upload-url → 向返回的 URL 发 PUTfinalize(下文有详述)。对超大媒体,只要调 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 能写入的最大对象。完整数字以及其余套餐限额都在定价页上。

402 是配额信号,不是 bug。如果 upload() 返回 error.code === 'quota_exceeded',说明你撞上了四个上限之一——对象太多、总字节太多,或者单个对象超过了你档位的每对象限额。把它呈现给用户(或者用 remove() 清掉旧对象),而不是一味重试。

实战示例

上传一个头像到 public 并渲染它

头像是 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 来守门谁能看到那一行——这就是你把「谁能读这个文件」变成你已经在数据上用的同一套所有权检查的方式。

上传大媒体——只管调 upload()

对大文件你不用做任何特殊处理。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 }

底层原理(REST)

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" }

第二步,用返回的 methodheaders 把字节直接 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 时才需要亲自摆弄这几步。