跳到主要内容

常见问题

与传统模式云存储有哪些差异?

维度传统模式云存储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.bucketspublic 字段是什么含义?

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.objectsstorage.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.objectsGRANT 是冗余无害的,但完全没有必要。

上传文件时 owner_id 是如何填入的?

owner_id 由 Storage API 在写入 storage.objects 时根据当前 JWT 的 sub 自动注入。在 RLS 的 WITH CHECK 子句中,你只需写:

WITH CHECK (owner_id = auth.uid())

即可在数据库层再次校验,杜绝伪造。

RLS 拒绝了请求,怎么排查?

按以下顺序排查:

  1. 是否登录anonauthenticated 是两套策略,匿名调用看不到 authenticated-only 的对象

  2. 查 JWT 当前身份:在 SQL 工作台执行 SELECT auth.role(), auth.uid();(在 PostgREST 链路下生效)

  3. 以 service_role 上帝视角看真实数据

    -- 在控制台 SQL 工作台(默认 service_role)执行
    SELECT id, bucket_id, name, owner_id
    FROM storage.objects
    WHERE bucket_id = 'xxx' AND name LIKE 'yyy%';
  4. 模拟客户端角色复测

    SET LOCAL role = 'authenticated';
    SET LOCAL request.jwt.claims = '{"sub":"<目标 uid>","role":"authenticated"}';

    SELECT * FROM storage.objects WHERE bucket_id = 'xxx';
  5. 如果 INSERT 失败:用预检函数确认是否是 RLS 拒绝:

    SELECT storage.can_insert_object('xxx', '<uid>/test.png', '{"size":1}'::jsonb);
    -- 抛 PT200 = RLS 通过、被人工 rollback
    -- 抛权限错误 = RLS 拒绝

是否可以在 RLS 中限制文件大小 / MIME 类型?

可以,但更推荐用 storage.buckets 自带的 file_size_limitallowed_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 迁移storage schema 与表结构与 Supabase 高度对齐(同源),SQL 策略与辅助函数(storage.foldername / storage.filename 等)可大量直接复用,但需要复核 public 语义、SDK 初始化、返回字段和签名 URL 行为

详细步骤见 从 Supabase Storage 迁移

文件版本控制如何做?

storage.objects 自带 version 字段,但默认不开启自动版本管理。若需要保留多版本:

  • 业务侧:按 <path>/v<n>/<filename> 约定路径
  • storage.objects 配合业务表记录版本元数据
  • 复杂版本需求建议结合对象存储后端能力(COS 版本控制)

CDN 加速、内容审核、数据万象怎么用?

CDN、扩展能力、内容审核、数据万象(图片处理 / 文档转换 / 智能检索)等高级功能与传统模式一致,与本目录共用

还有问题?