File Upload
Loading "Intro to File Upload"
Run locally for transcripts
A lot of the data users submit to us is fine in text form. But there are some
things that aren't represented by text very well. For example, images, videos,
documents, and audio files. For these, we need to use a different type of input.
To that end, the
file
input type
was introduced in HTML in version 4.01 in December 1999.The
<input type="file" />
element is an essential part of the HTML
specification that enables interaction with the file system on a user's device.
This element creates a user interface that allows users to select one or
multiple files from their system. These files can then be uploaded to a server
or manipulated client-side using JavaScript. The files selected are represented
in a FileList
object, which is a simple list of File
objects.Here's a simple example of its usage:
<form action="/upload" method="post" enctype="multipart/form-data">
<label for="file-upload-input">Upload File</label>
<input type="file" id="file-upload-input" name="file-upload" />
<button type="submit">Upload File</button>
</form>
In this case, when a file is chosen, it will be included in the form data upon
submission and then can be processed server-side. The
enctype
attribute is set
to "multipart/form-data" to ensure the file data is sent correctly to the
server (when it's unset, the encoding type is
"application/x-www-form-urlencoded"). This means, if you already have some
server code that's processing a form submission, you may have to update it to
account for the new encoding type if you wish to start accepting files. This is
because files are uploaded as binary data and not as plaintext.For multiple file selection, you simply add the
multiple
attribute:<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" id="file-upload" name="file-upload" multiple />
<input type="submit" value="Upload File" />
</form>
Once a file or files have been selected, they can be accessed via JavaScript using
the
files
property on the file input element, which returns a FileList
object.let fileList = document.getElementById('file-upload').files
Each
File
object within the FileList
contains properties such as name
,
size
, type
, which represents the MIME type, and lastModified
. These files
can then be read and manipulated using the FileReader
API.Keep in mind that due to privacy concerns, JavaScript in the browser doesn't
have full access to read and write to the file system. The
<input type="file" />
element, along with the File
, FileList
, and
FileReader
APIs, provides a secure way of accessing the file system for the
purposes of reading file data, uploading files, or manipulating files
client-side.You can also use the
accept
attribute to specify the types of files that can
be uploaded. This is a comma-separated list of MIME types or file extensions.
For example, to only allow images to be uploaded, you can use the following:<input type="file" id="file-upload" name="file-upload" accept="image/*" />
On the server
Let's say you're uploading a file that's 1GB in size. That's a lot of data to
send over the wire. So, the browser will split the file into chunks and send
them over the wire one at a time. This is called a
stream.
The server will then receive these chunks one at a time and then reassemble them
into the original file.
Another challenge is the fact that while we can easily store
formData
in
memory, storing a 1GB file in memory is not a good idea. Instead, we want to
hot-potato that file out of memory to somewhere else. This is where you need to
make a decision. Do you have a persistent file system you can write to (if
you're using serverless, the answer is kinda, but not really). Would you prefer
to send it off to a file host like S3? Or do you want to store it in a database?Each of these has its own implications, but for all of them, you'll simply proxy
the stream chunks from the client to the destination so you never have the
entire file in memory at once.
But maybe your files aren't that big. If they're just a couple MBs, you can get
away with storing those in memory for a short period of time. This is a fair bit
simpler, but you do still need to deal with the stream of data to construct the
file on the server.
In Remix
Remix provides a convenient way to handle file uploads. Remix strives to focus
on the web platform, which is why so far we've just used the platform standard
request.formData()
API for parsing the form. Unfortunately, parsing file
submissions is a little more involved. Luckily, it's just a little bit more
involved thanks to the utilities provided by Remix.These packages all have the
unstable_
prefix which means while the use case
is important to Remix, the specifics of the API could change. These should be
made official in the very near future and I don't personally expect any API
changes at this point.unstable_parseMultipartFormData
:
This is the utility that allows you to turn the stream of data and turn it into
a FormData
object. This is the same object you get from request.formData()
,
but the bit that represents the file will depend on the "uploadHandler" you use.unstable_createFileUploadHandler
:
This is a "uploadHandler" that you can use with
unstable_parseMultipartFormData
which will stream the file to disk and give
you back a path to that file with some other meta data.unstable_createMemoryUploadHandler
:
This is a "uploadHandler" that you can use with
unstable_parseMultipartFormData
which will store the file in memory and give
you back a web standard
File
object. This is
only suitable for small files and you should definitely use the maxPartSize
option to limit the size of files it will load into your server's memory.Here's how you could use the memory upload handler (copied from the Remix docs):
import {
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
unstable_parseMultipartFormData as parseMultipartFormData,
} from '@remix-run/node'
export const action = async ({ request }: ActionArgs) => {
const uploadHandler = createMemoryUploadHandler({
maxPartSize: 1024 * 1024 * 5, // 5 MB
})
const formData = await parseMultipartFormData(request, uploadHandler)
const file = formData.get('avatar')
// file is a "File" (https://mdn.io/File) polyfilled for node
// ... etc
}
Custom upload handlers can be created as well. And you can combine these using
the
unstable_composeUploadHandlers
utility which allows you to treat each
file as its own upload and use a different upload handler for each file.
Creating custom handlers is outside the scope of this workshop, but you can find
examples in the Remix examples repo of
uploading to S3
and cloudinary.Conform
Conform has full support for validating file uploads, there are some caveats,
but it's pretty straightforward. See
the Conform File Upload guide for details.