Skip to content

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