# NOTE: This file is auto generated by googly. Do not edit it manually. defmodule Googly.CloudStorage.Request do @moduledoc false # Builds and executes a request against the API using Req, then hands the # result to `Googly.CloudStorage.Response` for decoding. alias Googly.CloudStorage.Response @base_url "https://storage.googleapis.com/" @reserved [:token, :req, :decode, :content_type] @doc false def run(spec) do {call, params} = Keyword.split(spec[:opts] || [], @reserved) {query, body} = route_params(params, spec[:params] || %{}, spec[:query] || []) {req_params, req_rest} = Keyword.pop(call[:req] || [], :params, []) query = merge_params(query, req_params) url = @base_url |> URI.merge(fill(spec[:url], spec[:path_params] || %{})) |> URI.to_string() media? = media_download?(query) [method: spec[:method], url: url] |> put(:params, query != [] && query) |> put(:json, body) |> put(:auth, call[:token] && {:bearer, call[:token]}) |> maybe_put_raw_body(media?) |> execute(req_rest) |> Response.decode(spec[:decode], maybe_no_decode(call, media?)) end @doc false # Media upload. `data` may be iodata (held in memory) or a `File.Stream` # (e.g. `File.stream!(path)`), which is streamed from disk with a computed # content-length. `uploadType=media` sends the bytes raw; `uploadType=multipart` # wraps JSON metadata and the bytes in a multipart/related body. def upload(spec) do {call, params} = Keyword.split(spec[:opts] || [], @reserved) {query, _body} = route_params(params, spec[:params] || %{}, spec[:query] || []) {req_params, req_rest} = Keyword.pop(call[:req] || [], :params, []) query = merge_params([{"uploadType", spec[:upload_type]} | query], req_params) url = @base_url |> URI.merge(fill(spec[:url], spec[:path_params] || %{})) |> URI.to_string() {body, content_type, size} = build_upload(spec, call) headers = [{"content-type", content_type} | content_length(size)] [method: spec[:method], url: url, params: query, body: body, headers: headers] |> put(:auth, call[:token] && {:bearer, call[:token]}) |> execute(req_rest) |> Response.decode(spec[:decode], call) end # Builds the Req request and applies the remaining caller `:req` overrides with # Req.merge/2 (headers/auth/adapter/etc.). Query params are handled by run/upload # instead — they use string keys (exact wire names) that Req.merge would route # through Keyword.merge/2 and reject. defp execute(options, req_rest) do options |> Req.new() |> Req.merge(req_rest) |> Req.request() end # Combines query params, letting caller params override generated ones by key. defp merge_params(generated, caller) do Enum.reduce(caller, generated, fn {key, value}, acc -> List.keystore(acc, to_string(key), 0, {to_string(key), value}) end) end # Routes each opt to the query string or request body per the endpoint's spec; # unknown opts are ignored. defp route_params(opts, spec, query) do Enum.reduce(opts, {query, nil}, fn {key, value}, {q, b} -> case spec do %{^key => {:query, wire}} -> {[{wire, value} | q], b} %{^key => {:body, _}} -> {q, value} _ -> {q, b} end end) end # Returns `{body, content_type, byte_size}`. Google's simple/multipart uploads # require a content-length (a body without one is rejected with 411), so `data` # must be iodata or a File.Stream — both of known size. defp build_upload(spec, call) do {data, size} = payload(spec[:data]) case spec[:upload_type] do "media" -> {data, content_type(spec, call), size} "multipart" -> multipart(spec, call, data, size) end end defp multipart(spec, call, data, data_size) do boundary = "googly" <> Base.url_encode64(:crypto.strong_rand_bytes(12), padding: false) preamble = [ "--", boundary, "\r\n", "Content-Type: application/json; charset=UTF-8\r\n\r\n", Jason.encode!(spec[:metadata]), "\r\n", "--", boundary, "\r\n", "Content-Type: ", content_type(spec, call), "\r\n\r\n" ] closing = ["\r\n--", boundary, "--"] body = Stream.concat([preamble, data, closing]) size = IO.iodata_length(preamble) + data_size + IO.iodata_length(closing) {body, "multipart/related; boundary=#{boundary}", size} end # Normalises `data` into `{enumerable, byte_size}`. iodata is measured; a # File.Stream's size is read from disk with File.stat!, so its body must yield # exactly those bytes. We re-open the path in raw byte mode rather than trusting # the passed stream: the default `File.stream!/1` uses `:line` mode, which # rewrites newlines and so yields fewer bytes than the on-disk size — the # content-length would then overshoot the body and the upload would fail. # Size-less streams aren't supported — Google's simple/multipart uploads need a # content-length, so stream from a file or read the content into memory. defp payload(%File.Stream{path: path}), do: {File.stream!(path, 64 * 1024, [:raw, :binary]), File.stat!(path).size} defp payload(data) when is_binary(data) or is_list(data), do: {[data], IO.iodata_length(data)} defp payload(other) do raise ArgumentError, "upload data must be iodata or a File.Stream (uploads require a known " <> "content-length); got: #{inspect(other)}" end # Content type of the uploaded bytes: an explicit `:content_type` opt, else the # metadata's `content_type`, else octet-stream. defp content_type(spec, call) do call[:content_type] || (is_map(spec[:metadata]) && Map.get(spec[:metadata], :content_type)) || "application/octet-stream" end defp content_length(size), do: [{"content-length", Integer.to_string(size)}] defp media_download?(query) do Enum.any?(query, fn {k, v} -> k == "alt" and to_string(v) == "media" end) end # Tell Req not to decode the body (raw bytes) on a media download. defp maybe_put_raw_body(options, true), do: Keyword.put(options, :decode_body, false) defp maybe_put_raw_body(options, false), do: options # Tell Response not to build a struct on a media download. defp maybe_no_decode(call, true), do: Keyword.put(call, :decode, false) defp maybe_no_decode(call, false), do: call # Substitutes `{name}` / `{+name}` path templates with their values. defp fill(url, path_params) do Regex.replace(~r/\{\+?([^}]+)\}/, url, fn _, name -> to_string(Map.get(path_params, name, "")) end) end defp put(req, _key, falsy) when falsy in [nil, false], do: req defp put(req, key, value), do: Keyword.put(req, key, value) end