core/mix/upload_new_asset_versions.ex

# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved.

defmodule Mix.Tasks.AntikytheraCore.UploadNewAssetVersions do
  @shortdoc "Uploads assets to asset storage so that they can be delivered via CDN"

  @moduledoc """
  #{@shortdoc}.

  This mix task is invoked during gear deployment from within each gear project.
  """

  use Mix.Task
  alias AntikytheraEal.AssetStorage
  alias AntikytheraCore.Mix.AssetList

  def run(_) do
    Mix.Task.run("loadpaths")
    gear_name = Mix.Project.config()[:app]

    case all_assets(gear_name) do
      nil -> :ok
      mapping -> do_run(gear_name, mapping)
    end
  end

  defp all_assets(gear_name) do
    gear_name_camel = gear_name |> Atom.to_string() |> Macro.camelize()
    # the module is not yet loaded, it's inevitable to make a new atom here
    # credo:disable-for-next-line Credo.Check.Warning.UnsafeToAtom
    mod = Module.concat(gear_name_camel, "Asset")

    try do
      {:module, ^mod} = Code.ensure_loaded(mod)
      mod.all()
    rescue
      _nonexisting_module_or_undefined_function -> nil
    end
  end

  defp do_run(gear_name, mapping) do
    try do
      existing_assets = AssetStorage.list(gear_name) |> MapSet.new()
      pairs = Enum.map(mapping, fn {path, url} -> {path, url_to_key(url)} end)

      for {path, key} <- pairs, key not in existing_assets do
        do_upload(path, key)
      end

      keys = Enum.map(pairs, fn {_path, key} -> key end)
      AssetList.write!(gear_name, keys)
    after
      {_, 0} = System.cmd("git", ["clean", "-f"])
      {_, 0} = System.cmd("git", ["checkout", "."])
    end
  end

  defp url_to_key(url) do
    URI.parse(url).path |> String.replace_prefix("/", "")
  end

  defp do_upload(path_from_static, key) do
    {type, sub, _} = :cow_mimetypes.web(path_from_static)
    mime = "#{type}/#{sub}"
    path_from_project_top = Path.join(["priv", "static", path_from_static])
    gzip? = gzip?(mime)
    AssetStorage.upload(path_from_project_top, key, mime, gzip?)
    IO.puts("uploaded #{path_from_project_top} as key=#{key} mime=#{mime} gzip?=#{gzip?}")
  end

  # The following list is taken from CloudFront's documentation:
  # http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html
  mime_types_to_compress = ~W(
    application/eot
    application/font
    application/font-sfnt
    application/javascript
    application/json
    application/opentype
    application/otf
    application/pkcs7-mime
    application/truetype
    application/ttf
    application/vnd.ms-fontobject
    application/x-font-opentype
    application/x-font-truetype
    application/x-font-ttf
    application/x-httpd-cgi
    application/x-javascript
    application/x-mpegurl
    application/x-opentype
    application/x-otf
    application/x-perl
    application/x-ttf
    application/xhtml+xml
    application/xml
    application/xml+rss
    font/eot
    font/opentype
    font/otf
    font/ttf
    image/svg+xml
    text/css
    text/csv
    text/html
    text/javascript
    text/js
    text/plain
    text/richtext
    text/tab-separated-values
    text/x-component
    text/x-java-source
    text/x-script
    text/xml
  )

  Enum.each(mime_types_to_compress, fn mime ->
    defp gzip?(unquote(mime)), do: true
  end)

  defp gzip?(_), do: false
end