Last active
October 1, 2019 05:54
-
-
Save baldwindavid/0516131d9a768d57c96af0fd98206e7d to your computer and use it in GitHub Desktop.
Some methods to generate fields to be used for direct s3 upload
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule S3.DirectUpload do | |
import ExAws.Auth.Utils, only: [amz_date: 1] | |
import ExAws.S3.Utils | |
@default_max_size_kilobytes 5_242_880 | |
@default_expiry_seconds 3600 | |
@type canned_acl :: | |
:private | |
| :public_read | |
| :public_read_write | |
| :authenticated_read | |
| :bucket_owner_read | |
| :bucket_owner_full_control | |
@type presigned_fields_opts :: [ | |
{:expires_in, integer} | |
| {:acl, canned_acl()} | |
| {:policy_conditions_func, function()} | |
| {:added_fields, map()} | |
| {:max_size, integer} | |
] | |
@doc """ | |
Generate the endpoint url for Browser-Based Uploads. | |
""" | |
@spec browser_upload_endpoint(config :: map(), bucket :: binary()) :: binary() | |
def browser_upload_endpoint(config, bucket) do | |
config.scheme <> bucket <> "." <> config.host | |
end | |
@doc """ | |
Generate a map containing pre-signed values for S3 Uploads directly from the browser. | |
These fields are based on the [AWS Signature Version 4](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-authentication-HTTPPOST.html) | |
Optional paramenters: | |
:expires_in - Defines the expiration time of the signature (default is 3600) | |
:acl - Accessibility of the object (default is :private) | |
:policy_conditions_func - A function that takes the fields (sans policy and | |
x-amz-signature since those are calculated based upon said policy) | |
and generates a policy to ensure the fields do not get | |
tampered with. By default, this function is set call `default_policy_conditions`. | |
That default method requires that the bucket matches exactly as specified. | |
It also requires that all but the last segment of the key path matches | |
what is specified. This means that someone can't change the form field to | |
upload to an unexpected path. It also uses the max_size option to limit file | |
sizes. All other fields require an exact match. | |
A custom function can be passed for special requirements. This function will | |
receive the bucket name, fields, and max_size as arguments. Options and matchers can | |
be referenced at https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html | |
:added_fields - A number of fields will always be set automatically by this | |
method (key, acl, x-amz-algorithm, x-amz-credential, and x-amz-date). AWS | |
supports multiple additional fields such as success_action_status, Content-Type, | |
etc. These are not consistently named so need to be passed | |
here as a map with string keys. They should exactly match the key names listed | |
at https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTForms.html | |
The defaults for key, acl, etc. should be fine for most cases, but if they | |
are not, this option allows overriding those options. | |
Example | |
config = ExAws.Config.new(:s3) | |
bucket = "my-bucket" | |
fields = | |
Utilities.S3Direct.presigned_fields( | |
config, | |
bucket, | |
"some/path/to/${filename}", | |
added_fields: %{"success_action_status" => "201"}, | |
max_size: 10_000_000 | |
) | |
""" | |
@spec presigned_fields( | |
config :: map(), | |
bucket :: binary, | |
key :: binary, | |
opts :: presigned_fields_opts | |
) :: map() | |
def presigned_fields(config, bucket, key, opts \\ []) do | |
expires_in = Keyword.get(opts, :expires_in, @default_expiry_seconds) | |
acl = Keyword.get(opts, :acl, :private) | |
added_fields = Keyword.get(opts, :added_fields, %{}) | |
max_size = Keyword.get(opts, :max_size, @default_max_size_kilobytes) | |
policy_conditions_func = | |
Keyword.get(opts, :policy_conditions_func, &default_policy_conditions/3) | |
timestamp = :calendar.universal_time() | |
fields = | |
%{ | |
"key" => key, | |
"acl" => normalize_param(acl), | |
"x-amz-algorithm" => "AWS4-HMAC-SHA256", | |
"x-amz-credential" => | |
ExAws.Auth.Credentials.generate_credential_v4("s3", config, timestamp), | |
"x-amz-date" => amz_date(timestamp) | |
} | |
|> Map.merge(added_fields) | |
policy = | |
gen_policy(config, policy_conditions_func, fields, timestamp, expires_in, bucket, max_size) | |
signature = ExAws.Auth.Signatures.generate_signature_v4("s3", config, timestamp, policy) | |
fields | |
|> Map.put("policy", policy) | |
|> Map.put("x-amz-signature", signature) | |
end | |
def gen_policy(config, policy_conditions_func, fields, timestamp, expires_in, bucket, max_size) do | |
policy_conditions = policy_conditions_func.(fields, bucket, max_size) | |
policy_expiration = | |
timestamp_plus(timestamp, expires_in) | |
|> NaiveDateTime.from_erl!() | |
|> NaiveDateTime.to_iso8601() | |
policy = %{ | |
expiration: Enum.join([policy_expiration, "Z"]), | |
conditions: policy_conditions | |
} | |
policy | |
|> config.json_codec.encode!() | |
|> Base.encode64() | |
end | |
defp default_policy_conditions(fields, bucket, max_size) do | |
fields | |
|> Enum.reject(fn {k, v} -> k == "key" end) | |
|> Enum.map(fn {k, v} -> %{k => v} end) | |
|> Kernel.++([ | |
%{"bucket" => bucket}, | |
["starts-with", "$key", key_path_for(fields["key"])], | |
["content-length-range", 1, max_size] | |
]) | |
end | |
defp key_path_for(key) do | |
key | |
|> String.split("/") | |
|> Enum.reverse() | |
|> tl() | |
|> Enum.reverse() | |
|> Enum.join("/") | |
|> (&(&1 <> "/")).() | |
end | |
def timestamp_plus(timestamp, additional) do | |
timestamp | |
|> :calendar.datetime_to_gregorian_seconds() | |
|> Kernel.+(additional) | |
|> :calendar.gregorian_seconds_to_datetime() | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment