﻿---
hide_table_of_contents: true
---

import ApiIntro from '../components/ApiIntro';

# PG 云存储

CloudBase JS SDK v3 PG 模式提供基于 PostgreSQL 原生 Bucket 的云存储能力。通过 `app.storage.from(bucketId)` 获取指定 Bucket 的对象操作客户端，所有路径参数均为 **Bucket 内对象名**，不要传 `cloud://` fileID。

::::tip 与传统模式云存储的差异
PG 模式与传统模式 API 的关键差异：

| 维度 | PG 模式 | 传统模式 |
|------|---------|---------|
| 入口 | `app.storage.from("bucketId")` | `app.storage.from()` |
| 路径参数 | Bucket 内对象名，如 `user-123/avatar.png` | `cloud://` fileID |
| 权限模型 | PostgreSQL RLS Policy | 内置权限 + JSON 安全规则 |
| Bucket 管理 | 支持 `createBucket` / `listBuckets` / `updateBucket` / `deleteBucket` | 不支持 |
| 文件列表 | 支持 `list()` | 不支持 |
| 覆盖默认值 | `upsert` 默认 `false` | `upsert` 默认 `true` |
| 跨 Bucket 操作 | `move()` / `copy()` 支持 `destinationBucket` | 不支持 |
| 图片转换 | 不支持 | `download()` / 签名 URL 支持 `TransformOptions` |
| 流式下载 | `download().asStream()` | 不支持 |

详见：[API 参考（传统模式）](../webv3/storage)
::::

## 快速开始 {#quick-start}

```ts
import cloudbase from "@cloudbase/js-sdk";

const app = cloudbase.init({ env: "your-env-id" });
const bucket = app.storage.from("avatars");

const { data, error } = await bucket.upload("user-123/avatar.png", file, {
  contentType: "image/png",
  upsert: true,
});
if (error) throw error;

const { data: blob } = await bucket.download("user-123/avatar.png");
const { data: signed } = await bucket.createSignedUrl("user-123/avatar.png", 3600);
await bucket.remove(["user-123/avatar.png"]);
```

## API 方法 {#api-method}

### Bucket 客户端 {#bucket-client}

