defmodule Waffle.Storage.Google.CloudStorage do
@moduledoc """
The main storage integration for Waffle, this acts primarily as a wrapper
around `Google.Api.Storage.V1`. To use this module with Waffle, simply set
your `:storage` config appropriately:
```elixir
config :waffle, storage: Waffle.Storage.Google.CloudStorage
```
Ensure you have a valid bucket set, either through the configs or as an
environment variable, otherwise all calls will fail. The credentials available
through `Goth` must have the appropriate level of access to the bucket,
otherwise some (or all) calls may fail.
"""
@full_control_scope "https://www.googleapis.com/auth/devstorage.full_control"
alias GoogleApi.Storage.V1.Connection
alias GoogleApi.Storage.V1.Api.Objects
alias GoogleApi.Storage.V1.Model.Object
alias Waffle.Storage.Google.Util
alias Waffle.Types
@type object_or_error :: {:ok, GoogleApi.Storage.V1.Model.Object.t} | {:error, Tesla.Env.t}
@doc """
Put a Waffle file in a Google Cloud Storage bucket.
"""
@spec put(Types.definition, Types.version, Types.meta) :: object_or_error
def put(definition, version, meta) do
path = path_for(definition, version, meta)
acl = definition.acl(version, meta)
gcs_options =
definition
|> get_gcs_options(version, meta)
|> ensure_keyword_list()
|> Keyword.put(:acl, acl)
|> Enum.into(%{})
gcs_optional_params =
definition
|> get_gcs_optional_params(version, meta)
|> ensure_keyword_list()
insert(conn(), bucket(definition), path, data(meta), gcs_options, gcs_optional_params)
end
@doc """
Delete a file from a Google Cloud Storage bucket.
"""
@spec put(Types.definition, Types.version, Types.meta) :: object_or_error
def delete(definition, version, meta) do
Objects.storage_objects_delete(
conn(),
bucket(definition),
path_for(definition, version, meta)
)
end
@doc """
Retrieve the public URL for a file in a Google Cloud Storage bucket. Uses
`Waffle.Storage.Google.UrlV2` by default, which uses v2 signing if a signed
URL is requested, but this can be overriden in the options list or in the
application configs by setting `:url_builder` to any module that imlements the
behavior of `Waffle.Storage.Google.Url`.
"""
@spec url(Types.definition, Types.version, Types.meta, Keyword.t) :: String.t
def url(definition, version, meta, opts \\ []) do
signer = Util.option(opts, :url_builder, Waffle.Storage.Google.UrlV2)
signer.build(definition, version, meta, opts)
end
@doc """
Constructs a new connection object with scoped authentication. If no scope is
provided, the `devstorage.full_control` scope is used as a default.
"""
@spec conn(String.t) :: Tesla.Env.client
def conn(scope \\ @full_control_scope) do
token_store = Application.get_env(:waffle, :token_fetcher, Waffle.Storage.Google.Token.DefaultFetcher)
token_store.get_token(scope)
|> Connection.new()
end
@doc """
Returns the bucket for file uploads.
"""
@spec bucket(Types.definition) :: String.t
def bucket(definition), do: Util.var(definition.bucket())
@doc """
Returns the storage directory **within a bucket** to store the file under.
"""
@spec storage_dir(Types.definition, Types.version, Types.meta) :: String.t
def storage_dir(definition, version, meta) do
version
|> definition.storage_dir(meta)
|> Util.var()
end
@doc """
Returns the full file path for the upload destination.
"""
@spec path_for(Types.definition, Types.version, Types.meta) :: String.t
def path_for(definition, version, meta) do
definition
|> storage_dir(version, meta)
|> Path.join(fullname(definition, version, meta))
end
@doc """
A wrapper for `Waffle.Definition.Versioning.resolve_file_name/3`.
"""
@spec fullname(Types.definition, Types.version, Types.meta) :: String.t
def fullname(definition, version, meta) do
Waffle.Definition.Versioning.resolve_file_name(definition, version, meta)
end
@spec data(Types.file) :: {:file | :binary, String.t}
defp data({%{binary: nil, path: path}, _}), do: {:file, path}
defp data({%{binary: data}, _}), do: {:binary, data}
@spec insert(Tesla.Env.client, String.t, String.t, {:file | :binary, String.t}, String.t) :: object_or_error
defp insert(conn, bucket, name, {:file, path}, gcs_options, gcs_optional_params) do
object = %Object{name: name}
|> Map.merge(gcs_options)
Objects.storage_objects_insert_simple(
conn,
bucket,
"multipart",
object,
path,
gcs_optional_params
)
end
defp insert(conn, bucket, name, {:binary, data}, _gcs_options) do
Util.storage_objects_insert(
conn,
bucket,
[
body: data,
name: name,
]
)
end
defp get_gcs_options(definition, version, {file, scope}) do
try do
apply(definition, :gcs_object_headers, [version, {file, scope}])
rescue
UndefinedFunctionError ->
[]
end
end
defp get_gcs_optional_params(definition, version, {file, scope}) do
try do
apply(definition, :gcs_optional_params, [version, {file, scope}])
rescue
UndefinedFunctionError ->
[]
end
end
defp ensure_keyword_list(list) when is_list(list), do: list
defp ensure_keyword_list(map) when is_map(map), do: Map.to_list(map)
end