Bow

File uploads for Elixir

Features

Installation

def deps do
[
{:bow, "~> 0.3.0"},
# 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