A Little Background
As I continue to phase out Paperclip in favor of ActiveStorage, Iβve wanted to keep the methods I used to manage these assets as succinct and reusable as possible.
ActiveStorage gives us a dead simple way to save and update assets, but the ability to delete assets independently of the parent record, particularly if youβre using has_many_attached
, has been left to each individual app to figure out.
I have stumbled upon a couple of different takes on how to potentially do this, but Iβve not been satisfied with their approaches. Many do not take into account authorization and permissions, which if youβre not careful, allows any user to delete any attachment they choose just by randomly hitting URLs in your application. With that in mind I set out to try and roll my own.
The Approach
Since all attachments are saved as the same object/models types, Attachment and Blob, we can use a common controller to interact and modify with them, regardless of the parent record that it belongs to.
In my case I chose to stick closely to the naming conventions already given to us, so I created a new controller named ActiveStorage::AttachmentsController
, and since for now Iβm only worried about deleting attachments, we only need one method, delete
.
I use Devise for authentication, so we need to make sure that the user is logged in before we let them do anything, which we do with :authenticate_user!
.
I also want to make sure that the user has the permissions to modify these attachments, so I check their permissions to see if they can modify the parent record using the authorize_attachment_parent!
method. I use CanCanCan in this case, but you can always swap out your auth call to fit whatever method you use. This piece is _ critical _ to ensure that your users arenβt deleting anything they shouldnβt.
The purge_later
call will take care of the actual file deletion in your background queue, and will delete the corresponding Blob
record.
# app/controllers/active_storage/attachments_controller.rb
class ActiveStorage::AttachmentsController < ApplicationController
before_action :authenticate_user!
before_action :set_attachment, :authorize_attachment_parent!
def destroy
@attachment.purge_later
end
private
def set_attachment
@attachment = ActiveStorage::Attachment.find(params[:id])
@record = @attachment.record
end
def authorize_attachment_parent!
authorize! :manage, @record
end
end
Also be sure to wire this new controller up in your routes:
# config/routes.rb
scope :active_storage, module: :active_storage, as: :active_storage do
resources :attachments, only: [:destroy]
end
Itβs also important to update the user interface to remove any reference to the attachment, and in this case I used Rails UJS. The javascript necessary to remove the element from my UI is simple and straightforward:
// app/views/active_storage/attachments/destroy.js.erb
document.getElementById("<%= dom_id @attachment %>").remove()
The only assumptions made here are that you had the representation of your attachment wrapped in a div with ID of attachment_#{id}
, which you can easily render using Rails handy dom_id
method.
You can now use the following markup next all of your attachments to allow for easy deletion:
link_to "Delete", active_storage_attachment_path(attachment),
method: :delete, remote: :true,
data: { confirm: "Are you sure you wanna this?" }
Wrapping it Up
This approach is relatively simple, and relies heavily on the tools and features that Rails provides to us.
In my case Iβm also leveraging a common partial to render thumbnails, metadata, and links for attachments, making it dead simple to add attachments to any model I choose to in the future.
As always things evolve over time, and while Iβd like ot think Iβm perfect Iβm sure there are ways to improve this code, so if you have any suggestions or improvements Iβd love to hear them.
Top comments (1)
gist.github.com/thadeu/8ce22734754...