/bow

File uploads for Elixir (with Ecto support)

Primary LanguageElixirMIT LicenseMIT

Bow

File uploads for Elixir

Features

  • Generate multiple dependent versions of a file
  • Integration with Ecto
  • Allow downloading remote files (remote_avatar_url params etc.)
  • Multiple storage adapters (local disk, Amazon S3)

Installation

def deps do
  [
    {:bow, "~> 0.4.3"},

    # for AWS S3 support
    {:ex_aws, "~> 2.0"},
    {:ex_aws_s3, "~> 2.0"},

    # for Bow.Exec
    {:erlexec,  "~> 1.7.0"}
  ]
end

Usage

Minimal uploader definition

defmodule MyUploader do
  use Bow.Uploader

  # specify storage directory
  def store_dir(_file) do
    "uploads"
  end
end

Full uploader example

defmodule AttachmentUploader do
  use Bow.Uploader

  # define what versions to generate for given file
  def versions(_file) do
    [:original, :thumb]
  end


  # keep the origianal file name
  def filename(file, :original), do: file.name

  # prepend "thumb_" for thumbnail
  def filename(file, :thumb),    do: "thumb_\#{file.name}"


  # do nothing with original file
  def transform(file, :original), do: transform_original(file)

  # generate image thumbnail
  def transform(source, target, :thumb) do
    Bow.Exec.exec source, target,
      "convert ${input} -strip -gravity Center -resize 250x175^ -extent 250x175 ${output}"
  end


  # specify storage directory
  def store_dir(file) do
    "attachments/\#{file.scope.id}"
  end

  # specify storage options
  def store_options(_file) do
    [acl: :public_read]
  end
end

Usage with Ecto

# 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])
    # uncomment to add support for remote_avatar_url params
    # |> Bow.Ecto.cast_uploads(params, [:avatar])
    |> Bow.Ecto.validate() # optional validation using uploader rules
  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

Getting file URL

With standalone uploaders:

file = MyUploader.new("path/to/file.png")

Bow.url(file)         # => url of original file
Bow.url(file, :thumb) # => url of thumb version

With Ecto integration:

user = Repo.get(User, 1)

Bow.Ecto.url(user, :avatar) # url of avatar original
Bow.Ecto.url(user, :avatar, :thumb) # url of avatar thumb
Bow.Ecto.url(user, :photo, :thumb, signed: true) # you can pass storage-specific options

Overwriting file name

You can change the file name using uploader's cast/1 callback:

defmodule TimestampUploader do
  use Bow.Uploader
  use Bow.Ecto

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

  def store_dir(_file), do: "timestamp"
end

Validation

You can overwrite validate/1 to add validations for e.g. allowed extension.

defmodule AvatarUploader do
  # ...
  def validate(%{ext: ext}) when ext in ~w(.jpg .png), do: :ok
  def validate(_), do: {:error, :extension_not_allowed}
  # ...
end

Using Bow in test environment

It is best to use local storage adapter when testing.

# config/test.exs
config :bow,
  storage: Bow.Storage.Local,
  storage_prefix: "tmp/bow/"

Running Bow tests

mix test

Testing ecto integration

# edit config/config.exs

# create test database
MIX_ENV=test mix ecto.create

# run tests
mix test --only ecto

Testing S3 adapter

# install fake-s3 gem
gem install fakes3

# start fake-s3 server
fakes3 -r tmp/s3 -p 4567

# run tests
mix test --only s3