lib/file.ex

defmodule FileStorageApi.File do
  @moduledoc """
  Module for uploading deleting and fetching url of file
  """

  import FileStorageApi.Base

  @type t :: %__MODULE__{name: String.t(), properties: map}
  @callback upload(String.t(), atom, String.t(), String.t()) ::
              {:ok, String.t()} | {:file_upload_error, map | tuple}
  @callback delete(String.t(), String.t(), atom) :: {:ok, map} | {:error, map}
  @callback public_url(String.t(), String.t(), DateTime.t(), DateTime.t(), atom) ::
              {:ok, String.t()} | {:error, String.t()}
  @callback last_modified(t) :: {:ok, DateTime.t()} | {:error, atom}

  defstruct name: nil, properties: %{}

  @doc """
  Function to upload file has input args
  container_name: name of the container
  filename: path to the file with the data to store
  blob_name: how the blob is going to be called after storage

  Option field is available that has options for missing container fallback
  force_container: with false you can disable auto creation of container
  public: with public on true it will create bucket by default as public
  cors_policy: can have true or a configuration for configuring cors settings of bucket

  Returns reference to the file in the asset store
  """
  @spec upload(String.t(), String.t(), String.t(), keyword) ::
          {:ok, String.t()} | {:file_upload_error, map | tuple}
  def upload(container_name, filename, blob_name, opts \\ []) do
    force_container = Keyword.get(opts, :force_container, true)
    connection_name = Keyword.get(opts, :connection, :default)

    case {api_module(connection_name, File).upload(container_name, connection_name, filename, blob_name),
          force_container} do
      {{:ok, file}, _} ->
        {:ok, file}

      {{:error, :container_not_found}, true} ->
        container_options = Keyword.take(opts, [:cors_policy, :public])
        api_module(connection_name, Container).create(container_name, connection_name, Map.new(container_options))
        upload(container_name, filename, blob_name, Keyword.put(opts, :force_container, false))

      {{:error, error}, _} ->
        {:file_upload_error, error}
    end
  end

  @doc """
  Function to delete files

  Has 2 inputs
  container_name: name of container file is stored in
  filename: reference path of the file stored in the container
  """
  @spec delete(String.t(), String.t(), atom) :: {:ok, map} | {:error, map}
  def delete(container_name, filename, connection_name \\ :default) do
    api_module(connection_name, File).delete(container_name, filename, connection_name)
  end

  @doc """
  public_url returns an full url to be able to fetch the file with security tokens needed by default 1 day valid
  """
  @spec public_url(String.t(), String.t(), DateTime.t(), DateTime.t(), atom) ::
          {:ok, String.t()} | {:error, String.t()}
  def public_url(
        container_name,
        file_path,
        start_time \\ Timex.now(),
        expire_time \\ Timex.add(Timex.now(), Timex.Duration.from_days(1)),
        connection_name \\ :default
      ) do
    api_module(connection_name, File).public_url(container_name, file_path, start_time, expire_time, connection_name)
  end

  def last_modified(file, connection_name \\ :default) do
    api_module(connection_name, File).last_modified(file)
  end

  @doc """
  This function will create a temporary file and upload to asset store

  Opts field described at the upload function
  """
  @spec upload_file_from_content(binary, binary, binary | iodata, binary, keyword) ::
          {:ok, String.t()} | {:file_upload_error, map | tuple}
  def upload_file_from_content(filename, container_name, content, blob_name, opts \\ []) do
    Temp.track!()
    {:ok, dir_path} = Temp.mkdir("file-cache")
    file_path = Path.join(dir_path, filename)
    File.write(file_path, content)
    upload(container_name, file_path, blob_name, opts)
  after
    Temp.cleanup()
  end

  @spec sanitize(binary) :: binary
  def sanitize(name) do
    name
    |> String.trim()
    |> Recase.to_kebab()
    |> String.replace(~r/[^0-9a-z\-]/u, "")
    |> String.trim("-")
  end

  def mime_type(filename) do
    filename
    |> FileInfo.get_info()
    |> Map.to_list()
    |> List.first()
    |> elem(1)
    |> to_string()
    |> case do
      "text/" <> _ ->
        MIME.from_path(filename)

      mime_type ->
        mime_type
    end
  end
end