convex-file-system
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseConvexFS — File Storage for Convex
ConvexFS — Convex的文件存储组件
— Path-based file storage with global CDN delivery via bunny.net.convex-fs
— 基于路径的文件存储,通过bunny.net实现全球CDN分发。convex-fs
Installation & Setup
安装与配置
1. Install
1. 安装
bash
npm install convex-fsbash
npm install convex-fs2. Register component
2. 注册组件
typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import fs from "convex-fs/convex.config.js";
const app = defineApp();
app.use(fs);
export default app;typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import fs from "convex-fs/convex.config.js";
const app = defineApp();
app.use(fs);
export default app;3. Create ConvexFS instance
3. 创建ConvexFS实例
typescript
// convex/fs.ts
import { ConvexFS } from "convex-fs";
import { components } from "./_generated/api";
export const fs = new ConvexFS(components.fs, {
storage: {
type: "bunny",
apiKey: process.env.BUNNY_API_KEY!,
storageZoneName: process.env.BUNNY_STORAGE_ZONE!,
cdnHostname: process.env.BUNNY_CDN_HOSTNAME!,
tokenKey: process.env.BUNNY_TOKEN_KEY, // recommended for signed URLs
},
});typescript
// convex/fs.ts
import { ConvexFS } from "convex-fs";
import { components } from "./_generated/api";
export const fs = new ConvexFS(components.fs, {
storage: {
type: "bunny",
apiKey: process.env.BUNNY_API_KEY!,
storageZoneName: process.env.BUNNY_STORAGE_ZONE!,
cdnHostname: process.env.BUNNY_CDN_HOSTNAME!,
tokenKey: process.env.BUNNY_TOKEN_KEY, // recommended for signed URLs
},
});4. Register HTTP routes
4. 注册HTTP路由
typescript
// convex/http.ts
import { httpRouter } from "convex/server";
import { registerRoutes } from "convex-fs";
import { components } from "./_generated/api";
import { fs } from "./fs";
const http = httpRouter();
registerRoutes(http, components.fs, fs, {
pathPrefix: "/fs",
uploadAuth: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
return identity !== null;
},
downloadAuth: async (ctx, blobId, path) => {
const identity = await ctx.auth.getUserIdentity();
return identity !== null;
},
});
export default http;Creates: (upload proxy) and (302 redirect to CDN).
POST /fs/uploadGET /fs/blobs/{blobId}typescript
// convex/http.ts
import { httpRouter } from "convex/server";
import { registerRoutes } from "convex-fs";
import { components } from "./_generated/api";
import { fs } from "./fs";
const http = httpRouter();
registerRoutes(http, components.fs, fs, {
pathPrefix: "/fs",
uploadAuth: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
return identity !== null;
},
downloadAuth: async (ctx, blobId, path) => {
const identity = await ctx.auth.getUserIdentity();
return identity !== null;
},
});
export default http;创建以下路由:(上传代理)和(302重定向至CDN)。
POST /fs/uploadGET /fs/blobs/{blobId}5. Environment variables
5. 环境变量
Set in Convex dashboard (Settings → Environment Variables):
BUNNY_API_KEY=your-api-key
BUNNY_STORAGE_ZONE=your-storage-zone-name
BUNNY_CDN_HOSTNAME=your-zone.b-cdn.net
BUNNY_TOKEN_KEY=your-token-auth-key
BUNNY_REGION=ny # Optional: ny, la, sg, uk, se, br, jh, syd (default: Frankfurt)在Convex控制台中设置(设置 → 环境变量):
BUNNY_API_KEY=your-api-key
BUNNY_STORAGE_ZONE=your-storage-zone-name
BUNNY_CDN_HOSTNAME=your-zone.b-cdn.net
BUNNY_TOKEN_KEY=your-token-auth-key
BUNNY_REGION=ny # 可选:ny, la, sg, uk, se, br, jh, syd(默认:法兰克福)Core Concepts
核心概念
Architecture
架构
React Client ──▶ Convex Backend (ConvexFS) ──▶ bunny.net CDN (File Storage + Edge)- File metadata (paths, content types, sizes) → Convex tables
- File contents (blobs) → bunny.net Edge Storage
React Client ──▶ Convex Backend (ConvexFS) ──▶ bunny.net CDN (File Storage + Edge)- 文件元数据(路径、内容类型、大小)→ 存储在Convex表中
- 文件内容(Blob)→ 存储在bunny.net边缘存储中
Paths
路径
Any UTF-8 string. uses prefix matching (not directory listing):
list()- matches
prefix: "/users"AND/users/alice.txt/users-backup/data.bin - matches only under
prefix: "/users/"/users/
支持任意UTF-8字符串。方法使用前缀匹配(而非目录遍历):
list()- 会匹配
prefix: "/users"和/users/alice.txt/users-backup/data.bin - 仅匹配
prefix: "/users/"路径下的文件/users/
Blob lifecycle
Blob生命周期
Upload → Pending (4h TTL) → Committed (refCount=1) → Deleted (refCount=0) → GC cleanup- Reference counting: copy increments refCount (zero-copy), delete decrements it.
- Garbage collection: 3 automatic jobs — upload GC (hourly), blob GC (hourly), file expiration GC (every 15s).
- Grace period: orphaned blobs retained for (default 24h) before permanent deletion.
blobGracePeriod
上传 → 待处理(4小时TTL)→ 已提交(引用计数=1)→ 已删除(引用计数=0)→ 垃圾回收清理- 引用计数:复制操作会增加引用计数(零拷贝),删除操作会减少引用计数。
- 垃圾回收:3个自动任务 — 上传垃圾回收(每小时)、Blob垃圾回收(每小时)、文件过期垃圾回收(每15秒)。
- 宽限期:孤立Blob在永久删除前会保留时长(默认24小时)。
blobGracePeriod
Attributes
属性
typescript
interface FileAttributes {
expiresAt?: number; // Unix timestamp — auto-deleted by FGC
}Attributes are path-specific: cleared on move, not inherited on copy, removed on overwrite.
typescript
interface FileAttributes {
expiresAt?: number; // Unix时间戳 — 由FGC自动删除
}属性是路径专属的:移动文件时会清除属性,复制文件时不会继承属性,覆盖文件时会移除属性。
Core API
核心API
Query methods
查询方法
typescript
// Get file metadata by path
const file = await fs.stat(ctx, "/uploads/photo.jpg");
// Returns: { path, blobId, contentType, size, attributes } | null
// List files with pagination
const result = await fs.list(ctx, {
prefix: "/uploads/",
paginationOpts: { numItems: 50, cursor: null },
});
// Returns: { page: FileMetadata[], continueCursor, isDone }typescript
// 根据路径获取文件元数据
const file = await fs.stat(ctx, "/uploads/photo.jpg");
// 返回:{ path, blobId, contentType, size, attributes } | null
// 分页列出文件
const result = await fs.list(ctx, {
prefix: "/uploads/",
paginationOpts: { numItems: 50, cursor: null },
});
// 返回:{ page: FileMetadata[], continueCursor, isDone }Mutation methods
变更方法
typescript
// Commit uploaded blobs to paths
await fs.commitFiles(ctx, [
{ path: "/file.txt", blobId: "uuid-here" }, // overwrite if exists
{ path: "/new.txt", blobId: "uuid", basis: null }, // FAIL if exists
{ path: "/update.txt", blobId: "new-uuid", basis: "old-uuid" }, // CAS: fail if changed
]);
// Atomic multi-operation transaction
await fs.transact(ctx, [
{ op: "move", source: file, dest: { path: "/new/path.txt" } },
{ op: "copy", source: file, dest: { path: "/backup.txt", basis: null } },
{ op: "delete", source: file },
{ op: "setAttributes", source: file, attributes: { expiresAt: Date.now() + 3600000 } },
]);
// Convenience methods
await fs.move(ctx, "/old.txt", "/new.txt"); // throws if source missing or dest exists
await fs.copy(ctx, "/a.txt", "/b.txt"); // throws if source missing or dest exists
await fs.delete(ctx, "/file.txt"); // idempotent (no-op if missing)Basis values (for and destinations):
commitFilestransact| Value | Meaning |
|---|---|
| No check — overwrite if exists |
| File must NOT exist |
| File's current blobId must match (compare-and-swap) |
typescript
// 将上传的Blob提交至指定路径
await fs.commitFiles(ctx, [
{ path: "/file.txt", blobId: "uuid-here" }, // 若已存在则覆盖
{ path: "/new.txt", blobId: "uuid", basis: null }, // 若已存在则失败
{ path: "/update.txt", blobId: "new-uuid", basis: "old-uuid" }, // CAS:若文件已变更则失败
]);
// 原子多操作事务
await fs.transact(ctx, [
{ op: "move", source: file, dest: { path: "/new/path.txt" } },
{ op: "copy", source: file, dest: { path: "/backup.txt", basis: null } },
{ op: "delete", source: file },
{ op: "setAttributes", source: file, attributes: { expiresAt: Date.now() + 3600000 } },
]);
// 便捷方法
await fs.move(ctx, "/old.txt", "/new.txt"); // 若源文件不存在或目标文件已存在则抛出异常
await fs.copy(ctx, "/a.txt", "/b.txt"); // 若源文件不存在或目标文件已存在则抛出异常
await fs.delete(ctx, "/file.txt"); // 幂等操作(若文件不存在则无操作)Basis值(用于和的目标参数):
commitFilestransact| 值 | 含义 |
|---|---|
| 无检查 — 若已存在则覆盖 |
| 文件必须不存在 |
| 文件当前的blobId必须匹配(比较交换) |
Action methods
动作方法
typescript
// Generate signed download URL
const url = await fs.getDownloadUrl(ctx, blobId, { extraParams: { filename: "doc.pdf" } });
// Download blob data
const data = await fs.getBlob(ctx, blobId); // ArrayBuffer | null
// Download file contents + metadata
const result = await fs.getFile(ctx, "/file.txt"); // { data, contentType, size } | null
// Upload data and get blobId
const blobId = await fs.writeBlob(ctx, imageData, "image/webp");
// Upload + commit in one call
await fs.writeFile(ctx, "/report.pdf", pdfData, "application/pdf");typescript
// 生成签名下载URL
const url = await fs.getDownloadUrl(ctx, blobId, { extraParams: { filename: "doc.pdf" } });
// 下载Blob数据
const data = await fs.getBlob(ctx, blobId); // ArrayBuffer | null
// 下载文件内容 + 元数据
const result = await fs.getFile(ctx, "/file.txt"); // { data, contentType, size } | null
// 上传数据并获取blobId
const blobId = await fs.writeBlob(ctx, imageData, "image/webp");
// 上传并提交一步完成
await fs.writeFile(ctx, "/report.pdf", pdfData, "application/pdf");Client utilities
客户端工具
typescript
import { buildDownloadUrl } from "convex-fs";
const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path, { filename: "doc.pdf" });typescript
import { buildDownloadUrl } from "convex-fs";
const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path, { filename: "doc.pdf" });Upload & Serve Flow
上传与分发流程
Upload (React)
上传(React)
tsx
const handleUpload = async (file: File) => {
const siteUrl = (import.meta.env.VITE_CONVEX_URL ?? "").replace(/\.cloud$/, ".site");
// 1. Upload blob
const res = await fetch(`${siteUrl}/fs/upload`, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { blobId } = await res.json();
// 2. Commit to path (via mutation)
await commitFile({ blobId, filename: file.name });
};tsx
const handleUpload = async (file: File) => {
const siteUrl = (import.meta.env.VITE_CONVEX_URL ?? "").replace(/\.cloud$/, ".site");
// 1. 上传Blob
const res = await fetch(`${siteUrl}/fs/upload`, {
method: "POST",
headers: { "Content-Type": file.type },
body: file,
});
const { blobId } = await res.json();
// 2. 提交至指定路径(通过变更操作)
await commitFile({ blobId, filename: file.name });
};Serve (React)
分发(React)
tsx
import { buildDownloadUrl } from "convex-fs";
function Image({ path }: { path: string }) {
const file = useQuery(api.files.getFile, { path });
const siteUrl = (import.meta.env.VITE_CONVEX_URL ?? "").replace(/\.cloud$/, ".site");
if (!file) return <div>Loading...</div>;
const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path);
return <img src={url} alt={path} />;
}tsx
import { buildDownloadUrl } from "convex-fs";
function Image({ path }: { path: string }) {
const file = useQuery(api.files.getFile, { path });
const siteUrl = (import.meta.env.VITE_CONVEX_URL ?? "").replace(/\.cloud$/, ".site");
if (!file) return <div>加载中...</div>;
const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path);
return <img src={url} alt={path} />;
}Security Rules
安全规则
- Always authenticate uploads — open upload endpoints are a serious risk.
- Use path-based authorization — validate path ownership in .
downloadAuth - Enable token authentication on bunny.net — prevents URL tampering.
- Set appropriate URL TTLs: sensitive content 60–300s, general 3600s (default), streaming 3600s+.
- 始终验证上传权限 — 开放的上传端点存在严重安全风险。
- 使用基于路径的授权 — 在中验证路径归属。
downloadAuth - 在bunny.net上启用令牌认证 — 防止URL篡改。
- 设置合适的URL有效期:敏感内容60–300秒,普通内容3600秒(默认),流媒体内容3600秒以上。
Conflict Handling
冲突处理
typescript
import { ConvexError } from "convex/values";
import { isConflictError } from "convex-fs";
try {
await fs.commitFiles(ctx, files);
} catch (e) {
if (e instanceof ConvexError && isConflictError(e.data)) {
// e.data: { code, path, expected, found }
// Codes: SOURCE_NOT_FOUND, SOURCE_CHANGED, DEST_EXISTS, DEST_NOT_FOUND, DEST_CHANGED, CAS_CONFLICT
}
}typescript
import { ConvexError } from "convex/values";
import { isConflictError } from "convex-fs";
try {
await fs.commitFiles(ctx, files);
} catch (e) {
if (e instanceof ConvexError && isConflictError(e.data)) {
// e.data: { code, path, expected, found }
// 错误码:SOURCE_NOT_FOUND, SOURCE_CHANGED, DEST_EXISTS, DEST_NOT_FOUND, DEST_CHANGED, CAS_CONFLICT
}
}Constructor Options
构造函数选项
| Option | Type | Default | Description |
|---|---|---|---|
| | required | Bunny.net backend config |
| | | Signed URL expiration (seconds) |
| | | Orphaned blob retention (seconds) |
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| | 必填 | bunny.net后端配置 |
| | | 签名URL有效期(秒) |
| | | 孤立Blob保留时长(秒) |
Reference Files
参考文档
- Patterns & examples: User files, temp files, atomic ops, CAS updates, retry, pagination, React hooks → See references/examples.md
- Advanced topics: Multiple filesystems, disaster recovery, testing, GC details, Bunny.net setup, TypeScript types, troubleshooting → See references/advanced.md
- 模式与示例:用户文件、临时文件、原子操作、CAS更新、重试、分页、React钩子 → 查看references/examples.md
- 高级主题:多文件系统、灾难恢复、测试、垃圾回收细节、bunny.net配置、TypeScript类型、故障排除 → 查看references/advanced.md