The Remix.run documentation shows us how to upload files directly to the disk of the application, although you have to look around a bit.
The code to upload a file to the /public/uploads
directory looks like this.
// ./app/routes/upload.tsx
import {
ActionFunction,
Form,
unstable_createFileUploadHandler,
unstable_parseMultipartFormData,
} from 'remix';
export const fileUploadHandler = unstable_createFileUploadHandler({
directory: './public/uploads',
file: ({ filename }) => filename,
});
export const action: ActionFunction = async ({ request }) => {
const formData = await unstable_parseMultipartFormData(request, fileUploadHandler);
console.log(formData.get('upload')); // will return the filename
return {};
};
const Upload = () => {
return (
<Form method="post" encType="multipart/form-data">
<input type="file" name="upload" />
<button type="submit">upload</button>
</Form>
);
};
export default Upload;
The documentation also has an example to stream your upload to Cloudinary, with a custom uploadHandler
.
But, since I'm using the Google Cloud Platform, I want my files to be stored in a Cloud Storage bucket.
Therefor, I use the @google-cloud/storage package.
My route now looks like this
// ./app/routes/upload.tsx
import { ActionFunction, Form, unstable_parseMultipartFormData, useActionData } from 'remix';
import { cloudStorageUploaderHandler } from '~/services/upload-handler.server';
export const action: ActionFunction = async ({ request }) => {
const formData = await unstable_parseMultipartFormData(request, cloudStorageUploaderHandler);
const filename = formData.get('upload');
return { filename };
};
const Upload = () => {
const actionData = useActionData();
if (actionData && actionData.filename) {
return <>Upload successful.</>;
}
return (
<Form method="post" encType="multipart/form-data">
<input type="file" name="upload" />
<button type="submit">upload</button>
</Form>
);
};
export default Upload;
I created a service that should only run server-side in ./app/services/uploader-handler.server.ts
which looks like this.
import { Readable } from 'stream';
import { Storage } from '@google-cloud/storage';
import { UploadHandler } from 'remix';
const uploadStreamToCloudStorage = async (fileStream: Readable, fileName: string) => {
const bucketName = 'YOUR_BUCKET_NAME';
// Create Cloud Storage client
const cloudStorage = new Storage();
// Create a reference to the file.
const file = cloudStorage.bucket(bucketName).file(fileName);
async function streamFileUpload() {
fileStream.pipe(file.createWriteStream()).on('finish', () => {
// The file upload is complete
});
console.log(`${fileName} uploaded to ${bucketName}`);
}
streamFileUpload().catch(console.error);
return fileName;
};
export const cloudStorageUploaderHandler: UploadHandler = async ({
filename,
stream: fileStream,
}) => {
return await uploadStreamToCloudStorage(fileStream, filename);
};
Et voila, the file from the form, is now directly streamed to Google Cloud Storage.
Top comments (5)
Thank you for this blog and it also helps to me but it is older package now i am using new version for image upload to public /images folder in my application
but it gives app.create.jsx:40 Uncaught (in promise) TypeError: (0 , import_node.unstable_createFileUploadHandler) is not a function error
if anyone having solution about this so please help about this error
thank you in advance :)
This works great! But how do you then access those files? Say I upload an image to the bucket, how do I get the url to that image? What about a pdf?
You can do so by doing this inside of your component
const data = useActionData()
the data contains the response from the server when you upload the file
The upload handler im looking at does not have a Readable stream but rather a AstncIterable
my azure blob wants a readable stream
Sorry for the late reply. I just had to implement this feature again and I have the same issue.
Right now, my code (with Remix v2.2.0) looks like this: