lib/waffle/actions/url.ex

defmodule Waffle.Actions.Url do
  @moduledoc ~S"""
  Url generation.

  Saving your files is only the first half of any decent storage
  solution.  Straightforward access to your uploaded files is equally
  as important as storing them in the first place.

  Often times you will want to regain access to the stored files.  As
  such, `Waffle` facilitates the generation of urls.

      # Given some user record
      user = %{id: 1}

      Avatar.store({%Plug.Upload{}, user}) #=> {:ok, "selfie.png"}

      # To generate a regular, unsigned url (defaults to the first version):
      Avatar.url({"selfie.png", user})
      #=> "https://bucket.s3.amazonaws.com/uploads/1/original.png"

      # To specify the version of the upload:
      Avatar.url({"selfie.png", user}, :thumb)
      #=> "https://bucket.s3.amazonaws.com/uploads/1/thumb.png"

      # To generate a signed url:
      Avatar.url({"selfie.png", user}, :thumb, signed: true)
      #=> "https://bucket.s3.amazonaws.com/uploads/1/thumb.png?AWSAccessKeyId=AKAAIPDF14AAX7XQ&Signature=5PzIbSgD1V2vPLj%2B4WLRSFQ5M%3D&Expires=1434395458"

      # To generate urls for all versions:
      Avatar.urls({"selfie.png", user})
      #=> %{original: "https://.../original.png", thumb: "https://.../thumb.png"}

  **Default url**

  In cases where a placeholder image is desired when an uploaded file
  is not present, Waffle allows the definition of a default image to
  be returned gracefully when requested with a `nil` file.

      def default_url(version) do
        MyApp.Endpoint.url <> "/images/placeholders/profile_image.png"
      end

      Avatar.url(nil) #=> "http://example.com/images/placeholders/profile_image.png"
      Avatar.url({nil, scope}) #=> "http://example.com/images/placeholders/profile_image.png"

  **Virtual Host**

  To support AWS regions other than US Standard, it may be required to
  generate urls in the
  [`virtual_host`](http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html)
  style.  This will generate urls in the style:
  `https://#{bucket}.s3.amazonaws.com` instead of
  `https://s3.amazonaws.com/#{bucket}`.

  To use this style of url generation, your bucket name must be DNS
  compliant.

  This can be enabled with:

      config :waffle,
        virtual_host: true

  > When using virtual hosted–style buckets with SSL, the SSL wild card certificate only matches buckets that do not contain periods. To work around this, use HTTP or write your own certificate verification logic.

  **Asset Host**

  You may optionally specify an asset host rather than using the
  default `bucket.s3.amazonaws.com` format.

  In your application configuration, you'll need to provide an `asset_host` value:

      config :waffle,
        asset_host: "https://d3gav2egqolk5.cloudfront.net", # For a value known during compilation
        asset_host: {:system, "ASSET_HOST"} # For a value not known until runtime

  """

  alias Waffle.Actions.Url
  alias Waffle.Definition.Versioning

  defmacro __using__(_) do
    quote do
      def urls(file, options \\ []) do
        Enum.into __MODULE__.__versions, %{}, fn(r) ->
          {r, __MODULE__.url(file, r, options)}
        end
      end

      def url(file), do: url(file, nil)
      def url(file, options) when is_list(options), do: url(file, nil, options)
      def url(file, version), do: url(file, version, [])
      def url(file, version, options), do: Url.url(__MODULE__, file, version, options)

      defoverridable [{:url, 3}]
    end
  end

  # Apply default version if not specified
  def url(definition, file, nil, options),
    do: url(definition, file, Enum.at(definition.__versions, 0), options)

  # Transform standalone file into a tuple of {file, scope}
  def url(definition, file, version, options) when is_binary(file) or is_map(file) or is_nil(file),
    do: url(definition, {file, nil}, version, options)

  # Transform file-path into a map with a file_name key
  def url(definition, {file, scope}, version, options) when is_binary(file) do
    url(definition, {%{file_name: file}, scope}, version, options)
  end

  def url(definition, {file, scope}, version, options) do
    build(definition, version, {file, scope}, options)
  end

  #
  # Private
  #

  defp build(definition, version, {nil, scope}, _options) do
    definition.default_url(version, scope)
  end

  defp build(definition, version, file_and_scope, options) do
    case Versioning.resolve_file_name(definition, version, file_and_scope) do
      nil -> nil
      _ ->
        definition.__storage.url(definition, version, file_and_scope, options)
    end
  end
end