Retain an ActiveStorage upload for form redisplay

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:

  1. files which is the form.file_field
  2. 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 :slight_smile: 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 :smiley:

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 :slight_smile:

1 Like

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

1 Like

Yep that’s the one :slight_smile: I was searching all over the docs there for that but couldn’t find it! :smiley:

Oh, damn, switched from the Shrine, but didn’t expect that it will be a problem with official ActiveStorage gem :frowning:. Now I need to rewrite the forms using direct upload or maybe there is already solution in 2024? Does anybody know? :frowning:

No change, as far as I’m aware.

1 Like