Bow.Ecto behaviour (Bow v0.4.1) View Source

Integration of Bow.Uploader with Ecto.Schema

Usage

# Add `use Bow.Ecto` to the uploader
defmodule MyApp.UserAvatarUploader do
  use Bow.Uploader
  use Bow.Ecto # <---- HERE

  # file.scope will be the user struct
  def store_dir(file) do
    "users/#{file.scope.id}/avatar"
  end
end

# add avatar field to users table
defmodule MyApp.Repo.Migrations.AddAvatarToUsers do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :avatar, :string
    end
  end
end


# use `MyApp.UserAvatarUploader.Type` as field type
defmodule MyApp.User do
  schema "users" do
    field :email, :string
    field :avatar, MyApp.UserAvatarUploader.Type <-- HERE
  end

  def changeset(model \ %__MODULE__{}, params) do
    model
    |> cast(params, [:email, :avatar])
    |> Bow.Ecto.validate() # <---- HERE
  end
end


# create user and save files
changeset = User.changeset(%User{}, params)
with {:ok, user}    <- Repo.insert(changeset),
     {:ok, user, _} <- Bow.Ecto.store(user) do
  {:ok, user}
end

Link to this section Summary

Callbacks

Customize incoming file.

Functions

Download remote files for given fields, i.e. params["remote_avatar_url"] = "http://example.com/some/file.png"

Copy file from one record to another

Delete record files from storage

Load file from storage

Store files assigned to uploaders in Ecto Schema or Changeset.

Same as store/1 but raises an exception in case of upload error

Generate URL for record & field

Validate changeset using uploader's validate/1 function

Link to this section Callbacks

Specs

cast(file :: Bow.t()) :: Bow.t()

Customize incoming file.

This is the place to do custom file name transformation, since filename/2 is used both when uploading AND generating urls

Example - change file name to include timestamp

defmodule MyAvatarUploader do
  use Bow.Uploader
  use Bow.Ecto

  def cast(file) do
    ts = DateTime.utc_now |> DateTime.to_unix
    Bow.set(file, :rootname, "avatar_#{ts}")
  end
end

Link to this section Functions

Link to this function

cast_uploads(changeset, params, fields, client \\ %Tesla.Client{})

View Source

Specs

cast_uploads(any(), map(), list(), Tesla.Client.t()) :: Ecto.Changeset.t()

Download remote files for given fields, i.e. params["remote_avatar_url"] = "http://example.com/some/file.png"

Example

changeset
|> cast(params, [:name, :avatar])
|> Bow.Ecto.cast_uploads(params, [:avatar])
Link to this function

copy(src, src_field, dst)

View Source

Specs

copy(src :: Ecto.Schema.t(), field :: atom(), dst :: Ecto.Schema.t()) ::
  {:ok, map()} | {:error, any()}

Copy file from one record to another

Fields do not have be the same unless they use the same uploader

Example

user1 = Repo.get(1)
user2 = Repo.get(2)

Ecto.Bow.copy(user1, :avatar, user2)

Specs

delete(model()) :: {:ok, model()} | {:error, any()}

Delete record files from storage

Example

user = Repo.get(...)

user
|> Repo.delete!()
|> Bow.Ecto.delete()
Link to this function

download_params(params, fields, client \\ %Tesla.Client{})

View Source

Specs

download_params(map(), list(), Tesla.Client.t()) :: map()

Specs

load(Ecto.Schema.t(), field :: atom()) :: {:ok, Bow.t()} | {:error, any()}

Load file from storage

Example

user = Repo.get(...)
case Bow.Ecto.load(user, :avatar) do
  {:ok, file} -> # file.path is populated with tmp path
  {:error, reason} -> # handle load error
end

Specs

store(model()) :: {:ok, model(), map()} | {:error, model(), map()}

Store files assigned to uploaders in Ecto Schema or Changeset.

In order to understand how to properly use store/1 function you need to read these few points:

  • Ecto does not have callbacks (like after_save etc)
  • Ecto.Changeset.prepare_changes is run before data is saved into database, so when inserting a new record it will not have a primary key (id)
  • Uploading during type casting is a bad idea
  • You do want to use record primary key in storage directory definition
  • You don't want to upload the same file multiple time, even if it hasn't changed

You need to pass inserted/updated record since the changeset lacks primary key. When updating Bow.Ecto will upload only these files that were changed.

changeset = User.create_changeset(%User{}, params)
with  {:ok, user} <- Repo.insert(changeset)
      {:ok, _}    <- Bow.Ecto.store(user) do # pass record here, not changeset
  {:ok, user}
end

Creating

user = User.changeset(params)
with {:ok, user} <- Repo.insert(user),
    {:ok, user, results} <- Bow.Ecto.store(user) do
  # ...
else
  {:error, reason} -> # handle error
end

Updating

user = User.changeset(user, params)
with {:ok, user} <- Repo.update(user),
    {:ok, user, results} <- Bow.Ecto.store(user) do
  # ...
else
  {:error, reason} -> # handle error
end

There is also store!/1 function that will raise error instead of returning :ok/:error tuple.

Specs

store!(model() | {:ok, model()} | {:error, any()}) ::
  {:ok, model()} | {:error, any()} | model()

Same as store/1 but raises an exception in case of upload error

Creating

%User{}
|> User.changeset(params)
|> Repo.insert!()
|> Bow.Ecto.store!()

Updating

user
|> User.changeset(params)
|> Repo.update!()
|> Bow.Ecto.store!()

Specs

url(Ecto.Schema.t(), atom()) :: String.t() | nil

Generate URL for record & field

Link to this function

url(record, field, opts)

View Source

Specs

url(Ecto.Schema.t(), atom(), atom() | list()) :: String.t() | nil
Link to this function

url(record, field, version, opts)

View Source

Specs

url(Ecto.Schema.t(), atom(), atom(), list()) :: String.t() | nil

Specs

Validate changeset using uploader's validate/1 function

Example

def changeset(model, params) do
  model
  |> cast(params, [:name, :avatar])
  |> Bow.Ecto.validate()
end