loading...
Cover image for Handling ActiveStorage direct uploads and server side form validations

Handling ActiveStorage direct uploads and server side form validations

drbragg profile image Drew Bragg ・3 min read

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

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:

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

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 %>

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.

Posted on by:

drbragg profile

Drew Bragg

@drbragg

Full Stack Dev && Single Dad && Board Game Geek && Hockey Player

Discussion

pic
Editor guide
 

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

 

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.

 

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

 

Glad you found it helpful! This is basically the post I wish I had been able to find. 😂