lib/honu_web/storage/s3.ex

defmodule HonuWeb.Storage.S3 do
  alias Honu.Attachments.Blob
  alias HonuWeb.Storage

  @behaviour Storage

  @impl true
  def url(_conn, blob = %Blob{}, opts \\ []) do
    private_url(blob, opts)
  end

  #defp public_url(blob, opts) do
  #end

  defp private_url(blob, opts) do
    presigned_url(blob, opts)
  end

  defp presigned_url(blob, _opts) do
    metadata = AWS.S3.metadata()

    client =
      Honu.Storage.S3.client()
      |> Map.put(:service, metadata.signing_name)

    headers = []
    query = []

    host = build_host(client, metadata, headers)
    path = blob.key

    url =
      client
      |> build_uri(host, path)
      |> add_query(query, client)
      |> to_string()

    #headers = AWS.Signature.sign_v4(client, now(), "get", url, headers, "")

    now = now()
    :aws_signature.sign_v4_query_params(
      client.access_key_id,
      client.secret_access_key,
      client.region,
      client.service,
      {{now.year, now.month, now.day}, {now.hour, now.minute, now.second}},
      "get",
      url,
      [
        uri_encode_path: false,
        body_digest: "UNSIGNED-PAYLOAD",
        ttl: ttl()
      ]
    )
  end

  defp ttl do
    case HonuWeb.Storage.config(:service_urls_expire_in) do
      {:ok, value} when is_integer(value) -> value
      _ -> 300
    end
  end

  # https://github.com/aws-beam/aws-elixir/blob/master/lib/aws/request.ex

  defp now do
    NaiveDateTime.utc_now()
    |> NaiveDateTime.truncate(:second)
  end

  #defp build_host(%AWS.Client{} = client, %AWS.ServiceMetadata{} = metadata, headers) do
  defp build_host(client, metadata, headers) do
    build_options = %{
      region: client.region,
      endpoint: client.endpoint,
      service: metadata.signing_name,
      global?: metadata.global?,
      endpoint_prefix: metadata.endpoint_prefix,
      account_id: :proplists.get_value("x-amz-account-id", headers, nil)
    }

    case build_options do
      %{region: "local", endpoint: nil} ->
        "localhost"

      %{endpoint: endpoint} when is_binary(endpoint) ->
        endpoint

      %{endpoint: endpoint_fun} when is_function(endpoint_fun, 1) ->
        endpoint_fun.(build_options)

      %{global?: true, endpoint: endpoint} ->
        endpoint = resolve_endpoint_sufix(endpoint)

        build_final_endpoint([metadata.endpoint_prefix, endpoint], build_options)

      %{endpoint: endpoint} ->
        endpoint = resolve_endpoint_sufix(endpoint)

        build_final_endpoint([metadata.endpoint_prefix, client.region, endpoint], build_options)
    end
  end

  defp resolve_endpoint_sufix({:keep_prefixes, sufix}) when is_binary(sufix) do
    sufix
  end

  defp resolve_endpoint_sufix(nil), do: AWS.Client.default_endpoint()

  defp build_final_endpoint(parts, options) do
    parts =
      if options.endpoint_prefix == "s3-control" do
        account_id = options.account_id || raise "missing account_id"
        [account_id | parts]
      else
        parts
      end

    Enum.join(parts, ".")
  end

  #defp build_uri(%AWS.Client{} = client, host, path) do
  defp build_uri(client, host, path) do
    bucket = Honu.Storage.config!(:s3_bucket)
    URI.merge("#{client.proto}://#{bucket}.#{host}:#{client.port}", path)
  end

  defp add_query(uri = %URI{}, [], _client) do
    uri
  end

  defp add_query(uri = %URI{}, query, client) do
    querystring = AWS.Client.encode!(client, query, :query)

    if is_binary(uri.query) do
      %{uri | query: uri.query <> "&" <> querystring}
    else
      %{uri | query: querystring}
    end
  end
end