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