File uploads for Elixir
- 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)
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
defmodule MyUploader do
use Bow.Uploader
# specify storage directory
def store_dir(_file) do
"uploads"
end
end
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
# 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
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
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
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
It is best to use local storage adapter when testing.
# config/test.exs
config :bow,
storage: Bow.Storage.Local,
storage_prefix: "tmp/bow/"
mix test
# edit config/config.exs
# create test database
MIX_ENV=test mix ecto.create
# run tests
mix test --only ecto
# install fake-s3 gem
gem install fakes3
# start fake-s3 server
fakes3 -r tmp/s3 -p 4567
# run tests
mix test --only s3