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
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
}
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 )
The key to store the file under
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'
The key to store the file under
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' )
}
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' )
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 for the list operation An opaque string that allows you to paginate over the keys in storage
If true, include file metadata in the result
The maximum number of files to return
Only return files with keys that start with this prefix
An object with an array of files and an optional cursor property for pagination files
FileKey[] | FileMetadata[]
A list of files. If includeMetadata is true, each item includes full metadata. Otherwise, each item only has a key property.
An opaque string for pagination. If defined, there are more files to list. Pass this back in the options object on the next call.
Types
FileKey
A reference to a file in storage by its key.
interface FileKey {
key : string
}
The key of the file in storage
Metadata about a file in storage.
interface FileMetadata extends FileKey {
key : string
lastModified : number
name : string
size : number
type : string
}
The key of the file in storage
The last modified time of the file (in ms since the Unix epoch)
The size of the file in bytes
The MIME type of the file
ListOptions
Options for listing files in storage.
interface ListOptions {
cursor ?: string
includeMetadata ?: boolean
limit ?: number
prefix ?: string
}
An opaque string that allows you to paginate over the keys in storage
If true, include file metadata in the result
The maximum number of files to return
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 )[]
}
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.
The directory where files are stored. Will be created if it doesn’t exist.
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.
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 } "` ,
},
})
}
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` )