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 (3)
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