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.

Top comments (5)

Collapse
 
jignesh_rathod_1038 profile image
Jignesh Rathod • Edited

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

import {
  unstable_composeUploadHandlers,
  unstable_createMemoryUploadHandler,
  unstable_parseMultipartFormData,
  unstable_createFileUploadHandler,
  json,
} from "@remix-run/node"; 
Enter fullscreen mode Exit fullscreen mode

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 :)

Collapse
 
dkeen0123 profile image
Daniel Keen

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?

Collapse
 
dansmog profile image
Daniel Anthony juwon

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

Collapse
 
djacobno profile image
Daniel Jacobsen

The upload handler im looking at does not have a Readable stream but rather a AstncIterable
my azure blob wants a readable stream

Collapse
 
timvermaercke profile image
Tim Vermaercke

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:

# routes/api.upload.tsx

import { Storage } from "@google-cloud/storage";
import {
  ActionFunction,
  UploadHandler,
  UploadHandlerPart,
  unstable_parseMultipartFormData,
  writeAsyncIterableToWritable,
} from "@remix-run/node";

const uploadHandler: UploadHandler = async ({
  data,
  filename,
}: UploadHandlerPart): Promise<string | File | null | undefined> => {
  const bucketName = "BUCKET_NAME";
  const cloudStorageClient = new Storage();
  const destFileName = `${Date.now()}-${filename}`;
  const file = cloudStorageClient.bucket(bucketName).file(destFileName);
  await writeAsyncIterableToWritable(data, file.createWriteStream());

  return destFileName;
};

export const action: ActionFunction = async ({ request }) => {
  const formData = await unstable_parseMultipartFormData(
    request,
    uploadHandler
  );

  return {
    fileName: formData.get("upload"),
  };
};
Enter fullscreen mode Exit fullscreen mode