0
# Upload Handling
1
2
File upload processing for multipart form data with disk storage, size limits, and Node.js File interface implementation.
3
4
## Capabilities
5
6
### Create File Upload Handler
7
8
Creates an UploadHandler that saves uploaded files to disk with configurable options for security and performance.
9
10
```typescript { .api }
11
/**
12
* Creates an upload handler that saves files to disk
13
* @param options - Configuration options for file upload handling
14
* @returns UploadHandler function for processing uploaded files
15
*/
16
function unstable_createFileUploadHandler(
17
options?: FileUploadHandlerOptions
18
): UploadHandler;
19
20
interface FileUploadHandlerOptions {
21
/** Avoid file conflicts by appending a count on the end of the filename if it already exists on disk. Defaults to true */
22
avoidFileConflicts?: boolean;
23
/** The directory to write the upload */
24
directory?: string | FileUploadHandlerPathResolver;
25
/** The name of the file in the directory. Can be a relative path, the directory structure will be created if it does not exist */
26
file?: FileUploadHandlerPathResolver;
27
/** The maximum upload size allowed. If the size is exceeded an error will be thrown. Defaults to 3000000B (3MB) */
28
maxPartSize?: number;
29
/** Filter function to determine if a file should be processed */
30
filter?(args: FileUploadHandlerFilterArgs): boolean | Promise<boolean>;
31
}
32
33
type FileUploadHandlerPathResolver = (
34
args: FileUploadHandlerPathResolverArgs
35
) => string | undefined;
36
37
interface FileUploadHandlerFilterArgs {
38
filename: string;
39
contentType: string;
40
name: string;
41
}
42
43
interface FileUploadHandlerPathResolverArgs {
44
filename: string;
45
contentType: string;
46
name: string;
47
}
48
```
49
50
**Usage Examples:**
51
52
```typescript
53
import { unstable_createFileUploadHandler, unstable_parseMultipartFormData } from "@remix-run/node";
54
55
// Basic file upload handler
56
const uploadHandler = unstable_createFileUploadHandler({
57
directory: "/tmp/uploads"
58
});
59
60
// Advanced configuration
61
const uploadHandler = unstable_createFileUploadHandler({
62
directory: ({ name, filename, contentType }) => {
63
// Organize by content type
64
if (contentType.startsWith("image/")) {
65
return "/uploads/images";
66
}
67
return "/uploads/documents";
68
},
69
file: ({ filename }) => {
70
// Generate unique filename
71
const timestamp = Date.now();
72
const ext = filename ? path.extname(filename) : "";
73
return `upload_${timestamp}${ext}`;
74
},
75
maxPartSize: 5 * 1024 * 1024, // 5MB
76
filter: ({ name, filename, contentType }) => {
77
// Only allow specific file types
78
return contentType.startsWith("image/") || contentType === "application/pdf";
79
},
80
avoidFileConflicts: true
81
});
82
83
// Use in a Remix action
84
export async function action({ request }: ActionFunctionArgs) {
85
const formData = await unstable_parseMultipartFormData(
86
request,
87
uploadHandler
88
);
89
90
const file = formData.get("upload") as NodeOnDiskFile;
91
92
if (file) {
93
console.log("Uploaded file:", file.name);
94
console.log("File path:", file.getFilePath());
95
console.log("File size:", file.size);
96
}
97
98
return json({ success: true });
99
}
100
```
101
102
### Node On Disk File
103
104
File implementation for files stored on the Node.js filesystem, providing a complete File interface.
105
106
```typescript { .api }
107
/**
108
* File implementation for files stored on Node.js filesystem
109
*/
110
class NodeOnDiskFile implements Omit<File, "constructor"> {
111
/** The name of the file (basename) */
112
name: string;
113
/** Last modified timestamp (always 0 for new files) */
114
lastModified: number;
115
/** WebKit relative path (always empty string) */
116
webkitRelativePath: string;
117
/** File size in bytes (computed dynamically) */
118
get size(): number;
119
/** MIME type of the file */
120
type: string;
121
/** File prototype for proper type identification */
122
prototype: typeof File.prototype;
123
124
/**
125
* Creates a new NodeOnDiskFile instance
126
* @param filepath - Absolute path to the file on disk (private)
127
* @param type - MIME type of the file
128
* @param slicer - Optional slice configuration for partial file access
129
*/
130
constructor(
131
filepath: string,
132
type: string,
133
slicer?: { start: number; end: number }
134
);
135
136
/**
137
* Creates a new Blob containing a subset of the file's data
138
* @param start - Starting byte offset (optional)
139
* @param end - Ending byte offset (optional)
140
* @param type - MIME type for the new blob (optional)
141
* @returns New Blob with the specified slice
142
*/
143
slice(start?: number, end?: number, type?: string): Blob;
144
145
/**
146
* Reads the entire file into an ArrayBuffer
147
* @returns Promise resolving to file contents as ArrayBuffer
148
*/
149
arrayBuffer(): Promise<ArrayBuffer>;
150
151
/**
152
* Creates a ReadableStream for streaming file contents
153
* @returns ReadableStream for the file
154
*/
155
stream(): ReadableStream;
156
stream(): NodeJS.ReadableStream;
157
stream(): ReadableStream | NodeJS.ReadableStream;
158
159
/**
160
* Reads the entire file as text
161
* @returns Promise resolving to file contents as string
162
*/
163
text(): Promise<string>;
164
165
/**
166
* Removes the file from disk
167
* @returns Promise that resolves when file is deleted
168
*/
169
remove(): Promise<void>;
170
171
/**
172
* Gets the absolute file path on disk
173
* @returns Absolute path to the file
174
*/
175
getFilePath(): string;
176
177
/**
178
* Symbol.toStringTag for proper type identification
179
* @returns "File"
180
*/
181
get [Symbol.toStringTag](): string;
182
}
183
```
184
185
**File Handling Examples:**
186
187
```typescript
188
import { NodeOnDiskFile } from "@remix-run/node";
189
190
// Working with uploaded files
191
export async function action({ request }: ActionFunctionArgs) {
192
const formData = await unstable_parseMultipartFormData(request, uploadHandler);
193
const file = formData.get("document") as NodeOnDiskFile;
194
195
if (file) {
196
// Get file information
197
console.log("File name:", file.name);
198
console.log("File size:", file.size, "bytes");
199
console.log("MIME type:", file.type);
200
console.log("Path on disk:", file.getFilePath());
201
202
// Read file contents
203
const text = await file.text();
204
const buffer = await file.arrayBuffer();
205
206
// Stream file contents
207
const stream = file.stream();
208
209
// Create a slice of the file
210
const firstKB = file.slice(0, 1024);
211
212
// Clean up (remove from disk)
213
await file.remove();
214
}
215
216
return json({ processed: true });
217
}
218
```
219
220
**Security Features:**
221
222
- **File size limits**: Configurable maximum upload size with automatic cleanup on exceed
223
- **Content filtering**: Filter function to validate file types and properties
224
- **Conflict avoidance**: Automatic filename modification to prevent overwrites
225
- **Secure paths**: Path resolution prevents directory traversal attacks
226
- **Cleanup on error**: Automatic file removal if processing fails
227
228
**Performance Features:**
229
230
- **Streaming processing**: Files are processed as streams to handle large uploads
231
- **Lazy loading**: File contents are only read when accessed
232
- **Efficient slicing**: File slicing uses Node.js streams for memory efficiency
233
- **Directory creation**: Automatic creation of necessary directory structure
234
235
**Error Handling:**
236
237
- **MaxPartSizeExceededError**: Thrown when file exceeds size limit
238
- **Automatic cleanup**: Failed uploads are automatically removed from disk
239
- **Graceful degradation**: Returns undefined for filtered or invalid files