This article is also available on Medium.
If you've ever used Active Storage, I bet you must have encountered the same problem I was confused by.
Imagine if your app has a simple form with file upload tags along with other fields. Then one of the fields has unmet with validation rules on submission. So your app displays the form again. Finally, the user corrected the error and submit again. But files that should have saved were simply disappeared...
This kind of story isn't uncommon if you're using Active Storage. I had faced this too. But I found a solution after all.
Note:
If you can use direct_upload
in your application, you don't need to read this article. You can simply use this solution.
When using activestorage in Rails 6, how do I retain a file when redisplaying a form?
TL;DR
- You need to upload files and save
ActiveStorage::Blob
objects manually before form reappearance. - You need to submit blob's key using hidden fields.
Background
Active Storage uses 2 models to store attachment information to database. First one is ActiveStorage::Blob. This is a model to store file's metadata. And another is ActiveStorage::Attachment. This is an intermediate table to combine ActiveStorage::Blog
to your model.
Thehas_one_attached
method makes your model to manage these 2 models automatically. I'll talk about has-one case but most logics are same as has-many one.
Source code of has_one_attached
is here. Take a look at these 2 lines.
after_save { attachment_changes[name.to_s]&.save }
after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
If you made changes to your model's attachment, those actions are stored as object in attachment_changes
. For example, ActiveStorage::Attached::Changes::CreateOne, ActiveStorage::Attached::Changes::DeleteOne, and so on.
Your model will be configured to call these objects's save
method right before database access. (after_save
hook will be called before actual changes to the database.) Objects of ActiveStorage::Blob
and ActiveStorage::Attachment
will be created and referenced to your model on the save
method.
And attached files will be uploaded on after_commit
hook.
What is the root cause of problem?
Attachment persistence is handled when you save your model, as I mentioned in the previous section. This is a problem because your model won't be saved before form resubmission.
Solution
Problems are identified. What should we do?
Step.1
You need to add this hidden fields to your form. This makes file's signed_id to sent on form resubmission.
= f.file_field :doc
= f.hidden_field :doc, value: f.object.doc.signed_id if f.object.doc.attached?
I used this page as a reference.
When using activestorage in Rails 6, how do I retain a file when redisplaying a form?
Step.2
You need to write these codes on your app. This will let you to upload files and save ActiveStorage::Blob
object manually.
# In your app's controller
def update
...
# `obj` is your model that using `has_one_attached`.
if(obj.update)
redirect_to ...
else
obj.attachment_changes.each do |_, change|
if change.is_a?(ActiveStorage::Attached::Changes::CreateOne)
change.upload
change.blob.save
end
# TODO: ActiveStorage::Attached::Changes::CreateMany
end
...
end
end
That's it!
Top comments (1)
anyone an idea how to achieve that with the current active storage changes in github.com/rails/rails/commit/d092...?