Packages

Google Cloud Storage JSON API client library. Stores and retrieves potentially large, immutable data objects.

Current section

Files

Jump to
googly_cloud_storage lib googly cloud_storage request.ex
Raw

lib/googly/cloud_storage/request.ex

# 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 stays lazy with its size read from disk. 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} = stream), do: {stream, 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