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 | map, String.t(), String.t(), Keyword.t()) :: {:ok, String.t()} | {:file_upload_error, map | tuple} @callback delete(String.t(), String.t(), atom | map) :: {:ok, map} | {:error, map} @callback public_url(String.t(), String.t(), keyword) :: {: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} | {:error, :invalid_file} def upload(container_name, filename, blob_name, opts \\ []) do force_container = Keyword.get(opts, :force_container, true) connection = Keyword.get(opts, :connection, :default) upload_options = Keyword.take(opts, [:cache_control]) with {:ok, file_mime_type} <- mime_type(filename), {{:ok, file}, _} <- {api_module(connection, File).upload( container_name, connection, filename, blob_name, Keyword.merge(upload_options, content_type: file_mime_type) ), force_container} do {:ok, file} else {{:error, :container_not_found}, true} -> container_options = Keyword.take(opts, [:cors_policy, :public]) api_module(connection, Container).create(container_name, connection, Map.new(container_options)) upload(container_name, filename, blob_name, Keyword.put(opts, :force_container, false)) {{:error, error}, _} -> {:file_upload_error, error} 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 \\ :default) do api_module(connection, File).delete(container_name, filename, connection) 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(), keyword) :: {:ok, String.t()} | {:error, String.t()} def public_url( container_name, file_path, opts ) do options = Keyword.merge( [ start_time: Timex.now(), expire_time: Timex.add(Timex.now(), Timex.Duration.from_days(1)), connection: :default, public: false ], opts ) connection = Keyword.get(options, :connection) api_module(connection, File).public_url(container_name, file_path, options) end def last_modified(file, connection \\ :default) do api_module(connection, 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 defp mime_type(filename) do if File.exists?(filename) do filename |> FileInfo.get_info() |> Map.to_list() |> List.first() |> elem(1) |> to_string() |> case do "text/" <> _ -> {:ok, MIME.from_path(filename)} "application/octet-stream" -> {:ok, MIME.from_path(filename)} mime_type -> {:ok, mime_type} end else {:error, :invalid_file} end end end