Created
March 24, 2016 21:01
-
-
Save sikanhe/cfc636ea2c8a25ed7c86 to your computer and use it in GitHub Desktop.
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 AWS.S3 do | |
import AWS.Utils | |
@config Application.get_env(:vino, __MODULE__) | |
@allowed_file_types ~w(.jpeg .jpg .png) | |
def base_url(bucket) do | |
"https://#{bucket}.s3.amazonaws.com/" | |
end | |
@doc """ | |
Upload an image using put request to s3 | |
""" | |
def put_object(bucket, binary, key, opts \\ []) do | |
acl = opts[:acl] || "public-read" | |
file_size = byte_size(binary) |> Integer.to_string | |
url = Path.join base_url(bucket), key | |
opts_headers = opts[:headers] || [] | |
headers = [ | |
{"Content-Length", file_size}, | |
{"x-amz-acl", acl}, | |
{"x-amz-content-sha256", "UNSIGNED-PAYLOAD"} | |
] ++ opts_headers | |
headers = Vino.AWS.Auth.build_headers(:put, url, "s3", headers, nil, @config) | |
with :ok <- do_upload(url, binary, headers, [retries: 4]), do: {:ok, key} | |
end | |
def put_object!(bucket, binary, key, opts \\ []) do | |
case put_object(bucket, binary, key, opts) do | |
{:ok, key} -> key | |
{:error, error} -> raise error | |
end | |
end | |
@doc """ | |
Upload a file with retries attempt | |
""" | |
def do_upload(url, binary, headers, [retries: count]) when is_integer(count) do | |
case HTTPoison.put(url, binary, headers, [recv_timeout: :infinity, timeout: :infinity]) do | |
{:error, error} -> | |
IO.inspect error | |
if count > 0 do | |
do_upload(url, binary, headers, [retries: count - 1]) | |
else | |
{:error, error} | |
end | |
{:ok, %{status_code: 200}} -> | |
:ok | |
{:ok, %{status_code: ___, body: body}} -> | |
if count > 0 do | |
do_upload(url, binary, headers, [retries: count - 1]) | |
else | |
{:error, body} | |
end | |
end | |
end | |
def delete_object(bucket, key) do | |
url = Path.join base_url(bucket), key | |
headers = [ | |
{"x-amz-content-sha256", "UNSIGNED-PAYLOAD"} | |
] | |
headers = Vino.AWS.Auth.build_headers(:delete, url, "s3", headers, nil, @config) | |
do_delete(url, headers, [retries: 4]) | |
end | |
def do_delete(url, headers, [retries: count]) do | |
case HTTPoison.delete!(url, headers, [recv_timeout: 20*1000, timeout: 10*1000]) do | |
%{status_code: 204} -> | |
:ok | |
%{status_code: ___, body: body} -> | |
cond do | |
count > 0 -> | |
do_delete(url, headers, [retries: count - 1]) | |
true -> | |
{:error, body} | |
end | |
end | |
end | |
end | |
defmodule AWS.Auth do | |
@moduledoc """ | |
This module contains all the function needed to build a header for | |
"Authorization" field with Amazon Signature Version 4 | |
http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html | |
""" | |
use Timex | |
import AWS.Utils | |
def build_headers(http_method, url, service, headers, body, config) do | |
timestamp = Date.now | |
headers = [ | |
{"host", URI.parse(url).host}, | |
{"x-amz-date", amz_date(timestamp)} | |
] ++ headers | |
auth_header = auth_header( | |
http_method, | |
url, | |
headers, | |
body, | |
service, | |
timestamp, | |
config) | |
[{"Authorization", auth_header}] ++ headers | |
end | |
defp auth_header(http_method, url, headers, body, service, timestamp, config) do | |
signature = signature(http_method, url, headers, body, service, timestamp, config) | |
[ | |
"AWS4-HMAC-SHA256 Credential=", credentials(service, timestamp, config), ",", | |
"SignedHeaders=", signed_headers(headers), ",", | |
"Signature=", signature | |
] | |
|> IO.iodata_to_binary | |
end | |
def signature(http_method, url, headers, body, service, timestamp, config) do | |
build_canonical_request(http_method, url, headers, body) | |
|> string_to_sign(service, timestamp, config) | |
|> sign_aws_s4(config[:secret_key], date(timestamp), config[:region], service) | |
end | |
def build_canonical_request(http_method, url, headers, body) do | |
uri = URI.parse(url) | |
http_method = http_method |> Atom.to_string |> String.upcase | |
query_params = uri.query |> canonical_query_params | |
headers = headers |> canonical_headers | |
header_string = headers | |
|> Enum.map(fn {k, v} -> "#{k}:#{v}" end) | |
|> Enum.join("\n") | |
signed_headers_list = headers | |
|> Keyword.keys | |
|> Enum.join(";") | |
payload = case body do | |
nil -> "UNSIGNED-PAYLOAD" | |
_ -> hash_sha256(body) | |
end | |
[ | |
http_method, "\n", | |
uri_encode(uri.path), "\n", | |
query_params, "\n", | |
header_string, "\n", | |
"\n", | |
signed_headers_list, "\n", | |
payload | |
] |> IO.iodata_to_binary | |
end | |
defp string_to_sign(request, service, timestamp, config) do | |
request = hash_sha256(request) | |
""" | |
AWS4-HMAC-SHA256 | |
#{amz_date(timestamp)} | |
#{scope(service, timestamp, config)} | |
#{request} | |
""" | |
|> String.rstrip | |
end | |
defp signed_headers(headers) do | |
headers | |
|> Enum.map(fn({k, _}) -> String.downcase(k) end) | |
|> Enum.sort(&(&1 < &2)) | |
|> Enum.join(";") | |
end | |
defp canonical_query_params(nil), do: "" | |
defp canonical_query_params(params) do | |
params | |
|> URI.query_decoder | |
|> Enum.sort(fn {k1, _}, {k2, _} -> k1 < k2 end) | |
|> URI.encode_query | |
end | |
defp canonical_headers(headers) do | |
headers | |
|> Enum.map(fn | |
{k, v} when is_binary(v) -> {String.downcase(k), String.strip(v)} | |
{k, v} -> {String.downcase(k), v} | |
end) | |
|> Enum.sort(fn {k1, _}, {k2, _} -> k1 < k2 end) | |
end | |
defp credentials(service, timestamp, config) do | |
"#{config[:access_key]}/#{scope(service, timestamp, config)}" | |
end | |
defp scope(service, timestamp, config) do | |
"#{date(timestamp)}/#{config[:region]}/#{service}/aws4_request" | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment