I was wondering if there is an accepted way to retain a copy of an uploaded file in the case that the greater model fails validation and needs to be redisplayed? This would prevent the user needing to re-upload the file again to submit the form. I’ve not been able to turn anything up elsewhere.
Yes, there’s ways, though not too well formalized.
This is one approach
I’ve also done this with Dropzone. Let me post a few extracts of my code, but if any more questions I’ll try to wrap it up in a blog post in the next days.
The way I’ve done it is with 2 accessor variables:
-
files
which is theform.file_field
-
valid_blobs
which allows you to “re-render” only the valid files back in case there’s validation errors with any other parts of the form or with single files.
I’ve not managed to make it work with a single accessor only (example files
), but it might be possible.
In my case, I had antivirus validation on the Fields::FilesField
, which would remove invalid files from the files
collection.
<%# In case of validation errors, we re-render the files %>
<%
existing_files = form.object.valid_blobs.map do |blob|
{
name: blob.filename,
size: blob.byte_size,
signed_id: blob.signed_id,
isImage: blob.image?,
url: rails_blob_url(blob),
}
end
%>
<div class="dropzone dropzone-default dz-clickable dropzone-previews"
data-controller="dropzone"
data-dropzone-existing-files-value="<%= existing_files.to_json %>"
data-dropzone-max-file-size="20"
data-dropzone-max-files="50">
<div class="ta-center">
<%= form.file_field :files, direct_upload: true, data: { dropzone_target: 'input' }, multiple: true %>
</div>
<div class="dropzone-msg dz-message needsclick text-gray-600" data-dropzone-target="clickable">
<h3 class="dropzone-msg-title mb-0">Drag here to upload or click here to browse</h3>
<span class="dropzone-msg-desc text-xs">
20 MB file size maximum.
</span>
</div>
</div>
class CreateMessageForm # change to your preferred name
include ActiveModel::Model
include Field::FilesField
def save
return false unless valid?
valid_blobs.each do |blob|
# here somehow save the files (blob ids actually)
end
end
end
module Field
module FilesField
extend ActiveSupport::Concern
included do
attr_writer :files
# You can add validations here
# validate :correct_document_mime_type
validate :check_for_viruses
end
def valid_blobs
files.filter_map { |file| ActiveStorage::Blob.find_signed file }
end
def files
@files || []
end
private
def check_for_viruses
scanner = Ratonvirus.scanner
files.each do |file|
document = CaseDocument.new
document.file = file
next unless scanner.virus?(document.file)
if scanner.errors.any?
scanner.errors.each do |err|
errors.add :files, err
end
else
errors.add :files, :antivirus_virus_detected
end
end
end
end
end
Thanks @mtomov, you’re right, it’s certainly easier to do if the file is already in the storage service Unfortunately, in my case I’m only allowing uploads via the server. I suppose there’s scope there to emulate what the likes of
dragonfly
are doing by hooking into the temporary file system and retaining those files for a while between requests to allow for re-use, though of course this only works in single server situations. I suppose the file could also be uploaded to the storage service before the record is saved (like Rails 5 used to do) and we could use your technique.
I had another idea that I’ve not thought fully through, but one could probably create a model to the side that uses local disk storage as its service. We could intercept the assignment of files and also create a local blob immediately that could then be used as the basis for form resubmissions. It would work better with a reaping cron job and could also use a disk service that is shared between servers. Just thinking aloud
I see. If you’re not doing direct file upload, then your file is part of the POST parameters. In this case I don’t really get the issue of re-rendering the file, as you have a reference to the file in your POST parameters. I think at this point the file is stored as a Tempfile
. Single server or not won’t matter, as the request and response are handled by the same server.
So, if your file param is called file
, then you should have access to on re-render of the :new
action through params[:file]
.
def create
form = Form.new(form_params)
if form.save
redirect_to ...
else
# here you can access `params[:file]`
render :new, locals: {
form: form,
}, status: :unprocessable_entity
end
end
I’m probably missing something thought based on your description. Let me know.
We definitely have access to the file during the request-response cycle but what I’m looking for is to be able to find this file on the next request from this user (should the model fail to save, and they try again having fixed the problem unrelated to the file they uploaded). Normally the tempfile will be thrown away when the rails instance that dealt with it terminates (after so many requests? depends on the server), in any regard it’s unpredictable.
Hope that helps explain it a bit more
There’s a plug-in for this: cached_attachment_data
Or are you thinking of something more complex, say a second visit (rather than a reload after validation failure)?
Walter
Yep that’s the one I was searching all over the docs there for that but couldn’t find it!
Oh, damn, switched from the Shrine, but didn’t expect that it will be a problem with official ActiveStorage gem . Now I need to rewrite the forms using direct upload or maybe there is already solution in 2024? Does anybody know?
No change, as far as I’m aware.