convex-file-system
3
总安装量
3
周安装量
#60364
全站排名
安装命令
npx skills add https://github.com/imfa-solutions/skills --skill convex-file-system
Agent 安装分布
openclaw
3
gemini-cli
3
antigravity
3
claude-code
3
github-copilot
3
codex
3
Skill 文档
ConvexFS â File Storage for Convex
convex-fsâ Path-based file storage with global CDN delivery via bunny.net.
Installation & Setup
1. Install
npm install convex-fs
2. Register component
// 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
// 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
// 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: POST /fs/upload (upload proxy) and GET /fs/blobs/{blobId} (302 redirect to CDN).
5. Environment variables
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)
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
Paths
Any UTF-8 string. list() uses prefix matching (not directory listing):
prefix: "/users"matches/users/alice.txtAND/users-backup/data.binprefix: "/users/"matches only under/users/
Blob lifecycle
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
blobGracePeriod(default 24h) before permanent deletion.
Attributes
interface FileAttributes {
expiresAt?: number; // Unix timestamp â auto-deleted by FGC
}
Attributes are path-specific: cleared on move, not inherited on copy, removed on overwrite.
Core API
Query methods
// 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 }
Mutation methods
// 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 commitFiles and transact destinations):
| Value | Meaning |
|---|---|
undefined |
No check â overwrite if exists |
null |
File must NOT exist |
"blobId" |
File’s current blobId must match (compare-and-swap) |
Action methods
// 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");
Client utilities
import { buildDownloadUrl } from "convex-fs";
const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path, { filename: "doc.pdf" });
Upload & Serve Flow
Upload (React)
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 });
};
Serve (React)
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} />;
}
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+.
Conflict Handling
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
}
}
Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
storage |
StorageConfig |
required | Bunny.net backend config |
downloadUrlTtl |
number |
3600 |
Signed URL expiration (seconds) |
blobGracePeriod |
number |
86400 |
Orphaned blob retention (seconds) |
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