Skip to main content

file-storage

Key/value storage interfaces for server-side File objects. file-storage gives Remix apps one consistent API across local disk and memory backends.

Installation

npm i remix

Core Interface

FileStorage

A key/value interface for storing File objects.
import type { FileStorage } from 'remix/file-storage'

Methods

get
Get a File at the given key.
let file = await storage.get('user-123/avatar.png')
if (file) {
  console.log(file.name) // 'avatar.png'
  console.log(file.type) // 'image/png'
  console.log(file.size) // 12345
}
key
string
required
The key to look up
return
File | null | Promise<File | null>
The file with the given key, or null if no such key exists
set
Put a File in storage at the given key.
let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
await storage.set('user-123/hello.txt', file)
key
string
required
The key to store the file under
file
File
required
The file to store
return
void | Promise<void>
A promise that resolves when the file has been stored
put
Put a File in storage and return a new file backed by this storage.
let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
let storedFile = await storage.put('user-123/hello.txt', file)

// storedFile is backed by storage and can be streamed later
console.log(storedFile.name) // 'hello.txt'
key
string
required
The key to store the file under
file
File
required
The file to store
return
File | Promise<File>
A new File object backed by this storage
has
Check if a file with the given key exists.
if (await storage.has('user-123/avatar.png')) {
  console.log('Avatar exists')
}
key
string
required
The key to look up
return
boolean | Promise<boolean>
true if a file with the given key exists, false otherwise
remove
Remove the file with the given key from storage.
await storage.remove('user-123/old-avatar.png')
key
string
required
The key to remove
return
void | Promise<void>
A promise that resolves when the file has been removed
list
List the files in storage.
// List all files
let result = await storage.list()
console.log(result.files)
// [{ key: "user-123/avatar.png" }, { key: "user-456/photo.jpg" }]

// List with metadata
let resultWithMeta = await storage.list({ includeMetadata: true })
console.log(resultWithMeta.files)
// [
//   {
//     key: "user-123/avatar.png",
//     lastModified: 1737955705270,
//     name: "avatar.png",
//     size: 12345,
//     type: "image/png"
//   }
// ]

// List with prefix
let userFiles = await storage.list({ prefix: 'user-123/' })

// Pagination
let page1 = await storage.list({ limit: 10 })
if (page1.cursor) {
  let page2 = await storage.list({ limit: 10, cursor: page1.cursor })
}
options
ListOptions
Options for the list operation
return
ListResult
An object with an array of files and an optional cursor property for pagination

Types

FileKey

A reference to a file in storage by its key.
interface FileKey {
  key: string
}
key
string
The key of the file in storage

FileMetadata

Metadata about a file in storage.
interface FileMetadata extends FileKey {
  key: string
  lastModified: number
  name: string
  size: number
  type: string
}
key
string
The key of the file in storage
lastModified
number
The last modified time of the file (in ms since the Unix epoch)
name
string
The name of the file
size
number
The size of the file in bytes
type
string
The MIME type of the file

ListOptions

Options for listing files in storage.
interface ListOptions {
  cursor?: string
  includeMetadata?: boolean
  limit?: number
  prefix?: string
}
cursor
string
An opaque string that allows you to paginate over the keys in storage
includeMetadata
boolean
If true, include file metadata in the result
limit
number
The maximum number of files to return
prefix
string
Only return files with keys that start with this prefix

ListResult

The result of listing files in storage.
interface ListResult<T extends ListOptions> {
  cursor?: string
  files: (T extends { includeMetadata: true } ? FileMetadata : FileKey)[]
}
cursor
string
An opaque string for pagination. Pass this back in the options object on the next list() call to get the next page of results.
files
FileKey[] | FileMetadata[]
A list of the files in storage. Type depends on includeMetadata option.

Storage Backends

createFsFileStorage

Creates a FileStorage that is backed by a filesystem directory using node:fs.
import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage('./user-files')

let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
await storage.set('user-123/hello.txt', file)

let retrieved = await storage.get('user-123/hello.txt')
console.log(retrieved?.name) // 'hello.txt'
console.log(retrieved?.type) // 'text/plain'
Important notes:
  • No attempt is made to avoid overwriting existing files, so the directory used should be a new directory solely dedicated to this storage object.
  • Keys have no correlation to file names on disk, so they may be any string including characters that are not valid in file names.
  • Multiple files with the same name may be stored in the same storage object.
directory
string
required
The directory where files are stored. Will be created if it doesn’t exist.
return
FileStorage
A new file storage backed by a filesystem directory

createMemoryFileStorage

Creates a simple, in-memory implementation of the FileStorage interface.
import { createMemoryFileStorage } from 'remix/file-storage/memory'

let storage = createMemoryFileStorage()

let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
await storage.set('greeting', file)

let retrieved = await storage.get('greeting')
console.log(retrieved?.name) // 'hello.txt'
Note: This is useful for testing and development. All file data is lost when the process exits.
return
FileStorage
A new in-memory file storage instance

