常见问题
与传统模式云存储有哪些差异?
| 维度 | 传统模式云存储 | PG 模式云存储 |
|---|---|---|
| Bucket 概念 | 与环境强绑定,主要做路径前缀,不可按桶独立配权限 | storage.buckets 表的一行,可按桶配 public / file_size_limit / allowed_mime_types、并在 RLS 中以 bucket_id 做条件 |
| 权限描述方式 | 内置权限模式(READONLY / PRIVATE / ADMINWRITE / ADMINONLY / CUSTOM) + JSON 安全规则 | RLS Policy(SQL) |
| 权限粒度 | 桶级 / 路径前缀 / 简单角色 | 任意 SQL 表达式,可跨表 JOIN |
| 上传链路 | 客户端 SDK 拿临时签名后直传 COS | 客户端 → Storage API → COS,事务内协调元数据与对象 |
owner_id / 元数据 | 业务侧补齐,存在伪造与不一致风险 | Storage API 写入时自动用 JWT sub 注入,前端无法伪造;size / mimetype 等元信息直接由 SQL 读取 |
| 元数据访问 | 控制台 / SDK | 直接 SQL 查询 storage.objects,可与业务表 JOIN |
| 与业务数据联动 | 业务自行同步 | 同实例同 schema,可直接 JOIN |
| 选择方式 | 创建 Bucket 时挑选权限模式 | 由环境类型自动决定 |
| 触发器 / 自定义函数 | 不支持 | 支持(PL/pgSQL、PL/V8 等) |
| 计费 | 与传统形态一致 | 与传统形态一致 |
SDK 调用接口(
uploadFile/deleteFile/getTempFileURL等)与传统模式保持一致,业务代码无需感知链路差异。
参见 PG 模式概述。
storage.buckets 的 public 字段是什么含义?
public boolean DEFAULT false 只是元数据上的一个布尔标记,不会让 PostgreSQL 自动旁路 RLS。是否真的"公开可读"取决于你写在 storage.objects 上的 SELECT 策略。
如果想让 public = true 的桶自动允许匿名读:
CREATE POLICY objects_public_read ON storage.objects
FOR SELECT TO anon, authenticated
USING (
EXISTS (
SELECT 1 FROM storage.buckets b
WHERE b.id = storage.objects.bucket_id AND b.public
)
);
为什么 DELETE FROM storage.objects 直接报错?
storage.objects 与 storage.buckets 上有 protect_delete 触发器:
ERROR: Direct deletion from storage tables is not allowed.
Use the Storage API instead.
HINT: This prevents accidental data loss from orphaned objects.
SQLSTATE: 42501
为什么这样设计:直接 DELETE 只会删掉数据库里的元数据行,对象存储后端的真实文件会变成无人引用的"孤儿对象"。所以必须通过 SDK / Storage API 删除——后端在事务中协调"先删对象后删行",保证一致性。
正确做法:
// 客户端
await app.deleteFile({
fileList: [`cloud://<env-id>.<bucket>/<path>`],
});
逃生通道(仅限运维一次性清理):
SET LOCAL storage.allow_delete_query = 'true';
DELETE FROM storage.objects WHERE bucket_id = 'xxx' AND name = 'yyy';
-- 注意:这只删元 数据,对象存储后端的文件需要单独清理
为什么 storage.objects 不需要再 GRANT?
初始化脚本已统一执行:
GRANT ALL ON storage.objects TO anon, authenticated, service_role;
GRANT ALL ON storage.buckets TO anon, authenticated, service_role;
因此权限的唯一闸门是 RLS Policy——这与业务表"GRANT + RLS 双层"模型不同。在 storage.objects 上 GRANT 是冗余无害的,但完全没有必要。
上传文件时 owner_id 是如何填入的?
owner_id 由 Storage API 在写入 storage.objects 时根据当前 JWT 的 sub 自动注入。在 RLS 的 WITH CHECK 子句中,你只需写:
WITH CHECK (owner_id = auth.uid())
即可在数据库层再次校验,杜绝伪造。
RLS 拒绝了请求,怎么排查?
按以下顺序排查:
-
是否登录:
anon与authenticated是两套策略,匿名调用看不到authenticated-only 的对象 -
查 JWT 当前身份:在 SQL 工作台执行
SELECT auth.role(), auth.uid();(在 PostgREST 链路下生效) -
以 service_role 上帝视角看真实数据:
-- 在控制台 SQL 工作台(默认 service_role)执行SELECT id, bucket_id, name, owner_idFROM storage.objectsWHERE bucket_id = 'xxx' AND name LIKE 'yyy%'; -
模拟客户端角色复测:
SET LOCAL role = 'authenticated';SET LOCAL request.jwt.claims = '{"sub":"<目标 uid>","role":"authenticated"}';SELECT * FROM storage.objects WHERE bucket_id = 'xxx'; -
如果 INSERT 失败:用预检函数确认是否是 RLS 拒绝:
SELECT storage.can_insert_object('xxx', '<uid>/test.png', '{"size":1}'::jsonb);-- 抛 PT200 = RLS 通过、被人工 rollback-- 抛权限错误 = RLS 拒绝
是否可以在 RLS 中限制文件大小 / MIME 类型?
可以,但更推荐用 storage.buckets 自带的 file_size_limit 与 allowed_mime_types。如果业务规则比"全桶统一"复杂——比如"管理员可传 50 MB,普通用户最多 5 MB"——可在 RLS 里通过 metadata 判断:
CREATE POLICY size_limit_for_user ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'mixed'
AND owner_id = auth.uid()
AND COALESCE((metadata->>'size')::bigint, 0) <= 5 * 1024 * 1024
);
能从传统模式 / Supabase Storage 迁移过来吗?
- 从传统模式云存储迁移:因权限模型完全不同,需重写为 RLS 策略;文件实体可通过 SDK 对拷或在控制台批量迁移
- 从 Supabase Storage 迁移:
storageschema 与表结构与 Supabase 高度对齐(同源),SQL 策略与辅助函数(storage.foldername/storage.filename等)可大量直接复用,但需要复核public语义、SDK 初始化、返回字段和签名 URL 行为
详细步骤见 从 Supabase Storage 迁移。
文件版本控制如何做?
storage.objects 自带 version 字段,但默认不开启自动版本管理。若需要保留多版本:
- 业务侧:按
<path>/v<n>/<filename>约定路径 - 用
storage.objects配合业务表记录版本元数据 - 复杂版本需求建议结合对象存储后端能力(COS 版本控制)
CDN 加速、内容审核、数据万象怎么用?
CDN、扩展能力、内容审核、数据万象(图片处理 / 文档转换 / 智能检索)等高级功能与传统模式一致,与本目录共用:
还有问题?
- 看看 权限管理 和 RLS 策略模式库
- 或前往 传统模式云存储 - FAQ 查找通用问题