defmodule ExAws.Operation.S3 do @moduledoc """ Holds data necessary for an operation on the S3 service. """ defstruct stream_builder: nil, parser: &Function.identity/1, bucket: "", path: "/", http_method: nil, body: "", resource: "", params: %{}, headers: %{}, service: :s3 @type t :: %__MODULE__{} defimpl ExAws.Operation do def perform(operation, config) do body = operation.body headers = operation.headers http_method = operation.http_method {operation, config} = add_bucket_to_path(operation, config) url = operation |> add_resource_to_params() |> ExAws.Request.Url.build(config) hashed_payload = ExAws.Auth.Utils.hash_sha256(body) headers = headers |> Map.put("x-amz-content-sha256", hashed_payload) |> put_content_length_header(body, http_method) |> Map.to_list() ExAws.Request.request(http_method, url, body, headers, config, operation.service) |> ExAws.Request.default_aws_error() |> operation.parser.() end def stream!(%{stream_builder: fun}, config), do: fun.(config) defp put_content_length_header(headers, "", :get), do: headers defp put_content_length_header(headers, body, _) do Map.put(headers, "content-length", byte_size(body) |> Integer.to_string()) end @spec add_bucket_to_path(operation :: ExAws.Operation.S3.t(), config :: map) :: {operation :: ExAws.Operation.S3.t(), config :: map} def add_bucket_to_path(operation, config \\ %{}) def add_bucket_to_path(%{bucket: nil}, _config) do raise "#{__MODULE__}.perform/2 cannot perform operation on `nil` bucket" end def add_bucket_to_path(operation, %{virtual_host: true, host: base_host} = config) do vhost_domain = "#{operation.bucket}.#{base_host}" {put_in(operation.path, ensure_absolute(operation.path)), Map.put(config, :host, vhost_domain)} end def add_bucket_to_path(operation, config) do path = "/#{operation.bucket}#{ensure_absolute(operation.path)}" |> expand_dot() {operation |> Map.put(:path, path), config} end @spec add_resource_to_params(operation :: ExAws.Operation.S3.t()) :: ExAws.Operation.S3.t() def add_resource_to_params(operation) do params = operation.params |> Map.new() |> Map.put(operation.resource, 1) operation |> Map.put(:params, params) end defp ensure_absolute(<<"/", _rest::binary>> = path), do: path defp ensure_absolute(path), do: "/#{path}" # A subset of Elixir's built-in Path.expand/1 - because it's OS-specific # we can't use it to normalise paths with "." or ".." in them (otherwise # it breaks on Windows because the /'s become \'s). defp expand_dot(<<"/", rest::binary>>), do: "/" <> do_expand_dot(rest) defp expand_dot(path), do: do_expand_dot(path) defp do_expand_dot(path), do: do_expand_dot(:binary.split(path, "/", [:global]), []) defp do_expand_dot([".." | t], [_, _ | acc]), do: do_expand_dot(t, acc) defp do_expand_dot([".." | t], []), do: do_expand_dot(t, []) defp do_expand_dot(["." | t], acc), do: do_expand_dot(t, acc) defp do_expand_dot([h | t], acc), do: do_expand_dot(t, ["/", h | acc]) defp do_expand_dot([], []), do: "" defp do_expand_dot([], ["/" | acc]), do: IO.iodata_to_binary(:lists.reverse(acc)) end end