Examples

Basic File Storage

import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage('./uploads')

// Store a file
let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
let key = 'user-123/documents/hello.txt'
await storage.set(key, file)

// Retrieve the file
let retrieved = await storage.get(key)
if (retrieved) {
  console.log(retrieved.name) // 'hello.txt'
  console.log(retrieved.type) // 'text/plain'
  console.log(await retrieved.text()) // 'hello world'
}

// Remove the file
await storage.remove(key)

Handling File Uploads

import { createFsFileStorage } from 'remix/file-storage/fs'
import { parseFormData } from 'remix/form-data-parser'

let storage = createFsFileStorage('./uploads')

async function handleUpload(request: Request) {
  let formData = await parseFormData(request)
  let uploadedFile = formData.get('avatar')
  
  if (uploadedFile instanceof File) {
    let userId = getUserId(request)
    let key = `${userId}/avatar/${uploadedFile.name}`
    
    // Store the file
    await storage.set(key, uploadedFile)
    
    return Response.json({
      success: true,
      key,
      name: uploadedFile.name,
      size: uploadedFile.size,
    })
  }
  
  return Response.json({ error: 'No file uploaded' }, { status: 400 })
}

Listing User Files

import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage('./uploads')

async function getUserFiles(userId: string) {
  // List all files for a specific user with metadata
  let result = await storage.list({
    prefix: `${userId}/`,
    includeMetadata: true,
    limit: 50,
  })
  
  return result.files.map(file => ({
    key: file.key,
    name: file.name,
    size: file.size,
    type: file.type,
    uploadedAt: new Date(file.lastModified),
  }))
}

let files = await getUserFiles('user-123')
console.log(files)
// [
//   {
//     key: "user-123/avatar/photo.jpg",
//     name: "photo.jpg",
//     size: 12345,
//     type: "image/jpeg",
//     uploadedAt: Date(...)
//   }
// ]

Paginated File Listing

import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage('./uploads')

async function listAllFiles() {
  let allFiles: any[] = []
  let cursor: string | undefined
  
  do {
    let result = await storage.list({
      limit: 100,
      cursor,
      includeMetadata: true,
    })
    
    allFiles.push(...result.files)
    cursor = result.cursor
  } while (cursor)
  
  return allFiles
}

let files = await listAllFiles()
console.log(`Total files: ${files.length}`)

Streaming File Downloads

import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage('./uploads')

async function downloadFile(request: Request) {
  let url = new URL(request.url)
  let key = url.searchParams.get('key')
  
  if (!key) {
    return new Response('Missing key parameter', { status: 400 })
  }
  
  let file = await storage.get(key)
  
  if (!file) {
    return new Response('File not found', { status: 404 })
  }
  
  // Stream the file to the client
  return new Response(file.stream(), {
    headers: {
      'Content-Type': file.type,
      'Content-Length': file.size.toString(),
      'Content-Disposition': `attachment; filename="${file.name}"`,
    },
  })
}

Image Storage with Metadata

import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage('./images')

interface ImageInfo {
  userId: string
  category: 'avatar' | 'gallery' | 'document'
  file: File
}

async function storeImage({ userId, category, file }: ImageInfo) {
  // Generate a unique key with a meaningful structure
  let timestamp = Date.now()
  let key = `${userId}/${category}/${timestamp}-${file.name}`
  
  // Store the file (metadata is preserved automatically)
  let storedFile = await storage.put(key, file)
  
  return {
    key,
    url: `/api/images/${key}`,
    name: storedFile.name,
    size: storedFile.size,
    type: storedFile.type,
  }
}

// Usage
let imageFile = new File([imageData], 'profile.jpg', { type: 'image/jpeg' })
let info = await storeImage({
  userId: 'user-123',
  category: 'avatar',
  file: imageFile,
})

console.log(info)
// {
//   key: "user-123/avatar/1737955705270-profile.jpg",
//   url: "/api/images/user-123/avatar/1737955705270-profile.jpg",
//   name: "profile.jpg",
//   size: 12345,
//   type: "image/jpeg"
// }

Cleanup Old Files

import { createFsFileStorage } from 'remix/file-storage/fs'

let storage = createFsFileStorage('./temp-uploads')

async function cleanupOldFiles(maxAgeMs: number) {
  let cutoffTime = Date.now() - maxAgeMs
  let result = await storage.list({ includeMetadata: true })
  
  let removedCount = 0
  
  for (let file of result.files) {
    if (file.lastModified < cutoffTime) {
      await storage.remove(file.key)
      removedCount++
    }
  }
  
  return removedCount
}

// Remove files older than 7 days
let removed = await cleanupOldFiles(7 * 24 * 60 * 60 * 1000)
console.log(`Removed ${removed} old files`)

Build docs developers (and LLMs) love