Created
March 24, 2016 21:01
Revisions
-
sikanhe created this gist
Mar 24, 2016 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,210 @@ 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