For handling file uploads, we will use Shrine.cr.
Start by adding the shrine
shard to your shard.yml
and run shards install
.
dependencies:
shrine:
github: jetrockets/shrine.cr
branch: master
Next you will require the shard in src/shards.cr
# src/shards.cr
# Require your shards here
require "avram"
require "lucky"
require "carbon"
require "authentic"
require "shrine"
Lastly, we can create a config file for configuring where our uploaded files will be stored.
Create a new file in confg/shrine.cr
# config/shrine.cr
Shrine.configure do |config|
config.storages["cache"] = Shrine::Storage::FileSystem.new("public/uploads", prefix: "cache")
config.storages["store"] = Shrine::Storage::FileSystem.new("public/uploads", prefix: "uploads")
end
We need to store a reference ID to the image in our database.
# src/models/user.cr
class User < BaseModel
table do
column profile_image_id : String?
end
end
You will also need to add the migration. Run lucky gen.migration AddProfileImageToUser
# db/migrations/00000_add_profile_image_to_user.cr
def migrate
alter table_for(User) do
add profile_image_id : String?
end
end
def rollback
alter table_for(User) do
remove :profile_image_id
end
end
The file_attribute
is used in your save operation to specify the name of the param attribute that will contain the file.
# src/operations/save_user.cr
class SaveUser < User::SaveOperation
permit_columns name
file_attribute :profile_picture
before_save do
profile_picture.value.try { |pic| upload_pic(pic) }
end
private def upload_pic(pic)
result = Shrine.upload(File.new(pic.tempfile.path), "store", metadata: { "filename" => pic.filename })
# If the new file is uploaded, no reason to keep the old one!
# If multiple models can share an image, run a query before deleting
# to ensure you're not breaking any references.
if old_image = profile_image_id.original_value
delete_old_profile_image(old_image)
end
profile_image_id.value = result.id
end
private def delete_old_profile_image(old_image)
storage = Shrine.find_storage("store")
if storage.exists?(old_image)
storage.delete(old_image)
end
end
end
Your action code will look standard with no additional code needed.
# src/actions/users/create.cr
class Users::Create < BrowserAction
post "/users" do
SaveUser.create(params) do |op, user|
if user
redirect to: Users::Show.with(user.id)
else
html Users::NewPage, op: op
end
end
end
end
The two main items to take note of is the form_for
uses the multipart: true
option to properly set the enctype
,
and the use of the file_input
.
# src/pages/users/new_page.cr
class Users::NewPage < MainLayout
needs op : SaveUser
def content
form_for Users::Create, multipart: true do
mount Shared::Field, op.name
mount Shared::Field, op.profile_picture, &.file_input
end
end
end
When you’re ready to render the uploaded image
# src/pages/users/show_page.cr
class Users::ShowPage < MainLayout
needs user : User
def content
img src: profile_url(user)
end
private def profile_url(user) : String
if image_id = user.profile_image_id
Shrine.find_storage("store").url(image_id)
else
# Set a fallback if there's no image.
asset("images/fallback.jpg")
end
end
end