- [from()](#from) - 选择 Bucket，返回对象操作客户端
- [throwOnError()](#throwonerror) - 错误时抛出异常

### 文件操作 {#file-operations}

- [upload()](#upload) - 上传文件
- [update()](#update) - 更新文件
- [download()](#download) - 下载文件
- [remove()](#remove) - 删除文件
- [move()](#move) - 移动文件
- [copy()](#copy) - 复制文件
- [list()](#list) - 列出 Bucket 中的对象

### URL 管理 {#url-management}

- [createSignedUrl()](#createsignedurl) - 创建签名 URL
- [createSignedUrls()](#createsignedurls) - 批量创建签名 URL
- [getPublicUrl()](#getpublicurl) - 获取公开 URL
- [createSignedUploadUrl()](#createsigneduploadurl) - 创建上传签名 URL

### 文件信息 {#file-info}

- [info()](#info) - 获取文件信息
- [exists()](#exists) - 检查文件是否存在

### Bucket 管理 {#bucket-management-index}

- [Bucket 管理](#bucket-management) - 创建、查询、更新、删除 Bucket


---

## from {#from}

```ts
from(bucketId: string): PostgresNativeStorageFileApi

type PostgresNativeStorageFileApi = StorageFileApi;
```

<ApiIntro parameter={{
input: [{ name: "bucketId", type: "string", required: true, description: "PG Bucket ID，对应 `storage.buckets.id`。" }],
output: [{ name: "PostgresNativeStorageFileApi", type: "Object", required: true, description: "PG 原生 Bucket 对象操作客户端。" }]
}}>

```ts
const bucket = app.storage.from("avatars");
```

</ApiIntro>


---

## throwOnError {#throwonerror}

```ts
throwOnError(): this
```

<ApiIntro parameter={{
input: [],
output: [{ name: "this", type: "PostgresNativeStorageFileApi", required: true, description: "当前客户端实例，支持链式调用。" }]
}}>

```ts
const bucket = app.storage.from("avatars").throwOnError();
await bucket.upload("user-123/avatar.png", file);
```

</ApiIntro>


---

## upload {#upload}

上传文件到云存储。

```ts
upload(
  path: string,
  fileBody: FileBody,
  fileOptions?: UploadOptions
): Promise<Result<UploadResult>>

type FileBody = File | Blob | ArrayBuffer | ArrayBufferView | FormData | ReadableStream<Uint8Array> | string;

type Result<T> = { data: T; error: null } | { data: null; error: StorageError };

interface UploadOptions {
  cacheControl?: string;
  contentType?: string;
  metadata?: Record<string, any>;
  upsert?: boolean; // 默认 false
}

interface UploadResult {
  id: string;
  path: string;
  fullPath: string; // bucketId/objectName
}
```

<ApiIntro parameter={{
input: [
{ name: "path", type: "string", required: true, description: "Bucket 内对象名，如 `user-123/avatar.png`。不要传 `cloud://` fileID。" },
{ name: "fileBody", type: "FileBody", required: true, description: "文件内容。" },
{ name: "fileOptions", type: "UploadOptions", required: false, description: "上传选项。默认不覆盖同名对象；覆盖需传 `upsert: true`。", children: [
{ name: "cacheControl", type: "string", required: false, description: "缓存控制。" },
{ name: "contentType", type: "string", required: false, description: "文件 MIME 类型。" },
{ name: "metadata", type: "object", required: false, description: "自定义元数据。" },
{ name: "upsert", type: "boolean", required: false, description: "是否覆盖同名对象，默认 `false`。" }
] }
],
output: [
{ name: "data.id", type: "string", required: true, description: "对象 ID。" },
{ name: "data.path", type: "string", required: true, description: "Bucket 内对象名。" },
{ name: "data.fullPath", type: "string", required: true, description: "对象完整路径，格式为 `bucketId/objectName`。" },
{ name: "error", type: "StorageError | null", required: false, description: "错误信息。" }
]
}}>

```ts
const { data, error } = await app.storage.from("avatars").upload("user-123/avatar.png", file, {
  contentType: "image/png",
  upsert: true,
  metadata: { usage: "avatar" },
});

if (error) throw error;
console.log(data.fullPath);
```

</ApiIntro>


---

## update {#update}

更新已存在的文件。

```ts
update(path: string, fileBody: FileBody, fileOptions?: UploadOptions): Promise<Result<UploadResult>>

interface UploadOptions {
  cacheControl?: string;
  contentType?: string;
  metadata?: Record<string, any>;
}
```

<ApiIntro parameter={{
input: [
{ name: "path", type: "string", required: true, description: "Bucket 内对象名。" },
{ name: "fileBody", type: "FileBody", required: true, description: "新的文件内容。" },
{ name: "fileOptions", type: "UploadOptions", required: false, description: "上传选项。" }
],
output: [
{ name: "data", type: "UploadResult | null", required: false, description: "更新结果。" },
{ name: "error", type: "StorageError | null", required: false, description: "错误信息。" }
]
}}>

```ts
await app.storage.from("avatars").update("user-123/avatar.png", newFile, {
  contentType: "image/png",
});
```

</ApiIntro>


---

## download {#download}

下载文件并返回内容。

```ts
download(
  path: string,
  options?: DownloadOptions,
  parameters?: FetchParameters
): DownloadBuilder

interface DownloadOptions {
  download?: string | boolean;
  cacheNonce?: string;
}

interface FetchParameters {
  signal?: AbortSignal;
  cache?: RequestCache;
}
```

<ApiIntro parameter={{
input: [
{ name: "path", type: "string", required: true, description: "Bucket 内对象名。" },
{ name: "options", type: "DownloadOptions", required: false, description: "下载选项。" },
{ name: "parameters", type: "FetchParameters", required: false, description: "请求控制参数。" }
],
output: [
{ name: "data", type: "Blob | ReadableStream | null", required: false, description: "默认 await 返回 Blob；调用 `.asStream()` 返回 ReadableStream。" },
{ name: "error", type: "StorageError | null", required: false, description: "错误信息。" }
]
}}>

```ts
const { data: blob } = await app.storage.from("avatars").download("user-123/avatar.png");

const { data: stream } = await app.storage
  .from("videos")
  .download("demo.mp4")
  .asStream();
```

</ApiIntro>


---

## remove {#remove}

删除一个或多个文件。

```ts
remove(paths: string[]): Promise<Result<FullObject[]>>

interface FullObject {
  name: string;
  bucket_id: string;
  owner_id?: string | null;
  metadata?: Record<string, any>;
}
```

<ApiIntro parameter={{
input: [{ name: "paths", type: "string[]", required: true, description: "Bucket 内对象名数组。" }],
output: [
{ name: "data", type: "FullObject[] | null", required: false, description: "删除成功的对象信息。" },
{ name: "error", type: "StorageError | null", required: false, description: "错误信息。" }
]
}}>

```ts
await app.storage.from("avatars").remove(["user-123/avatar.png"]);
```

</ApiIntro>


---

## move {#move}

移动文件到新位置。

```ts
move(fromPath: string, toPath: string, options?: DestinationOptions): Promise<Result<{ message: string }>>

interface DestinationOptions {
  destinationBucket?: string;
  upsert?: boolean;
}
```

<ApiIntro parameter={{
input: [
{ name: "fromPath", type: "string", required: true, description: "源对象名。" },
{ name: "toPath", type: "string", required: true, description: "目标对象名。" },
{ name: "options", type: "DestinationOptions", required: false, description: "目标 Bucket 等选项。" }
],
output: [
{ name: "data.message", type: "string", required: false, description: "操作结果消息。" },
{ name: "error", type: "StorageError | null", required: false, description: "错误信息。" }
]
}}>

```ts
await app.storage.from("avatars").move("user-123/avatar.png", "user-123/archive/avatar.png");
```

</ApiIntro>


---

## copy {#copy}

复制文件到新位置。

```ts
copy(fromPath: string, toPath: string, options?: DestinationOptions): Promise<Result<{ path: string }>>

interface DestinationOptions {
  destinationBucket?: string;
  upsert?: boolean;
  copyMetadata?: boolean;
  metadata?: Record<string, any>;
}
```

<ApiIntro parameter={{
input: [
{ name: "fromPath", type: "string", required: true, description: "源对象名。" },
{ name: "toPath", type: "string", required: true, description: "目标对象名。" },
{ name: "options", type: "DestinationOptions", required: false, description: "跨 Bucket、覆盖和元数据选项。" }
],
output: [
{ name: "data.path", type: "string", required: false, description: "目标对象完整路径。" },
{ name: "error", type: "StorageError | null", required: false, description: "错误信息。" }
]
}}>

```ts
await app.storage.from("avatars").copy("user-123/avatar.png", "user-123/avatar-copy.png", {
  upsert: true,
  copyMetadata: true,
});
```

</ApiIntro>


---

## createSignedUrl {#createsignedurl}

创建文件临时访问链接。

```ts
createSignedUrl(path: string, expiresIn: number, options?: SignedUrlOptions): Promise<Result<SignedUrlResult>>

interface SignedUrlOptions {
  download?: string | boolean;
  cacheNonce?: string;
}

interface SignedUrlResult {
  fullSignedURL: string;
}
```

<ApiIntro parameter={{
input: [
{ name: "path", type: "string", required: true, description: "Bucket 内对象名。" },
{ name: "expiresIn", type: "number", required: true, description: "有效期，单位秒。" },
{ name: "options", type: "SignedUrlOptions", required: false, description: "签名 URL 选项。" }
],
output: [
{ name: "data.fullSignedURL", type: "string", required: false, description: "可直接访问的完整签名 URL。" },
{ name: "error", type: "StorageError | null", required: false, description: "错误信息。" }
]
}}>

```ts
const { data } = await app.storage.from("avatars").createSignedUrl("user-123/avatar.png", 3600, {
  download: "avatar.png",
});
```

</ApiIntro>


---

## createSignedUrls {#createsignedurls}

批量创建临时访问链接。

```ts
createSignedUrls(paths: string[], expiresIn: number, options?: SignedUrlOptions): Promise<Result<SignedUrlsResultItem[]>>

interface SignedUrlsResultItem {
  path: string;
  fullSignedURL: string | null;
  error: string | null;
}
```

<ApiIntro parameter={{
input: [
{ name: "paths", type: "string[]", required: true, description: "Bucket 内对象名数组。" },
{ name: "expiresIn", type: "number", required: true, description: "有效期，单位秒。" },
{ name: "options", type: "SignedUrlOptions", required: false, description: "签名 URL 选项。" }
],
output: [
{ name: "data", type: "SignedUrlsResultItem[] | null", required: false, description: "批量签名结果。" },
{ name: "error", type: "StorageError | null", required: false, description: "错误信息。" }
]
}}>

```ts
await app.storage.from("avatars").createSignedUrls(["user-123/avatar.png"], 3600);
```

</ApiIntro>


---

## getPublicUrl {#getpublicurl}

获取公开访问 URL。

```ts
getPublicUrl(path: string, options?: PublicUrlOptions): { data: { publicUrl: string } }

interface PublicUrlOptions {
  download?: string | boolean;
  cacheNonce?: string;
}
```

<ApiIntro parameter={{
input: [
{ name: "path", type: "string", required: true, description: "Bucket 内对象名。" },
{ name: "options", type: "PublicUrlOptions", required: false, description: "公开 URL 选项。" }
],
output: [{ name: "data.publicUrl", type: "string", required: true, description: "公开访问 URL。" }]
}}>

```ts
const { data } = app.storage.from("public-assets").getPublicUrl("logos/cloudbase.png");
```

</ApiIntro>


---

## info {#info}

获取文件信息。

```ts
info(path: string): Promise<Result<ObjectInfo>>

interface ObjectInfo {
  id: string;
  name: string;
  bucketId: string;
  size?: number | null;
  contentType?: string | null;
  metadata?: Record<string, any>;
  createdAt?: string | null;
}
```

<ApiIntro parameter={{
input: [{ name: "path", type: "string", required: true, description: "Bucket 内对象名。" }],
output: [{ name: "data", type: "ObjectInfo | null", required: false, description: "对象元信息。" }, { name: "error", type: "StorageError | null", required: false, description: "错误信息。" }]
}}>

```ts
await app.storage.from("avatars").info("user-123/avatar.png");
```

</ApiIntro>


---

## exists {#exists}

检查文件是否存在。

```ts
exists(path: string): Promise<Result<boolean>>
```

<ApiIntro parameter={{
input: [{ name: "path", type: "string", required: true, description: "Bucket 内对象名。" }],
output: [{ name: "data", type: "boolean | null", required: false, description: "是否存在。" }, { name: "error", type: "StorageError | null", required: false, description: "错误信息。" }]
}}>

```ts
await app.storage.from("avatars").exists("user-123/avatar.png");
```

</ApiIntro>


---

## createSignedUploadUrl {#createsigneduploadurl}

创建上传签名 URL。

```ts
createSignedUploadUrl(path: string, options?: { upsert?: boolean }): Promise<Result<CreateSignedUploadUrlResult>>

interface CreateSignedUploadUrlResult {
  fullSignedURL: string;
  token: string;
  path: string;
}
```

<ApiIntro parameter={{
input: [{ name: "path", type: "string", required: true, description: "Bucket 内对象名。" }, { name: "options", type: "{ upsert?: boolean }", required: false, description: "是否允许覆盖。" }],
output: [{ name: "data", type: "CreateSignedUploadUrlResult | null", required: false, description: "签名上传 URL 与 token。" }, { name: "error", type: "StorageError | null", required: false, description: "错误信息。" }]
}}>

```ts
const bucket = app.storage.from("avatars");
const { data: signed } = await bucket.createSignedUploadUrl("user-123/avatar.png", { upsert: true });
await bucket.uploadToSignedUrl("user-123/avatar.png", signed.token, file);
```

</ApiIntro>


---

## list {#list}

列出 Bucket 中的对象。

```ts
list(path?: string, options?: ListOptions, parameters?: FetchParameters): Promise<Result<ListObjectsResponse>>

interface ListOptions {
  limit?: number;
  cursor?: string;
  withDelimiter?: boolean;
  sortBy?: { column?: "name" | "created_at" | "updated_at"; order?: "asc" | "desc" };
}
```

<ApiIntro parameter={{
input: [{ name: "path", type: "string", required: false, description: "路径前缀，不传表示 Bucket 根目录。" }, { name: "options", type: "ListOptions", required: false, description: "列表选项。" }],
output: [{ name: "data", type: "ListObjectsResponse | null", required: false, description: "包含 folders、objects、hasNext、nextCursor。" }, { name: "error", type: "StorageError | null", required: false, description: "错误信息。" }]
}}>

```ts
const { data } = await app.storage.from("avatars").list("user-123", {
  limit: 20,
  withDelimiter: true,
});
```

</ApiIntro>


---

## Bucket 管理 {#bucket-management}

以下 API 直接挂在 `app.storage` 上，仅 PG 环境可用：

```ts
listBuckets(options?: ListBucketOptions): Promise<Result<Bucket[]>>
getBucket(bucketId: string): Promise<Result<Bucket>>
createBucket(bucketId: string, options?: CreateBucketOptions): Promise<Result<{ name: string }>>
updateBucket(bucketId: string, options: UpdateBucketOptions): Promise<Result<{ message: string }>>
deleteBucket(bucketId: string): Promise<Result<{ message: string }>>

interface CreateBucketOptions {
  public?: boolean;
  type?: "STANDARD";
  fileSizeLimit?: number | string | null;
  allowedMimeTypes?: string[] | null;
}
```

<ApiIntro parameter={{
input: [{ name: "bucketId", type: "string", required: true, description: "Bucket ID。" }, { name: "options", type: "Object", required: false, description: "Bucket 配置。" }],
output: [{ name: "data", type: "Bucket | Bucket[] | Object | null", required: false, description: "Bucket 操作结果。" }]
}}>

```ts
await app.storage.createBucket("avatars", {
  public: false,
  type: "STANDARD",
  fileSizeLimit: "100MB",
  allowedMimeTypes: ["image/png", "image/jpeg"],
});

const { data: buckets } = await app.storage.listBuckets({ limit: 20, offset: 0 });
```

</ApiIntro>

---


---

# 最佳实践

## 1. 错误处理

```ts
const { data, error } = await app.storage.from("avatars").upload("user-123/avatar.png", file);

if (error) {
  console.error("操作失败:", error.message);
  return;
}
```

## 2. 文件路径规范

- 先通过 `from(bucketId)` 选择 Bucket，之后所有路径均为 Bucket 内对象名。
- 不要传 `cloud://` fileID。
- 路径不要以 `/` 开头，不要包含连续 `/`。

# 常见问题

## 1. PG 模式为什么不能传 `cloud://` fileID？

PG 原生对象 API 已经通过 `from(bucketId)` 指定 Bucket，方法入参中的 `path` 表示 Bucket 内对象名，因此不需要也不应该传 `cloud://` fileID。

## 2. 文件访问权限在哪里配置？

PG 模式由 PostgreSQL `storage.objects` / `storage.buckets` 上的 RLS Policy 控制。

# 相关资源

- [PG 模式云存储](../../../storage/pg/introduce)
- [PG 模式云存储 SDK 使用](../../../storage/pg/sdk)
- [PG 模式云存储权限管理](../../../storage/pg/data-permission)
- [PG 模式云存储 HTTP API](../../../http-api/storage-pg/pg-storage-api)