File uploads for Elixir
- Generate multiple dependent versions of a file
- Integration with Ecto
- Allow downloading remote files (
remote_avatar_urlparams etc.) - Multiple storage adapters (local disk, Amazon S3)
defdepsdo[{:bow,"~> 0.4.3"},# for AWS S3 support{:ex_aws,"~> 2.0"},{:ex_aws_s3,"~> 2.0"},# for Bow.Exec{:erlexec,"~> 1.7.0"}]enddefmoduleMyUploaderdouseBow.Uploader# specify storage directorydefstore_dir(_file)do"uploads"endenddefmoduleAttachmentUploaderdouseBow.Uploader# define what versions to generate for given filedefversions(_file)do[:original,:thumb]end# keep the origianal file namedeffilename(file,:original),do: file.name# prepend "thumb_" for thumbnaildeffilename(file,:thumb),do: "thumb_\#{file.name}"# do nothing with original filedeftransform(file,:original),do: transform_original(file)# generate image thumbnaildeftransform(source,target,:thumb)doBow.Exec.execsource,target,"convert ${input} -strip -gravity Center -resize 250x175^ -extent 250x175 ${output}"end# specify storage directorydefstore_dir(file)do"attachments/\#{file.scope.id}"end# specify storage optionsdefstore_options(_file)do[acl: :public_read]endend# Add `use Bow.Ecto` to the uploaderdefmoduleMyApp.UserAvatarUploaderdouseBow.UploaderuseBow.Ecto# <---- HERE# file.scope will be the user structdefstore_dir(file)do"users/#{file.scope.id}/avatar"endend# add avatar field to users tabledefmoduleMyApp.Repo.Migrations.AddAvatarToUsersdouseEcto.Migrationdefchangedoaltertable(:users)doadd:avatar,:stringendendend# use `MyApp.UserAvatarUploader.Type` as field typedefmoduleMyApp.Userdoschema"users"dofield:email,:stringfield:avatar,MyApp.UserAvatarUploader.Type# <-- HEREenddefchangeset(model\\%__MODULE__{},params)domodel|>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 rulesendend# create user and save fileschangeset=User.changeset(%User{},params)with{:ok,user}<-Repo.insert(changeset),{:ok,user,_}<-Bow.Ecto.store(user)do{:ok,user}endWith standalone uploaders:
file=MyUploader.new("path/to/file.png")Bow.url(file)# => url of original fileBow.url(file,:thumb)# => url of thumb versionWith Ecto integration:
user=Repo.get(User,1)Bow.Ecto.url(user,:avatar)# url of avatar originalBow.Ecto.url(user,:avatar,:thumb)# url of avatar thumbBow.Ecto.url(user,:photo,:thumb,signed: true)# you can pass storage-specific optionsYou can change the file name using uploader's cast/1 callback:
defmoduleTimestampUploaderdouseBow.UploaderuseBow.Ectodefcast(file)do# replace "myfile.png" with "avatar_12343456.png"ts=DateTime.utc_now|>DateTime.to_unixBow.set(file,:rootname,"avatar_#{ts}")enddefstore_dir(_file),do: "timestamp"endYou can overwrite validate/1 to add validations for e.g. allowed extension.
defmoduleAvatarUploaderdo# ...defvalidate(%{ext: ext})whenextin~w(.jpg .png),do: :okdefvalidate(_),do: {:error,:extension_not_allowed}# ...endIt is best to use local storage adapter when testing.
# config/test.exsconfig: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