defmodule Waffle.Storage.Google 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
```
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.
"""
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(%{})
insert(conn(), bucket(definition), path, data(meta), gcs_options)
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.
"""
@spec conn() :: Tesla.Env.client()
def conn() do
{:ok, token} = Goth.fetch(Waffle.Storage.Google.Goth)
Connection.new(token.token)
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) do
object =
%Object{name: name}
|> Map.merge(gcs_options)
Objects.storage_objects_insert_simple(
conn,
bucket,
"multipart",
object,
path
)
end
defp insert(conn, bucket, name, {:binary, data}, acl) do
Objects.storage_objects_insert_iodata(
conn,
bucket,
"multipart",
%Object{name: name, acl: acl},
data
)
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 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