Skip to content

Instantly share code, notes, and snippets.

@kauffmanes
Last active December 14, 2023 01:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kauffmanes/5f16081c5e42c1881cdd758b2047482f to your computer and use it in GitHub Desktop.
Save kauffmanes/5f16081c5e42c1881cdd758b2047482f to your computer and use it in GitHub Desktop.
Upload a file to cloudfront in a Remix application

This gist is meant to show my current attempt at saving a Remix form that includes a file. It's a simplified version of the form. The file (an image) should be uploaded to Cloudfront (which then gets sent to an S3 bucket).

// app/routes/settings.tsx - Remix route

import { 
  unstable_composeUploadHandlers as composeUploadHandlers,
  unstable_createMemoryUploadHandler as createMemoryUploadHandler,
  unstable_parseMultipartFormData as parseMultipartFormData,
} from "@remix-run/node";

import { Form, useActionData } from "@remix-run/react";

export const action = async ({ params, request }: ActionFunctionArgs) => {
  
  const uploadHandler: UploadHandler = composeUploadHandlers(
     async ({ name, data, filename }) => {
      
      // only apply this handler to file
      if (name !== "update-avatar") {
        return undefined;
      }
      
      return await uploadImage({ filename, data });
     },
    createMemoryUploadHandler()
  );
  
  const formData = await parseMultipartFormData(request, uploadHandler);
  
  // these fields are both correct and working
  const displayName = formData.get("display-name");
  const updateAvatar = formData.get("update-avatar");
  
  // this works - db is updated with correct values from form
  await updateUser(data);
  return redirect("/settings");
};

export default function SettingsGeneral() {

  // hook from Blues stack here: https://github.com/remix-run/blues-stack/blob/main/app/utils.ts
  const user = useUser();
  
  return (
    <div>
      <Form method="POST" encType="multipart/form-data">
        <input type="file" name="update-avatar" accept="image/*" />
        <input type="text" name="display-name" defaultValue={user.displayName} />
        <button type="submit">Save profile</Button>
      </Form>
    </div>
  );
}
// app/cloudfront.server.ts

import { getSignedUrl } from "@aws-sdk/cloudfront-signer";
import invariant from "tiny-invariant";


export async function uploadImage({ filename, data }: { filename: string; data: data: AsyncIterable<Uint8Array> }) {
  
  // key matching where it will live in the bucket
  const keyPrefix = `/public/images/${filename}`;
  
  // where to upload (this method works! see below)
  const url = getCFSignedURl(
    `${process.env.CLOUDFRONT_DISTRIBUTION_URL}${keyPrefix}`,
  );
  
  // https://stackoverflow.com/questions/73276404/how-to-convert-async-asynciterableuint8array-to-file-in-javascript
  // https://github.com/remix-run/remix/issues/3238
  // This feels icky.
  let dataArray = [];

  for await (const x of data) {
    dataArray.push(x);
  }

  const blob = new Blob(dataArray, { type: contentType });
  const file = new File([blob], filename, { type: contentType });
  
  const response = await fetch(url, {
    method: "PUT",
    body: data,
  });
  
  if (!response.ok) {
    const error = await response.text();
    console.error("Failed to upload avatar", error);
    throw new Error("Failed to upload avatar");
  }
  
  // this is what we want to save to the db and add the value for formData's `update-avatar`.
  return `${process.env.CLOUDFRONT_DISTRIBUTION_URL}${keyPrefix}`;
  
}

// This function goes and gets a presigned URL from Cloudfront. I have the extra methods 
// enabled. Read https://aws.amazon.com/blogs/aws/amazon-cloudfront-content-uploads-post-put-other-methods for more info.
// This method works. It returns the correct URL to use. Permissions are set up correctly. You can ignore it for the purposes of my plea for help, but this is what's happening in it, in case it matters.
function getCFSignedURl(url: string) {
  invariant(process.env.CLOUDFRONT_PUBLIC_KEY, "Missing CLOUDFRONT_PUBLIC_KEY environment variable");
  invariant(process.env.CLOUDFRONT_PRIVATE_KEY, "Missing CLOUDFRONT_PRIVATE_KEY environment variable");
  
  const params = {
    url,
    keyPairId: process.env.CLOUDFRONT_PUBLIC_KEY,
    privateKey: process.env.CLOUDFRONT_PRIVATE_KEY,
    dateLessThan: "2024-12-31",
  };
  
  return getSignedUrl(params);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment