convex-file-system

📁 imfa-solutions/skills 📅 3 days ago
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.txt AND /users-backup/data.bin
  • prefix: "/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

  1. Always authenticate uploads — open upload endpoints are a serious risk.
  2. Use path-based authorization — validate path ownership in downloadAuth.
  3. Enable token authentication on bunny.net — prevents URL tampering.
  4. 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