mirror of
https://github.com/ClaperCo/Claper.git
synced 2026-02-24 12:09:59 +01:00
128 lines
3.7 KiB
Elixir
128 lines
3.7 KiB
Elixir
defmodule SimpleS3Upload do
|
|
@moduledoc """
|
|
Dependency-free S3 Form Upload using HTTP POST sigv4
|
|
|
|
https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-post-example.html
|
|
"""
|
|
|
|
@doc """
|
|
Signs a form upload.
|
|
|
|
The configuration is a map which must contain the following keys:
|
|
|
|
* `:region` - The AWS region, such as "us-east-1"
|
|
* `:access_key_id` - The AWS access key id
|
|
* `:secret_access_key` - The AWS secret access key
|
|
|
|
|
|
Returns a map of form fields to be used on the client via the JavaScript `FormData` API.
|
|
|
|
## Options
|
|
|
|
* `:key` - The required key of the object to be uploaded.
|
|
* `:max_file_size` - The required maximum allowed file size in bytes.
|
|
* `:content_type` - The required MIME type of the file to be uploaded.
|
|
* `:expires_in` - The required expiration time in milliseconds from now
|
|
before the signed upload expires.
|
|
|
|
## Examples
|
|
|
|
config = %{
|
|
region: "us-east-1",
|
|
access_key_id: System.fetch_env!("AWS_ACCESS_KEY_ID"),
|
|
secret_access_key: System.fetch_env!("AWS_SECRET_ACCESS_KEY")
|
|
}
|
|
|
|
{:ok, fields} =
|
|
SimpleS3Upload.sign_form_upload(config, "my-bucket",
|
|
key: "public/my-file-name",
|
|
content_type: "image/png",
|
|
max_file_size: 10_000,
|
|
expires_in: :timer.hours(1)
|
|
)
|
|
|
|
"""
|
|
def sign_form_upload(config, bucket, opts) do
|
|
key = Keyword.fetch!(opts, :key)
|
|
max_file_size = Keyword.fetch!(opts, :max_file_size)
|
|
content_type = Keyword.fetch!(opts, :content_type)
|
|
expires_in = Keyword.fetch!(opts, :expires_in)
|
|
|
|
expires_at = DateTime.add(DateTime.utc_now(), expires_in, :millisecond)
|
|
amz_date = amz_date(expires_at)
|
|
credential = credential(config, expires_at)
|
|
|
|
encoded_policy =
|
|
Base.encode64("""
|
|
{
|
|
"expiration": "#{DateTime.to_iso8601(expires_at)}",
|
|
"conditions": [
|
|
{"bucket": "#{bucket}"},
|
|
["eq", "$key", "#{key}"],
|
|
{"acl": "public-read"},
|
|
["eq", "$Content-Type", "#{content_type}"],
|
|
["content-length-range", 0, #{max_file_size}],
|
|
{"x-amz-server-side-encryption": "AES256"},
|
|
{"x-amz-credential": "#{credential}"},
|
|
{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
|
|
{"x-amz-date": "#{amz_date}"}
|
|
]
|
|
}
|
|
""")
|
|
|
|
fields = %{
|
|
"key" => key,
|
|
"acl" => "public-read",
|
|
"content-type" => content_type,
|
|
"x-amz-server-side-encryption" => "AES256",
|
|
"x-amz-credential" => credential,
|
|
"x-amz-algorithm" => "AWS4-HMAC-SHA256",
|
|
"x-amz-date" => amz_date,
|
|
"policy" => encoded_policy,
|
|
"x-amz-signature" => signature(config, expires_at, encoded_policy)
|
|
}
|
|
|
|
{:ok, fields}
|
|
end
|
|
|
|
defp amz_date(time) do
|
|
time
|
|
|> NaiveDateTime.to_iso8601()
|
|
|> String.split(".")
|
|
|> List.first()
|
|
|> String.replace("-", "")
|
|
|> String.replace(":", "")
|
|
|> Kernel.<>("Z")
|
|
end
|
|
|
|
defp credential(%{} = config, %DateTime{} = expires_at) do
|
|
"#{config.access_key_id}/#{short_date(expires_at)}/#{config.region}/s3/aws4_request"
|
|
end
|
|
|
|
defp signature(config, %DateTime{} = expires_at, encoded_policy) do
|
|
config
|
|
|> signing_key(expires_at, "s3")
|
|
|> sha256(encoded_policy)
|
|
|> Base.encode16(case: :lower)
|
|
end
|
|
|
|
defp signing_key(%{} = config, %DateTime{} = expires_at, service) when service in ["s3"] do
|
|
amz_date = short_date(expires_at)
|
|
%{secret_access_key: secret, region: region} = config
|
|
|
|
("AWS4" <> secret)
|
|
|> sha256(amz_date)
|
|
|> sha256(region)
|
|
|> sha256(service)
|
|
|> sha256("aws4_request")
|
|
end
|
|
|
|
defp short_date(%DateTime{} = expires_at) do
|
|
expires_at
|
|
|> amz_date()
|
|
|> String.slice(0..7)
|
|
end
|
|
|
|
defp sha256(secret, msg), do: :crypto.mac(:hmac, :sha256, secret, msg)
|
|
end
|