Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add upload router for signed S3 upload URLs #107

Closed
wants to merge 1 commit into from
Closed

Add upload router for signed S3 upload URLs #107

wants to merge 1 commit into from

Conversation

singingwolfboy
Copy link
Collaborator

Inspired by #101, this pull request implements just a small part of the functionality available there: an API that the frontend can use to get a presigned URL for uploading a file to S3. The API is available at /upload/signedUrl, and returns a JSON result that looks like this:

{
  "signedUrl": "https://my-bucket-name-here.s3.amazonaws.com/avatar.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIIUYHKT2IF5RHTNA%2F20200210%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200210T173539Z&X-Amz-Expires=60&X-Amz-Signature=63ec203cfc1209550e19007aaa85c002cdbe3b1f5174be2770708f3da6769c81&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read",
  "publicUrl": "https://my-bucket-name-here.s3.amazonaws.com/avatar.jpg"
}

The API accepts two query parameters: fileName and contentType. If the preserveFileName option is set, fileName will be used for constructing the key on S3. Note that this option will allow users to overwrite existing files in the S3 bucket!

Open questions:

  • Would it be better to structure this as a GraphQL query rather than a RESTful API endpoint? I considered doing so, but since this query doesn't touch the database at all, I wasn't sure that it was appropriate...
  • How much configuration should go in the @app/config/src/index.ts file, and how much should be written directly into the Javascript that generates the signed URL? This starter is an opinionated project, so I tried to walk the line between the two.
  • What should we do about automated tests?

@rudin
Copy link

rudin commented Feb 12, 2020

I did exactly that, create a plugin which offers an query that returns a signed url:
Might require some modification for more generic usecases (I also generate a secret, which will be the filename of the file hosted at digitalocean).
The advantage of doing it like this is that when firing a mutation, I can directly query for a signed upload url (in the same request).

import aws from "aws-sdk";
import { makeExtendSchemaPlugin, gql } from "graphile-utils";
import uuidv4 from "uuid/v4";

const DO_REGION = "####";
const DO_SPACE = "####";

const fileType = "image/jpeg";

const UploadPlugin = makeExtendSchemaPlugin(build => {
  // Get any helpers we need from `build`
  const { pgSql: sql, inflection } = build;
  // @ts-ignore
  const s3 = new aws.S3({
    endpoint: new aws.Endpoint(`${DO_REGION}.digitaloceanspaces.com`),
    accessKeyId: process.env.DO_ACCESS_KEY_ID,
    secretAccessKey: process.env.DO_SECRET_ACCESS_KEY,
    region: DO_REGION,
    signatureVersion: "v4",
  });
  // client uses secret and url for upload
  return {
    typeDefs: gql`
      type SignedUrl {
        url: String
        secret: String
      }
      extend type Query {
        upload: SignedUrl
      }
    `,
    resolvers: {
      Query: {
        upload: () => {
          const secret = uuidv4()
            .split("-")
            .join("");
          const s3Params = {
            Bucket: DO_SPACE,
            Key: secret,
            ContentType: fileType,
            ACL: "public-read",
            Expires: 60,
          };
          const url = s3.getSignedUrl("putObject", s3Params);
          return { url, secret };
        },
      },
    },
  };
});

Hope this can be of any help.

@benjie
Copy link
Member

benjie commented Feb 24, 2020

Would it be better to structure this as a GraphQL query rather than a RESTful API endpoint?

I'd do it as a GraphQL mutation to get the signed URL rather than a REST endpoint 👍 - @rudin's plugin is very similar to what I'd have done except getting the request URI would be a mutation (you're effectively "creating" a signed URL, and you may want to track it). I'd also probably have made a couple extra things non-nullable and shaped it according to the GraphQL Mutation Input Objects Specification.

How much configuration should go in the @app/config/src/index.ts file, and how much should be written directly into the Javascript that generates the signed URL? This starter is an opinionated project, so I tried to walk the line between the two.

Putting config in @app/config makes sense. I'd probably make the bucket name an envvar as it's likely to differ between development, staging and production.

What should we do about automated tests?

@Banashek
Copy link

Banashek commented Mar 7, 2020

What should we do about automated tests?

I've used localstack for something similar before.

The repository has a test in their suite that might be usable as a reference.

@benjie
Copy link
Member

benjie commented May 15, 2020

Closing in favour of #108

@benjie benjie closed this May 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants