DEV Community

Tim Vermaercke
Tim Vermaercke

Posted on

Uploading files to Google Cloud Storage with Remix.run

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

Et voila, the file from the form, is now directly streamed to Google Cloud Storage.

Discussion (0)