Reputation: 3032
I have a Rails model with:
has_many_attached :files
When uploading via Active Storage by default if you upload new files it deletes all the existing uploads and replaces them with the new ones.
I have a controller hack from this which is less than desirable for many reasons:
What is the correct way to update images with has_many_attached in Rails 6
Is there a way to configure Active Storage to keep the existing ones?
Upvotes: 9
Views: 4677
Reputation: 1
In Rails 7.1 the default option for = f.file_field :files, multiple: true, include_hidden: true You can check it in dev/tools in browser This option means that form submission will include hidden And hidden are id's of existing attachments
<% @message.files.each do |file| %>
<%= f.hidden_field :files, multiple: true, value: file.signed_id %>
<% end %>
[3.4 Replacing vs Adding Attachments][1] [1]: https://edgeguides.rubyonrails.org/active_storage_overview.html#replacing-vs-adding-attachments
So that is a solution!
Upvotes: 0
Reputation: 1089
As of Rails 7, you can call include_hidden: false
in your form to stop this happening.
<%= form.file_field :images, multiple: true, include_hidden: false %>
More details on the file_field
documentation.
:include_hidden - When multiple: true and include_hidden: true, the field will be prefixed with an field with an empty value to support submitting an empty collection of files.
https://edgeapi.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-file_field
Upvotes: 0
Reputation: 291
The solution suggested for overwriting the writer by @edariedl DOES NOT WORK because it causes a stack level too deep
Based on ActiveStorage source code at this line
You can override the writer for the has_many_attached
like so:
class Model < ApplicationModel
has_many_attached :files
def files=(attachables)
attachables = Array(attachables).compact_blank
if attachables.any?
attachment_changes["files"] =
ActiveStorage::Attached::Changes::CreateMany.new("files", self, files.blobs + attachables)
end
end
end
You can create a model concern that will encapsulate all this logic and make it a bit more dynamic, by allowing you to specify the has_many_attached
fields for which you want the old behaviour, while still maintaining the new behaviour for newer has_many_attached
fields, should you add any after you enable the new behaviour.
in app/models/concerns/append_to_has_many_attached.rb
module AppendToHasManyAttached
def self.[](fields)
Module.new do
extend ActiveSupport::Concern
fields = Array(fields).compact_blank # will always return an array ( worst case is an empty array)
fields.each do |field|
field = field.to_s # We need the string version
define_method :"#{field}=" do |attachables|
attachables = Array(attachables).compact_blank
if attachables.any?
attachment_changes[field] =
ActiveStorage::Attached::Changes::CreateMany.new(field, self, public_send(field).public_send(:blobs) + attachables)
end
end
end
end
end
end
and in your model :
class Model < ApplicationModel
include AppendToHasManyAttached['files'] # you can include it before or after, order does not matter, explanation below
has_many_attached :files
end
NOTE: It does not matter if you prepend
or include
the module because the methods generated by ActiveStorage are added inside this generated module which is called very early when you inherit from ActiveRecord::Base
here
==> So your writer will always take precedence.
If you want something even more dynamic and robust, you can still create a model concern, but instead you loop inside the attachment_reflections
of your model like so :
reflection_names = Model.reflect_on_all_attachments.filter { _1.macro == :has_many_attached }.map { _1.name.to_s } # we filter to exclude `has_one_attached` fields
# => returns ['files']
reflection_names.each do |name|
define_method :"#{name}=" do |attachables|
# ....
end
end
However I believe for this to work, you need to include this module after all the calls to your has_many_attached
otherwise it won't work because the reflections array won't be fully populated ( each call to has_many_attached appends to that array)
Upvotes: 8
Reputation: 3352
Looks like there is a configuration that does exactly that
config.active_storage.replace_on_assign_to_many = false
Unfortunately it is deprecated according to current rails source code and it will be removed in Rails 7.1
config.active_storage.replace_on_assign_to_many
is deprecated and will be removed in Rails 7.1. Make sure that your code works well withconfig.active_storage.replace_on_assign_to_many
set totrue
before upgrading. To append new attachables to the Active Storage association, prefer usingattach
. Using association setter would result in purging the existing attached attachments and replacing them with new ones.
It looks like explicite usage of attach
will be the only way forward.
So one way is to set everything in the controller:
def update
...
if model.update(model_params)
model.files.attach(params[:model][:files]) if params.dig(:model, :files).present?
else
...
end
end
If you don't like to have this code in controller. You can for example override default setter for the model eg like this:
class Model < ApplicationModel
has_many_attached :files
def files=(attachables)
files.attach(attachables)
end
end
Not sure if I'd suggest this solution. I'd prefer to add new method just for appending files:
class Model < ApplicationModel
has_many_attached :files
def append_files=(attachables)
files.attach(attachables)
end
end
and in your form use
<%= f.file_field :append_files %>
It might need also a reader in the model and probably a better name, but it should demonstrate the concept.
Upvotes: 23