DEV Community

Cover image for Handling ActiveStorage direct uploads and server side form validations
Drew Bragg
Drew Bragg

Posted on

Handling ActiveStorage direct uploads and server side form validations

Ok, so this is slightly hacky. Let me also preface this by saying there's probably no real need to do this if you add some client side form validation. Nonetheless sometimes submissions can get through and it's nice to know you can persist the direct upload when we rerender the view.

If anyone knows a better way to do the PLEASE feel free to let me know in the comments below. I searched around a bit but came up with nothing so this is how I'm handling it for now.

If you're building a Rails 5.2 app, or just follow Rails in general, there's a good chance you're familiar with ActiveStorage. If you read through the Rails docs you'll see it's really easy to set up. Even direct uploads are crazy easy. What's not so easy, well not so clear, is handling the direct upload if our submission is kicked back from the controller.

Let's say we have something like this going on:
ActiveStorage direct upload example

Pretty standard Rails stuff here. Everything but the ActiveStorage and validation is what you would expect from a typical scaffold.

This will all work as expected with the super cool feature of the post image being directly uploaded to whatever storage solution we are using.

Ok, cool. So what's the issue?

Well, remember how our model validates the presence of title and body? We'll run into a SNAFU if we submit the form without the required information.

Now, this is where things get a little tricky. Our form will repopulate when our controller calls render :new with the exception of the image we attached. To further complicate the matter, the image we attached has already been uploaded via the direct upload. This presents two issues, one, if we simply attach the file to the form again it will now be uploaded twice. And two, if we have any kind of preview/variant/indication to our users that there is an attached image, like when using the form for editing, we are going to confuse our users. Why? Because if we're conditionally rendering the image or changing a label or just telling the user that, yes, the post has an attached image.



@post.image.attached?
# => true


Enter fullscreen mode Exit fullscreen mode

Wait, what?

Yup. Because of the direct upload a record on the active_storage_blobs table has been created, like it should. And because we tried to attach the image to a post a record on the active_storage_attachments join table was created. Unfortunately since our post was not saved, the record_id column is nil so this attachment won't persist. At least at the time or this writing (or because I missed something entirely) ActiveStorage is not smart enough to handle the invalid attachment record. So if we correct the form issues and resubmit, we won't have the image associated with our post, leaving an orphaned file in our cloud storage and our users a little confused (and probably) frustrated.

Solution?

Like I said, kinda more like a hack. If we add the following line to to the else block of our posts_controller:


 ruby
@image = params[:post][:image] if params[:post][:image].present?


Enter fullscreen mode Exit fullscreen mode

and update our view form with something like this:



<% if @image %>
    <%= f.hidden_field :image, value: @image %>
<% else %>
    <%= f.file_field :image, direct_upload: true %>
<% end %>


Enter fullscreen mode Exit fullscreen mode

Now that everything looks like this:

fixed it!

Like magic, things will work a little more as expected. Next time we try to attach an image to our post, submit the form without a title or body, and then resubmit our uploaded image will be attached.

Neat! Why?

Cause reasons! And because of how Rails/ActiveStore handles direct uploads. After the direct upload is completed it sends back a signed_id that Rails uses to created the blob record in the database and attached to the post via the attachment join table. The sign_id is passed via params to our controller and ActiveStorage handles all the magic. So if we updated our controller to store the value of params[:document][:file] and our view to place that value (if it exists) into a hidden field on the form we will be able to submit the file and because the signed_id already exists on the blob table, ActiveStorage is smart enough (this time) to only create an attachment record.

Like I said, adding a little bit of JavaScript to our form to handle validation before the form hits our server is the way to go here, but honestly, who wants to write JavaScript if they don't have to. And this way our server stills can function as a second line of defense.

Top comments (8)

Collapse
 
nameisvijay profile image
Vijay Meena • Edited

Oh, Just upgraded to rails 6 today in order to check behavior. Things are changed in rails 6 (atleast for non direct-uploaded file). Now, uploaded file will only persist to storage if post gets saved successfully. Thus there won't be orphaned for records which won't be saved.

I still feel that server may not get chance to validate during direct-upload. I have yet to check behavior for direct-upload

Collapse
 
alispat profile image
Alisson Patrick

Hi guys! I believe I found a simpler solution for this issue on ActiveStorage.

Just put this hidden_field on your form:

<%= f.hidden_field :image, id: nil, value: params[:post][:image] rescue nil %>
Enter fullscreen mode Exit fullscreen mode

And there is no need to do any treatment on your controller.

Your form will end up like this:

<%= form_with(model: @post, local: trye) do |f| %>
  <%= f.text_field :title %>
  <%= f.text_area :body %>
  <%= f.hidden_field :image, id: nil, value: params[:post][:image] rescue nil %>
  <%= f.file_field :image, direct_upload: true %>
<% end %>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bkspurgeon profile image
Ben Koshy

Thanks for the post. It's so simple -- after you know the solution!

Collapse
 
drbragg profile image
Drew Bragg

Glad you found it helpful! This is basically the post I wish I had been able to find. ๐Ÿ˜‚

Collapse
 
nameisvijay profile image
Vijay Meena • Edited

It won't stop creating orphaned file on cloud storage for some cases. For exmample, user has to upload their profile picture during registration to my website. If a user upload profile pic but doesn't submit form then it will still create orphaned file on storage.

I haven't used active storage yet but I used javascript and aws-sdk to direct-upload files few months back on rails4 app. I faced same problem and implemented same hack as you suggested. But, I modified design a bit. I created two buckets on my amazon s3 storage. First "cache" and second "production". My app direct-uploaded all the stuff to "cache" bucket and then my model checked required validation at form-submission and used before_save filter which copied image from "cache" bucket to "production" bucket using aws-sdk if validation succeeded.

I also set expiry time limit to "cache" file so all these temporary files were auto-purged. This way, all orphaned uploads were only saved to "cache" and actual records were saved to "production" and "cache" was auto-purged in 24 hours by s3.

I don't know if it can work with active-storage (because i heard that we can only use single bucket with active-storage) I also don't know if what i did was a proper way to do it but it worked for me.

Collapse
 
bkspurgeon profile image
Ben Koshy

This is the exact method employed by the Shrine gem - which is a masterpiece IMO. Takes away all the headaches of validations with the "cache" and "storage" sequestrations. However if you are using ActionText, and want the equivalent functionality using Shrine - then it looks like you're out of luck if you want something out of the box.

Collapse
 
medboo profile image
medboo

other simple ways are:

  • Put image upload on its own (in a separate step), after the user fills up the form, he could direct upload the image in the next step.

  • You can make a popup that lists all user uploads, so users can upload images and the popup will be the source of choosing images, that also help in the future if the user wants to use the same images.

  • in case you insist to use the way you describe in your post, you can make a cron job that delete orphaned files after x time from their creation

sometimes you just need to find another way to think about it!

Collapse
 
amarafinbarrs profile image
Amara Finbarrs

Please can someone help me, I have tried but I can't seem to make the code work. The image form still clears if my form fails a validation check?