
We have faced an issue with ActiveStorage in our project recently and when we looked into the source code, we found something interesting and wondered why it is not documented anywhere. Let’s get into the issue we have faced and see the solution.
What the issue was?
For example, Our app has the User model and a user can attach many images.
# In User model we had something like the below,
class User < ApplicationRecord has_many_attached :images # for multiple file upload end
# In the controller
class UsersController < ApplicationController def create user = User.create!(user_params) redirect_to user end def update user = User.update!(user_params) redirect_to user end private def user_params params.require(:user).permit(:name, :email, images: []) end end
In the form,
<%= form.file_field :images, multiple: true %>
Creating user object with attachments:
There is no problem in the creation logic. All the attached images are inserted and mapped to the user.
Updating user object with no existing images attached:
As like create action, there is no issue here. All the attached images are inserted and mapped to the existing user.
Updating user object with existing attached images:
- Say, the user was created with N images.
- When updating other attributes of the user, if the request receives images parameter as empty array, all the existing attachments of the user will be deleted.
What we used to do with paperclip?
But this is not the case when using paperclip with nested attributes. We used to specify `_destroy` as true to delete an existing attachment.
How to control the behaviour?
Lets dive into the ActiveStorage’s source code (Setter method for the attachments)
# active_storage/attached/model.rb
def #{name}=(attachables) if ActiveStorage.replace_on_assign_to_many attachment_changes["#{name}"] = if Array(attachables).none? ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self) else ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables) end else if Array(attachables).any? attachment_changes["#{name}"] = ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables) end end end
The one thing noted from the source code is that there is a configuration available to make decision when the attachment is empty.
replace_on_assign_to_many:
This is a configuration available to “Optionally replace existing attachments instead of adding to them when assigning to a collection of attachments”. By default, this configuration is set as true.
Here is the line from Rails source code(# rails/application/configuration.rb)
active_storage.replace_on_assign_to_many = true
So in the update request, all the attachments which are being sent in the request is attached and all the existing attachments are removed.
How to make the existing images persisted when replace_on_assign_to_many = true?
When sending signed ids in the update request, the existing images will be persisted even when the replace_on_assign_to_many value is configured as true.
# By adding below code the signed ids are being sent in the attachment key for each existing images. So that the existing attachments with the attachment key won’t be removed.
<% if user.images.each do |image| %> <%= form.hidden_field :images, multiple: true, value: image.signed_id %> <% end %>
If you want to remove any of the existing images, you can just remove the hidden field, so that the signed id of the particular image won’t be sent in the request and that image will be removed.
Disabling purging existing images by default
If you don’t want to remove the images when updating the record with new attachments, You can set the configuration replace_on_assign_to_many to false in any of the configuration file or in initializer.
# Example configuration in config/application.rb
config.active_storage.replace_on_assign_to_many = false
By this configuration, ActiveStorage will merge the existing attachments and the new attachments always. Check the else part of the below source code,
def #{name}=(attachables) if ActiveStorage.replace_on_assign_to_many attachment_changes["#{name}"] = if Array(attachables).none? ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self) else ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables) end else if Array(attachables).any? attachment_changes["#{name}"] = ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables) end end end
If you want to remove the existing images later, you can do this by using the purge or purge_later method available in ActiveStorage. You can also refer to the issue raised in Rails GitHub repository for more info.
Note : This configuration is available only from Rails 6. Please check Rails 6 change logs for further info.
Hope, the walkthrough would help you in handling similar issues. Lets connect in some other interesting blogs.
– Aarthi K,
ROR Development team,
Mallow Technologies.