CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-remix-run--node

Node.js platform abstractions and utilities for Remix applications

Pending
Overview
Eval results
Files

upload-handling.mddocs/

Upload Handling

File upload processing for multipart form data with disk storage, size limits, and Node.js File interface implementation.

Capabilities

Create File Upload Handler

Creates an UploadHandler that saves uploaded files to disk with configurable options for security and performance.

/**
 * Creates an upload handler that saves files to disk
 * @param options - Configuration options for file upload handling
 * @returns UploadHandler function for processing uploaded files
 */
function unstable_createFileUploadHandler(
  options?: FileUploadHandlerOptions
): UploadHandler;

interface FileUploadHandlerOptions {
  /** Avoid file conflicts by appending a count on the end of the filename if it already exists on disk. Defaults to true */
  avoidFileConflicts?: boolean;
  /** The directory to write the upload */
  directory?: string | FileUploadHandlerPathResolver;
  /** The name of the file in the directory. Can be a relative path, the directory structure will be created if it does not exist */
  file?: FileUploadHandlerPathResolver;
  /** The maximum upload size allowed. If the size is exceeded an error will be thrown. Defaults to 3000000B (3MB) */
  maxPartSize?: number;
  /** Filter function to determine if a file should be processed */
  filter?(args: FileUploadHandlerFilterArgs): boolean | Promise<boolean>;
}

type FileUploadHandlerPathResolver = (
  args: FileUploadHandlerPathResolverArgs
) => string | undefined;

interface FileUploadHandlerFilterArgs {
  filename: string;
  contentType: string;
  name: string;
}

interface FileUploadHandlerPathResolverArgs {
  filename: string;
  contentType: string;
  name: string;
}

Usage Examples:

import { unstable_createFileUploadHandler, unstable_parseMultipartFormData } from "@remix-run/node";

// Basic file upload handler
const uploadHandler = unstable_createFileUploadHandler({
  directory: "/tmp/uploads"
});

// Advanced configuration
const uploadHandler = unstable_createFileUploadHandler({
  directory: ({ name, filename, contentType }) => {
    // Organize by content type
    if (contentType.startsWith("image/")) {
      return "/uploads/images";
    }
    return "/uploads/documents";
  },
  file: ({ filename }) => {
    // Generate unique filename
    const timestamp = Date.now();
    const ext = filename ? path.extname(filename) : "";
    return `upload_${timestamp}${ext}`;
  },
  maxPartSize: 5 * 1024 * 1024, // 5MB
  filter: ({ name, filename, contentType }) => {
    // Only allow specific file types
    return contentType.startsWith("image/") || contentType === "application/pdf";
  },
  avoidFileConflicts: true
});

// Use in a Remix action
export async function action({ request }: ActionFunctionArgs) {
  const formData = await unstable_parseMultipartFormData(
    request,
    uploadHandler
  );
  
  const file = formData.get("upload") as NodeOnDiskFile;
  
  if (file) {
    console.log("Uploaded file:", file.name);
    console.log("File path:", file.getFilePath());
    console.log("File size:", file.size);
  }
  
  return json({ success: true });
}

Node On Disk File

File implementation for files stored on the Node.js filesystem, providing a complete File interface.

/**
 * File implementation for files stored on Node.js filesystem
 */
class NodeOnDiskFile implements Omit<File, "constructor"> {
  /** The name of the file (basename) */
  name: string;
  /** Last modified timestamp (always 0 for new files) */
  lastModified: number;
  /** WebKit relative path (always empty string) */
  webkitRelativePath: string;
  /** File size in bytes (computed dynamically) */
  get size(): number;
  /** MIME type of the file */
  type: string;
  /** File prototype for proper type identification */
  prototype: typeof File.prototype;

  /**
   * Creates a new NodeOnDiskFile instance
   * @param filepath - Absolute path to the file on disk (private)
   * @param type - MIME type of the file
   * @param slicer - Optional slice configuration for partial file access
   */
  constructor(
    filepath: string,
    type: string,
    slicer?: { start: number; end: number }
  );

  /**
   * Creates a new Blob containing a subset of the file's data
   * @param start - Starting byte offset (optional)
   * @param end - Ending byte offset (optional)
   * @param type - MIME type for the new blob (optional)
   * @returns New Blob with the specified slice
   */
  slice(start?: number, end?: number, type?: string): Blob;

  /**
   * Reads the entire file into an ArrayBuffer
   * @returns Promise resolving to file contents as ArrayBuffer
   */
  arrayBuffer(): Promise<ArrayBuffer>;

  /**
   * Creates a ReadableStream for streaming file contents
   * @returns ReadableStream for the file
   */
  stream(): ReadableStream;
  stream(): NodeJS.ReadableStream;
  stream(): ReadableStream | NodeJS.ReadableStream;

  /**
   * Reads the entire file as text
   * @returns Promise resolving to file contents as string
   */
  text(): Promise<string>;

  /**
   * Removes the file from disk
   * @returns Promise that resolves when file is deleted
   */
  remove(): Promise<void>;

  /**
   * Gets the absolute file path on disk
   * @returns Absolute path to the file
   */
  getFilePath(): string;

  /**
   * Symbol.toStringTag for proper type identification
   * @returns "File"
   */
  get [Symbol.toStringTag](): string;
}

File Handling Examples:

import { NodeOnDiskFile } from "@remix-run/node";

// Working with uploaded files
export async function action({ request }: ActionFunctionArgs) {
  const formData = await unstable_parseMultipartFormData(request, uploadHandler);
  const file = formData.get("document") as NodeOnDiskFile;
  
  if (file) {
    // Get file information
    console.log("File name:", file.name);
    console.log("File size:", file.size, "bytes");
    console.log("MIME type:", file.type);
    console.log("Path on disk:", file.getFilePath());
    
    // Read file contents
    const text = await file.text();
    const buffer = await file.arrayBuffer();
    
    // Stream file contents
    const stream = file.stream();
    
    // Create a slice of the file
    const firstKB = file.slice(0, 1024);
    
    // Clean up (remove from disk)
    await file.remove();
  }
  
  return json({ processed: true });
}

Security Features:

  • File size limits: Configurable maximum upload size with automatic cleanup on exceed
  • Content filtering: Filter function to validate file types and properties
  • Conflict avoidance: Automatic filename modification to prevent overwrites
  • Secure paths: Path resolution prevents directory traversal attacks
  • Cleanup on error: Automatic file removal if processing fails

Performance Features:

  • Streaming processing: Files are processed as streams to handle large uploads
  • Lazy loading: File contents are only read when accessed
  • Efficient slicing: File slicing uses Node.js streams for memory efficiency
  • Directory creation: Automatic creation of necessary directory structure

Error Handling:

  • MaxPartSizeExceededError: Thrown when file exceeds size limit
  • Automatic cleanup: Failed uploads are automatically removed from disk
  • Graceful degradation: Returns undefined for filtered or invalid files

Install with Tessl CLI

npx tessl i tessl/npm-remix-run--node

docs

cookie-session.md

global-polyfills.md

index.md

server-runtime.md

session-storage.md

stream-utilities.md

upload-handling.md

tile.json