### Create Separate ConvexFS Instances (userFiles) Source: https://convexfs.dev/guides/advanced-configuration Shows how to create a dedicated ConvexFS instance for user files. This example configures the storage to use Bunny CDN and sets a grace period for blobs. It requires environment variables for API keys and storage zone details. ```typescript import { ConvexFS } from "convex-fs"; import { components } from "./_generated/api"; export const userFiles = new ConvexFS(components.userFiles, { storage: { type: "bunny", apiKey: process.env.USER_FILES_BUNNY_API_KEY!, storageZoneName: process.env.USER_FILES_STORAGE_ZONE!, cdnHostname: process.env.USER_FILES_CDN_HOSTNAME!, }, blobGracePeriod: 604800, // 7 days for user content }); ``` -------------------------------- ### Install ConvexFS npm package Source: https://convexfs.dev/guides/app-setup Install the ConvexFS component package using npm. This is the first step in integrating ConvexFS into your project. ```bash $ npm i convex-fs ``` -------------------------------- ### Implementing Download Authentication for ConvexFS Source: https://convexfs.dev/guides/authn-authz This example illustrates the implementation of a download authentication callback for ConvexFS HTTP routes. The `downloadAuth` callback is used to control access to downloaded files, allowing for granular permission checks based on user identity and file path. ```typescript registerRoutes(http, components.fs, fs, { pathPrefix: "/fs", // ... downloadAuth: async (ctx, blobId, path, extraParams) => { const identity = await ctx.auth.getUserIdentity(); if (!identity) { return false; } // With path available, you can check permissions directly if (path) { // e.g., verify user owns this file path return path.startsWith(`/users/${identity.subject}/`); } // Fallback for requests without path (legacy URLs) return false; }, }); ``` -------------------------------- ### Handling Conflicts with commitFiles() Source: https://convexfs.dev/guides/transactions-atomicity Provides an example of how to handle conflicts when using `fs.commitFiles()` by implementing retry logic when a precondition fails. ```APIDOC ## POST /convex/fs/commitFiles - Robust Update with Retries ### Description Implements a robust update mechanism for `fs.commitFiles()` that includes retry logic to handle potential conflicts. It attempts to commit a file, and if a conflict occurs, it re-reads the file's state and retries the operation up to a maximum number of attempts. ### Method POST ### Endpoint `/convex/fs/commitFiles` ### Parameters #### Request Body - **files** (array) - Required - An array of file objects to commit. - **path** (string) - Required - The path of the file. - **blobId** (string) - Required - The ID of the new blob to commit. - **basis** (string | null) - Optional - The expected current blobId of the file or null if the file should not exist. This is determined dynamically before each commit attempt. ### Request Example ```json { "files": [ { "path": "/path/to/resource.dat", "blobId": "", "basis": "" } ] } ``` ### Response #### Success Response (200) - **success** (boolean) - Indicates if the update was successful after retries. #### Response Example ```json { "success": true } ``` #### Error Response (e.g., 500) - **error** (object) - Details about the error if the operation fails after maximum retries. - **message** (string) - A message indicating failure after max retries. #### Error Example ```json { "error": { "message": "Failed after max retries" } } ``` ``` -------------------------------- ### Atomically swap two files (JavaScript) Source: https://convexfs.dev/guides/transactions-atomicity This example demonstrates an atomic file swap using `fs.transact` in Convex. It performs three move operations in sequence: moving file A to a temporary location, moving file B to A's original path, and finally moving the temporary file to B's original path. The entire sequence succeeds or fails as a single atomic unit. ```javascript export const swapFiles = mutation({ args: { pathA: v.string(), pathB: v.string(), }, handler: async (ctx, args) => { const fileA = await fs.stat(ctx, args.pathA); const fileB = await fs.stat(ctx, args.pathB); if (!fileA || !fileB) { throw new Error("Both files must exist"); } // Atomic swap: A -> temp, B -> A, temp -> B // All three operations succeed or none do await fs.transact(ctx, [ { op: "move", source: fileA, dest: { path: "/tmp/swap" } }, { op: "move", source: fileB, dest: { path: args.pathA } }, { op: "move", source: { ...fileA, path: "/tmp/swap" }, dest: { path: args.pathB }, }, ]); }, }); ``` -------------------------------- ### Configuring Download URL Expiration TTL in ConvexFS Source: https://convexfs.dev/guides/authn-authz This example shows how to set the `downloadUrlTtl` option when initializing ConvexFS. This option controls the duration (in seconds) for which the signed CDN URLs remain valid. Shorter TTLs enhance security for sensitive content, while longer TTLs improve user experience for media playback and large downloads. ```javascript const fs = new ConvexFS(components.fs, { storage: { /* ... */ }, downloadUrlTtl: 300, // URLs expire after 5 minutes (default: 3600 = 1 hour) }); ``` -------------------------------- ### Get File URL with Server-Side Validation (Convex/TypeScript) Source: https://convexfs.dev/guides/serving-files This Convex query function retrieves a file's metadata using fs.stat and constructs a secure download URL using buildDownloadUrl. It includes the 'path' parameter for server-side validation, ensuring the file exists and the blobId matches, which enhances security by preventing access to deleted or unauthorized files. ```typescript import { query } from "./_generated/server"; import { v } from "convex/values"; import { buildDownloadUrl } from "convex-fs"; import { fs } from "./fs"; export const getFileUrl = query({ args: { path: v.string() }, handler: async (ctx, args) => { const siteUrl = process.env.CONVEX_SITE_URL!; const file = await fs.stat(ctx, args.path); if (!file) { return null; } // Include the path for server-side validation return buildDownloadUrl(siteUrl, "/fs", file.blobId, args.path); }, }); ``` -------------------------------- ### Initialize ConvexFS with Bunny.net Storage Source: https://convexfs.dev/guides/advanced-configuration Demonstrates initializing a ConvexFS instance with Bunny.net Edge Storage. It requires API keys and zone details, and allows configuring download URL TTL and blob grace periods. ```javascript 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, region: "ny", }, downloadUrlTtl: 3600, blobGracePeriod: 86400, }); ``` -------------------------------- ### Use Case: Temporary Upload Staging Source: https://convexfs.dev/guides/file-expiration Example of staging uploaded files in a temporary directory with automatic cleanup after a set duration. ```APIDOC ## Use Case: Temporary Upload Staging ### Description This use case demonstrates staging uploaded files in a temporary location with automatic expiration, and then finalizing them by moving them to their permanent location. ### API Endpoints Involved - `POST /convex/fs/commitFiles` (for staging) - `POST /convex/fs/move` (for finalizing) ### Example Workflow 1. **Stage Upload**: Upload a file and commit it with an expiration time to a staging directory. ```javascript // Assuming ctx and args are available const path = `/staging/${args.blobId}/${args.filename}`; const expiresAt = Date.now() + 4 * 60 * 60 * 1000; // 4 hours await fs.commitFiles(ctx, [ { path, blobId: args.blobId, attributes: { expiresAt } }, ]); return path; ``` 2. **Finalize Upload**: Move the staged file to its final destination. The move operation will clear the expiration attributes by default. ```javascript // Assuming ctx and args are available await fs.move(ctx, args.stagingPath, args.finalPath); ``` ### Notes - The `finalizeUpload` function moves the file, and the move operation inherently clears the `expiresAt` attribute. The finalized file will not expire unless a new expiration is set on it. ``` -------------------------------- ### ConvexFS Constructor Options Source: https://convexfs.dev/guides/advanced-configuration Options for initializing a ConvexFS instance, including storage configuration, download URL time-to-live, and blob grace period. ```APIDOC ## ConvexFS Constructor Options ### Description Configuration options when creating a `ConvexFS` instance. ### Parameters #### Request Body - **storage** (object) - Required - Configuration for the storage backend. - **type** (string) - Required - Must be `"bunny"` for Bunny.net Edge Storage. - **apiKey** (string) - Required - Your Bunny.net Edge Storage API key. - **storageZoneName** (string) - Required - Name of your storage zone. - **cdnHostname** (string) - Required - CDN hostname for downloads. - **tokenKey** (string) - Optional - Token authentication key for signed CDN URLs. - **region** (string) - Optional - Storage zone region code (e.g., `"uk"`, `"ny"`, `"la"`). Defaults to Frankfurt. - **downloadUrlTtl** (number) - Optional - Time-to-live for signed download URLs in seconds. Defaults to `3600` (1 hour). - **blobGracePeriod** (number) - Optional - Grace period in seconds before orphaned blobs are permanently deleted. Defaults to `86400` (24 hours). ### Request Example ```json { "storage": { "type": "bunny", "apiKey": "YOUR_API_KEY", "storageZoneName": "YOUR_ZONE_NAME", "cdnHostname": "YOUR_CDN_HOSTNAME", "tokenKey": "YOUR_TOKEN_KEY", "region": "ny" }, "downloadUrlTtl": 7200, "blobGracePeriod": 172800 } ``` ### Response #### Success Response (200) ConvexFS instance is created successfully. #### Response Example ```json { "message": "ConvexFS instance created successfully." } ``` ``` -------------------------------- ### Read File Expiration Source: https://convexfs.dev/guides/file-expiration Retrieves the `expiresAt` timestamp from a file's attributes using `fs.stat()`. The example shows how to check if a file has an expiration date and calculate the remaining time until expiration. ```javascript const file = await fs.stat(ctx, "/temp/upload.jpg"); if (file?.attributes?.expiresAt) { const expiresIn = file.attributes.expiresAt - Date.now(); console.log(`File expires in ${expiresIn}ms`); } ``` -------------------------------- ### Create Separate ConvexFS Instances (staticAssets) Source: https://convexfs.dev/guides/advanced-configuration Illustrates the creation of a separate ConvexFS instance for static assets. Similar to the user files instance, it uses Bunny CDN for storage but with a shorter blob grace period. Environment variables are necessary for configuration. ```typescript import { ConvexFS } from "convex-fs"; import { components } from "./_generated/api"; export const staticAssets = new ConvexFS(components.staticAssets, { storage: { type: "bunny", apiKey: process.env.STATIC_BUNNY_API_KEY!, storageZoneName: process.env.STATIC_STORAGE_ZONE!, cdnHostname: process.env.STATIC_CDN_HOSTNAME!, }, blobGracePeriod: 86400, // 24 hours for static assets }); ``` -------------------------------- ### Using commitFiles() - Create File If Not Exists Source: https://convexfs.dev/guides/transactions-atomicity Demonstrates how to use `fs.commitFiles()` with `basis: null` to create a file only if it does not already exist. ```APIDOC ## POST /convex/fs/commitFiles ### Description Commits uploaded blobs to file paths. Supports a `basis` precondition on each file to control overwriting behavior. ### Method POST ### Endpoint `/convex/fs/commitFiles` ### Parameters #### Request Body - **files** (array) - Required - An array of file objects to commit. - **path** (string) - Required - The path of the file. - **blobId** (string) - Required - The ID of the blob to commit. - **basis** (string | null | undefined) - Optional - Precondition for the commit. - `undefined`: No check, overwrite if exists. - `null`: File must NOT exist. - `""`: File must have this exact blobId. ### Request Example ```json { "files": [ { "path": "/path/to/file.txt", "blobId": "", "basis": null } ] } ``` ### Response #### Success Response (200) - **success** (boolean) - Indicates if the commit operation was successful. #### Response Example ```json { "success": true } ``` ``` -------------------------------- ### buildDownloadUrl with CDN Parameters Source: https://convexfs.dev/guides/cdn-parameters Demonstrates how to use the `buildDownloadUrl` function to include extra CDN parameters for on-the-fly transformations. ```APIDOC ## POST /buildDownloadUrl ### Description Appends extra CDN parameters to a download URL, which can be used to trigger Edge Rules for transformations like image optimization or setting custom headers. ### Method POST ### Endpoint /buildDownloadUrl ### Parameters #### Query Parameters - **siteUrl** (string) - Required - The base URL of your Convex site. - **pathPrefix** (string) - Required - The path prefix for file access (e.g., "/fs"). - **blobId** (string) - Required - The unique identifier for the file blob. - **filePath** (string) - Required - The path to the file within the ConvexFS storage. - **cdnParams** (object) - Optional - An object containing key-value pairs for CDN parameters. These are JSON-encoded into the `cdn-params` query parameter. - **filename** (string) - Optional - Specifies a custom filename for the download. - **quality** (string) - Optional - Specifies the desired image quality (e.g., "high"). ### Request Example ```json { "siteUrl": "https://your-convex-site.convex.cloud", "pathPrefix": "/fs", "blobId": "some-blob-id", "filePath": "/documents/report.pdf", "cdnParams": { "filename": "quarterly-report.pdf", "quality": "high" } } ``` ### Response #### Success Response (200) - **url** (string) - The generated download URL with encoded CDN parameters. #### Response Example ```json { "url": "https://your-convex-site.convex.cloud/fs/some-blob-id?cdn-params=eyJoZWxsbyI6IndvcmxkIn0=" } ``` ``` -------------------------------- ### Display File Details (React Frontend) Source: https://convexfs.dev/guides/filesystem-operations Fetches and displays file metadata in a React component using the `useQuery` hook. It handles loading states and displays file not found messages. ```typescript import { useQuery } from "convex/react"; import { api } from "../convex/_generated/api"; function FileDetails({ path }: { path: string }) { const file = useQuery(api.files.getFile, { path }); if (file === undefined) { return
Loading...
; } if (file === null) { return
File not found
; } return (

Path: {file.path}

Type: {file.contentType}

Size: {file.size} bytes

); } ``` -------------------------------- ### Convex Query for Listing Files with Auth Source: https://convexfs.dev/guides/authn-authz This Convex query demonstrates how to authenticate a user before calling the ConvexFS fs.list() method. It first checks for user identity and then proceeds to list files, allowing for additional custom access privilege checks. ```typescript import { query } from "./_generated/server"; import { paginationOptsValidator } from "convex/server"; import { v } from "convex/values"; import { fs } from "./fs"; export const listMyFiles = query({ args: { prefix: v.optional(v.string()), paginationOpts: paginationOptsValidator, }, handler: async (ctx, args) => { // Standard Convex auth check const identity = await ctx.auth.getUserIdentity(); if (!identity) { throw new Error("Not authenticated"); } // Check any other access privileges specific to your app here // e.g., verify user has permission to access this prefix // Now call ConvexFS - user is authenticated return await fs.list(ctx, { prefix: args.prefix, paginationOpts: args.paginationOpts, }); }, }); ``` -------------------------------- ### downloadAuth Callback with CDN Parameters Source: https://convexfs.dev/guides/cdn-parameters Illustrates how the `downloadAuth` callback receives extra CDN parameters, allowing for authorization decisions based on these parameters. ```APIDOC ## POST /registerRoutes (downloadAuth callback) ### Description Handles authorization for file downloads by inspecting extra CDN parameters passed to the download URL. ### Method POST ### Endpoint /registerRoutes ### Parameters #### Request Body - **ctx** (object) - The Convex context object. - **blobId** (string) - The unique identifier for the file blob. - **path** (string) - The path to the file. - **extraParams** (object) - An object containing the decoded CDN query parameters passed to the download URL. - **filename** (string) - Optional - The custom filename specified. - **quality** (string) - Optional - The desired image quality. ### Request Example ```javascript registerRoutes(http, components.fs, fs, { pathPrefix: "/fs", downloadAuth: async (ctx, blobId, path, extraParams) => { // Example: only allow certain parameter values if (extraParams?.quality && extraParams.quality !== "high") { return false; } return true; } }); ``` ### Response #### Success Response (200) - **boolean** - Returns `true` if the download is authorized, `false` otherwise. #### Response Example ```json { "authorized": true } ``` ``` -------------------------------- ### Build Download URL with Extra CDN Parameters (TypeScript) Source: https://convexfs.dev/guides/cdn-parameters Demonstrates how to use the `buildDownloadUrl` function from `convex-fs` to include extra CDN parameters. These parameters are JSON-encoded and appended to the CDN URL, allowing for custom transformations via Bunny.net Edge Rules. The function takes site URL, path prefix, blob ID, file path, and an optional object for CDN parameters. ```typescript import { buildDownloadUrl } from "convex-fs"; // Without extra params (existing behavior) const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path); // With extra params const url = buildDownloadUrl(siteUrl, "/fs", file.blobId, file.path, { filename: "quarterly-report.pdf", }); ``` -------------------------------- ### Build Download URL in Frontend (JavaScript/TypeScript) Source: https://convexfs.dev/guides/serving-files This frontend code snippet demonstrates using the buildDownloadUrl function directly within a React component to generate image source URLs. It maps over an array of image objects, each containing a 'path' and 'blobId', to dynamically set the 'src' attribute for img tags, enabling direct rendering of files from ConvexFS. ```javascript import { buildDownloadUrl } from "convex-fs"; function ImageGallery({ images, siteUrl }: Props) { return (
{images.map((image) => ( {image.path} ))}
); } ``` -------------------------------- ### Create ConvexFS object instance Source: https://convexfs.dev/guides/app-setup Create an instance of the `ConvexFS` class in a TypeScript file within your `convex/` directory (e.g., `fs.ts`). This object will be used to interact with ConvexFS. It requires configuration for storage, such as Bunny.net Edge Storage. ```typescript import { ConvexFS } from "convex-fs"; import { components } from "./_generated/api"; // Using Bunny.net Edge Storage with token-authenticated Pull Zone export const fs = new ConvexFS(components.fs, { storage: { type: "bunny", apiKey: process.env.BUNNY_API_KEY!, storageZoneName: process.env.BUNNY_STORAGE_ZONE!, region: process.env.BUNNY_REGION, cdnHostname: process.env.BUNNY_CDN_HOSTNAME!, // e.g., "myzone.b-cdn.net" tokenKey: process.env.BUNNY_TOKEN_KEY, }, }); ``` -------------------------------- ### fs.transact() - Safe Copy with Destination Precondition Source: https://convexfs.dev/guides/transactions-atomicity Illustrates how to use fs.transact() for a safe copy operation. The copy will only succeed if the destination path does not already exist (basis: null). ```APIDOC ## POST /fs/transact ### Description Executes one or more filesystem operations atomically. This example shows a safe copy operation where the destination must not exist. ### Method POST ### Endpoint `/fs/transact` ### Parameters #### Request Body - **operations** (array) - Required - An array of operations to perform atomically. - **op** (string) - Required - The type of operation (e.g., "delete", "move", "copy"). - **source** (object) - Required - The source file object obtained from `fs.stat()`, including the expected `blobId`. - **dest** (object) - Optional - For "move" and "copy" operations, specifies the destination path and an optional `basis` precondition. - **path** (string) - Required - The destination path. - **basis** (string | null | undefined) - Optional - Precondition for the destination path: - `undefined`: Overwrite if exists. - `null`: Destination must NOT exist. - ``: Destination must have this exact blobId. ### Request Example ```json { "operations": [ { "op": "copy", "source": { "path": "/uploads/photo.jpg", "blobId": "some-blob-id-123", "size": 1024, "mtime": 1678886400000 }, "dest": { "path": "/archive/photo.jpg", "basis": null } } ] } ``` ### Response #### Success Response (200) - **result** (boolean) - Indicates if the transaction was successful. #### Response Example ```json { "result": true } ``` ``` -------------------------------- ### Read file before move operation (JavaScript) Source: https://convexfs.dev/guides/transactions-atomicity This snippet demonstrates reading a file's metadata before attempting to move it. It highlights the potential for data races if the file is modified or deleted by another operation between the `stat` and `move` calls. ```javascript const file = await fs.stat(ctx, "/uploads/photo.jpg"); // ... time passes, other mutations run ... // Move it to a new location await fs.move(ctx, "/uploads/photo.jpg", "/archive/photo.jpg"); ``` -------------------------------- ### Retrieve File Details (Convex Backend) Source: https://convexfs.dev/guides/filesystem-operations Creates a Convex backend query using `fs.stat()` to retrieve metadata for a specific file identified by its path. Returns `null` if the file does not exist. ```typescript import { query } from "./_generated/server"; import { v } from "convex/values"; import { fs } from "./fs"; export const getFile = query({ args: { path: v.string() }, handler: async (ctx, args) => { return await fs.stat(ctx, args.path); }, }); ``` -------------------------------- ### Register Routes for Each Filesystem (convex/http.ts) Source: https://convexfs.dev/guides/advanced-configuration Details how to register HTTP routes for each ConvexFS instance using `registerRoutes`. This allows mounting each filesystem at a specific path prefix and defining authentication logic for uploads and downloads. It integrates with Convex's httpRouter. ```typescript import { httpRouter } from "convex/server"; import { registerRoutes } from "convex-fs"; import { components } from "./_generated/api"; import { userFiles } from "./userFiles"; import { staticAssets } from "./staticAssets"; const http = httpRouter(); registerRoutes(http, components.userFiles, userFiles, { pathPrefix: "/user-files", uploadAuth: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); return identity !== null; }, downloadAuth: async (ctx, blobId) => { // Check user has access to this blob return true; }, }); registerRoutes(http, components.staticAssets, staticAssets, { pathPrefix: "/static", uploadAuth: async (ctx) => { // Only admins can upload static assets const identity = await ctx.auth.getUserIdentity(); return identity?.role === "admin"; }, downloadAuth: async () => true, // Public access }); export default http; ``` -------------------------------- ### Download File by Path using fs.getFile Source: https://convexfs.dev/guides/filesystem-operations The fs.getFile() method retrieves a file's content by its path. It returns an object containing the file data (as an ArrayBuffer), its MIME type, and its size. This method is suitable for most download scenarios as it handles path lookup. ```typescript import { action } from "./_generated/server"; import { v } from "convex/values"; import { fs } from "./fs"; export const processFile = action({ args: { path: v.string() }, handler: async (ctx, args) => { const result = await fs.getFile(ctx, args.path); if (!result) { throw new Error("File not found"); } // result.data is an ArrayBuffer // result.contentType is the MIME type // result.size is the file size in bytes const text = new TextDecoder().decode(result.data); return { content: text, size: result.size }; }, }); ``` -------------------------------- ### HTTP Route Options Source: https://convexfs.dev/guides/advanced-configuration Configuration for registering HTTP endpoints for uploads and downloads using `registerRoutes()`. ```APIDOC ## HTTP Route Options ### Description Options for configuring the HTTP endpoints when calling `registerRoutes()`. ### Parameters #### Path Parameters None #### Query Parameters None #### Request Body - **pathPrefix** (string) - Optional - URL path prefix for upload/download routes. Defaults to `"/fs"`. - **uploadAuth** (function) - Required - Callback function to authenticate uploads. Should return `Promise`. - **downloadAuth** (function) - Required - Callback function to authenticate downloads. Should return `Promise`. ### Request Example ```javascript registerRoutes(http, components.fs, fs, { pathPrefix: "/api/v1/files", uploadAuth: async (ctx) => { const identity = await ctx.auth.getUserIdentity(); return identity !== null; }, downloadAuth: async (ctx, blobId) => { // Implement download authentication logic return true; } }); ``` ### Response #### Success Response (200) HTTP routes are registered successfully. #### Response Example ```json { "message": "HTTP routes registered successfully." } ``` ### Error Handling - **401**: Upload rejected due to authentication failure. - **403**: Download rejected due to authentication failure. ``` -------------------------------- ### Mount ConvexFS Component Multiple Times (convex.config.ts) Source: https://convexfs.dev/guides/advanced-configuration Demonstrates how to mount the ConvexFS component multiple times in your `convex.config.ts` file, allowing for distinct configurations for different filesystems. This is achieved by using the `name` option during the `app.use` call. ```typescript import { defineApp } from "convex/server"; import fs from "convex-fs/convex.config.js"; const app = defineApp(); app.use(fs, { name: "userFiles" }); app.use(fs, { name: "staticAssets" }); export default app; ``` -------------------------------- ### fs.transact() - Delete with Source Precondition Source: https://convexfs.dev/guides/transactions-atomicity Demonstrates how to use fs.transact() to delete a file only if it hasn't changed since it was last read. This prevents accidental deletion of the wrong version. ```APIDOC ## POST /fs/transact ### Description Executes one or more filesystem operations atomically. Each operation can specify preconditions to ensure data integrity. ### Method POST ### Endpoint `/fs/transact` ### Parameters #### Request Body - **operations** (array) - Required - An array of operations to perform atomically. - **op** (string) - Required - The type of operation (e.g., "delete", "move", "copy"). - **source** (object) - Required - The source file object obtained from `fs.stat()`, including the expected `blobId`. - **dest** (object) - Optional - For "move" and "copy" operations, specifies the destination path and an optional `basis` precondition. - **path** (string) - Required - The destination path. - **basis** (string | null | undefined) - Optional - Precondition for the destination path: - `undefined`: Overwrite if exists. - `null`: Destination must NOT exist. - ``: Destination must have this exact blobId. ### Request Example ```json { "operations": [ { "op": "delete", "source": { "path": "/uploads/photo.jpg", "blobId": "some-blob-id-123", "size": 1024, "mtime": 1678886400000 } } ] } ``` ### Response #### Success Response (200) - **result** (boolean) - Indicates if the transaction was successful. #### Response Example ```json { "result": true } ``` ``` -------------------------------- ### Compare-and-Swap Updates with commitFiles() Source: https://convexfs.dev/guides/transactions-atomicity Illustrates how to use the `basis` field in `fs.commitFiles()` for compare-and-swap (CAS) semantics, useful for updating files based on their current contents. ```APIDOC ## POST /convex/fs/commitFiles - CAS Update ### Description Performs a compare-and-swap update on a file using `fs.commitFiles()`. The `basis` field ensures the file has not changed since it was last read before committing the new blob. ### Method POST ### Endpoint `/convex/fs/commitFiles` ### Parameters #### Request Body - **files** (array) - Required - An array of file objects to commit. - **path** (string) - Required - The path of the file. - **blobId** (string) - Required - The ID of the new blob to commit. - **basis** (string) - Required - The expected current blobId of the file. If this does not match the actual blobId, the commit will fail, preventing unintended overwrites. ### Request Example ```json { "files": [ { "path": "/path/to/image.jpg", "blobId": "", "basis": "" } ] } ``` ### Response #### Success Response (200) - **success** (boolean) - Indicates if the CAS update was successful. #### Response Example ```json { "success": true } ``` ``` -------------------------------- ### Display Image using Convex Query (React/TypeScript) Source: https://convexfs.dev/guides/serving-files This React component fetches a file URL from a Convex backend using the useQuery hook and displays it within an img tag. It calls the `api.files.getFileUrl` function, passing the file path, and uses the returned URL to set the src attribute of the image, allowing for dynamic image loading. ```typescript import { useQuery } from "convex/react"; import { api } from "../convex/_generated/api"; function Image({ path }: { path: string }) { const url = useQuery(api.files.getFileUrl, { path }); return {path}; } ``` -------------------------------- ### ConvexFS Download Authentication Callback Source: https://convexfs.dev/guides/advanced-configuration Defines the type signature for the `downloadAuth` callback, used to authenticate download requests. It receives the HTTP action context, blob ID, and optional path/extra parameters, returning a boolean to permit or deny the download. ```typescript downloadAuth: async ( ctx: HttpActionCtx, blobId: string, path?: string, extraParams?: Record, ) => Promise; ``` -------------------------------- ### Filter File Listing by Prefix (React Frontend) Source: https://convexfs.dev/guides/filesystem-operations Demonstrates how to filter the file list by a specific path prefix using the `usePaginatedQuery` hook in a React component. This allows fetching only files within a certain directory. ```typescript const { results, status, loadMore } = usePaginatedQuery( api.files.listFiles, { prefix: "/images/" }, { initialNumItems: 20 }, ); ``` -------------------------------- ### Handle Download Authorization with Extra Parameters (TypeScript) Source: https://convexfs.dev/guides/cdn-parameters Illustrates how the `downloadAuth` callback in ConvexFS receives extra parameters passed to the download URL. This enables authorization logic based on these parameters, such as validating specific values like 'quality'. The callback signature includes context, blob ID, path, and the `extraParams` object. ```typescript registerRoutes(http, components.fs, fs, { pathPrefix: "/fs", uploadAuth: async (ctx) => { /* ... */ }, downloadAuth: async (ctx, blobId, path, extraParams) => { // extraParams contains any query params passed to the download URL // (excluding 'path' which is handled separately) // Example: only allow certain parameter values if (extraParams?.quality && extraParams.quality !== "high") { return false; } return true; }, }); ``` -------------------------------- ### Download Authorization Callback Source: https://convexfs.dev/guides/authn-authz Implement a custom `downloadAuth` callback to control access to files. This callback can check permissions based on the user's identity and the provided blobId or path. ```APIDOC ## POST /fs/download (Conceptual) ### Description This endpoint, when configured with a custom `downloadAuth` callback, allows for fine-grained control over file access. The callback determines if a user is authorized to download a specific blob, either by checking the provided path or by querying for files associated with the blobId. ### Method POST (Conceptual - actual registration is via `registerRoutes`) ### Endpoint /fs/download ### Parameters #### Path Parameters - **blobId** (string) - Required - The unique identifier for the blob to download. - **path** (string) - Optional - The specific path associated with the blob, used for direct authorization checks. #### Query Parameters None #### Request Body None (Authorization is handled via callback logic) ### Request Example (No direct request body, authorization is determined by the `downloadAuth` callback logic) ### Response #### Success Response (200) - **Boolean** (boolean) - `true` if authorized, `false` otherwise. (This is the return value of the `downloadAuth` callback, which influences the subsequent redirect to a CDN URL). #### Response Example ```json { "authorized": true } ``` ### Notes - The `downloadAuth` callback receives `ctx`, `blobId`, `path`, and `extraParams`. - If `path` is provided, it checks if the path starts with `/users/${identity.subject}/`. - If `path` is not provided, it queries `api.files.canUserAccessBlob` to determine access based on `userId` and `blobId`. ``` -------------------------------- ### Compare-and-Swap File Update using commitFiles() Source: https://convexfs.dev/guides/transactions-atomicity This action demonstrates a compare-and-swap (CAS) update for a file. It reads the current file's blobId, processes the image using an external library (sharp), and then commits the new blob only if the file's blobId has not changed since it was read. This prevents race conditions. ```typescript "use node"; import sharp from "sharp"; import { action } from "convex/server"; import { v } from "convex/values"; import { fs } from "convex-fs"; // Assume detectFaceRegion is defined elsewhere and returns a sharp region object // async function detectFaceRegion(imageData: Buffer): Promise export const cropToFace = action({ args: { path: v.string() }, handler: async (ctx, args) => { // Read current file state and contents const file = await fs.stat(ctx, args.path); if (!file) { throw new Error("File not found"); } const imageData = await fs.getFile(ctx, args.path); // Use sharp to detect face and crop (external library call) const cropped = await sharp(imageData.data) .extract(await detectFaceRegion(imageData.data)) // Placeholder for actual face detection .toBuffer(); // Upload the processed image const newBlobId = await fs.writeBlob(ctx, cropped, "image/jpeg"); // Only overwrite if file hasn't changed since we read it await fs.commitFiles(ctx, [ { path: args.path, blobId: newBlobId, basis: file.blobId }, ]); }, }); ``` -------------------------------- ### List Files with Pagination (Convex Backend) Source: https://convexfs.dev/guides/filesystem-operations Exposes an `fs.list()` query on the Convex backend to retrieve a paginated list of files, optionally filtered by a prefix. It utilizes `paginationOptsValidator` for handling pagination parameters. ```typescript import { query } from "./_generated/server"; import { paginationOptsValidator } from "convex/server"; import { v } from "convex/values"; import { fs } from "./fs"; export const listFiles = query({ args: { prefix: v.optional(v.string()), paginationOpts: paginationOptsValidator, }, handler: async (ctx, args) => { return await fs.list(ctx, { prefix: args.prefix, paginationOpts: args.paginationOpts, }); }, }); ``` -------------------------------- ### Upload File by Path using fs.writeFile Source: https://convexfs.dev/guides/filesystem-operations The fs.writeFile() method uploads data and commits it to a specified path in a single operation. It takes the file data (as bytes), the output path, and the MIME type. If a file already exists at the path, it will be overwritten. ```typescript import { action } from "./_generated/server"; import { v } from "convex/values"; import { fs } from "./fs"; export const saveProcessedImage = action({ args: { data: v.bytes(), outputPath: v.string(), }, handler: async (ctx, args) => { await fs.writeFile(ctx, args.outputPath, args.data, "image/webp"); }, }); ``` -------------------------------- ### Copy File using fs.copy Source: https://convexfs.dev/guides/filesystem-operations The fs.copy() method creates a duplicate of a file at a new path, analogous to the Unix 'cp' command. It fails if the source file does not exist or if the destination path already exists. This operation creates a new blob entry for the copied file. ```typescript export const copyFile = mutation({ args: { sourcePath: v.string(), destPath: v.string(), }, handler: async (ctx, args) => { await fs.copy(ctx, args.sourcePath, args.destPath); }, }); ``` -------------------------------- ### fs.transact() - Atomic File Swap Source: https://convexfs.dev/guides/transactions-atomicity Demonstrates performing multiple filesystem operations (move) atomically using fs.transact(). If any operation fails, the entire transaction is rolled back. ```APIDOC ## POST /fs/transact ### Description Executes multiple filesystem operations atomically in sequence. Each operation can depend on the state resulting from previous operations within the same transaction. ### Method POST ### Endpoint `/fs/transact` ### Parameters #### Request Body - **operations** (array) - Required - An array of operations to perform atomically. - **op** (string) - Required - The type of operation (e.g., "delete", "move", "copy"). - **source** (object) - Required - The source file object obtained from `fs.stat()`, including the expected `blobId`. - **dest** (object) - Optional - For "move" and "copy" operations, specifies the destination path and an optional `basis` precondition. - **path** (string) - Required - The destination path. - **basis** (string | null | undefined) - Optional - Precondition for the destination path. ### Request Example ```json { "operations": [ { "op": "move", "source": { "path": "/path/to/fileA.txt", "blobId": "blob-id-A", "size": 500, "mtime": 1678886400000 }, "dest": { "path": "/tmp/swap" } }, { "op": "move", "source": { "path": "/path/to/fileB.txt", "blobId": "blob-id-B", "size": 700, "mtime": 1678886500000 }, "dest": { "path": "/path/to/fileA.txt" } }, { "op": "move", "source": { "path": "/tmp/swap", "blobId": "blob-id-A", "size": 500, "mtime": 1678886400000 }, "dest": { "path": "/path/to/fileB.txt" } } ] } ``` ### Response #### Success Response (200) - **result** (boolean) - Indicates if the transaction was successful. #### Response Example ```json { "result": true } ``` ``` -------------------------------- ### Create File If Not Exists using commitFiles() Source: https://convexfs.dev/guides/transactions-atomicity This mutation creates a file only if it does not already exist. It uses `fs.commitFiles()` with a `basis` of `null` to ensure the file is new. This is useful for idempotent file creation. ```typescript import { mutation } from "convex/server"; import { v } from "convex/values"; import { fs } from "convex-fs"; export const createFileIfNotExists = mutation({ args: { path: v.string(), blobId: v.string(), }, handler: async (ctx, args) => { // Only create if file doesn't exist await fs.commitFiles(ctx, [ { path: args.path, blobId: args.blobId, basis: null }, ]); }, }); ``` -------------------------------- ### List Files with Pagination (React Frontend) Source: https://convexfs.dev/guides/filesystem-operations Uses the `usePaginatedQuery` hook from `convex-fs/react` to fetch and display a paginated list of files in a React component. It handles loading states and provides a 'Load more' button. ```typescript import { usePaginatedQuery } from "convex-fs/react"; import { api } from "../convex/_generated/api"; function FileList() { const { results, status, loadMore } = usePaginatedQuery( api.files.listFiles, {}, // args (prefix is optional) { initialNumItems: 20 }, ); return (
{results.map((file) => (
{file.path} - {file.contentType} ({file.size} bytes)
))} {status === "CanLoadMore" && ( )} {status === "LoadingMore" &&
Loading...
}
); } ``` -------------------------------- ### Generate Download URL with Filename (TypeScript) Source: https://convexfs.dev/guides/cdn-parameters Shows a Convex query function `getDownloadUrl` that generates a download URL including a custom filename. It retrieves file metadata, extracts the filename from the path, and uses `buildDownloadUrl` to pass this filename as an extra CDN parameter. This enables custom download filenames via Bunny.net Edge Rules. ```typescript import { query } from "./_generated/server"; import { v } from "convex/values"; import { buildDownloadUrl } from "convex-fs"; import { fs } from "./fs"; export const getDownloadUrl = query({ args: { path: v.string() }, handler: async (ctx, args) => { const siteUrl = process.env.CONVEX_SITE_URL!; const file = await fs.stat(ctx, args.path); if (!file) { return null; } // Extract filename from path (e.g., "/documents/report.pdf" -> "report.pdf") const filename = args.path.split("/").pop() ?? "download"; return buildDownloadUrl(siteUrl, "/fs", file.blobId, args.path, { filename, }); }, }); ``` -------------------------------- ### Configure ConvexFS component in convex.config.ts Source: https://convexfs.dev/guides/app-setup Add the ConvexFS component to your app by registering it in the `convex.config.ts` file. This involves importing the `fs` module and using `app.use(fs)`. ```typescript import { defineApp } from "convex/server"; import fs from "convex-fs/convex.config.js"; const app = defineApp(); app.use(fs); export default app; ```