Mallow's Blog

ActiveStorage – Prevent existing file deletion on multiple file uploads in Rails 6

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.

Leave a Reply

%d bloggers like this: