Disassembling Rails — How does ActionText deal with file upload?

  1. Upload
  2. Parse, Transform and Attach
  3. Render

Upload

  1. Rails’ JS library will first send a post request to `/rails/active_storage/direct_uploads` (this endpoint is configurable) with file metadata like
{
"blob"=>{
"filename"=>"goby-logo.png",
"content_type"=>"image/png", "byte_size"=>5109,
"checksum"=>"H4+4U/vWyxzVUOmMLG9Ghg=="
},
"direct_upload"=>{
"blob"=>{
"filename"=>"goby-logo.png",
"content_type"=>"image/png",
"byte_size"=>5109,
"checksum"=>"H4+4U/vWyxzVUOmMLG9Ghg=="
}
}
}
<figure 
data-trix-attachment={
“contentType”:”image/png”,
”filename”:”goby-logo.png”,
”filesize”:5109,
”height”:142,
”sgid”:”BAh7CEkiCGdpZAY6BkVUSSI8Z2lkOi8vYWN0aW9udGV4dG5wbHVzMS9BY3RpdmVTdG9yYWdlOjpCbG9iLzM_ZXhwaXJlc19pbgY7AFRJIgxwdXJwb3NlBjsAVEkiD2F0dGFjaGFibGUGOwBUSSIPZXhwaXJlc19hdAY7AFQw—4e7f74086b3917849b01b3136fd6465220a11804”,
”url”:”http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--9ec5e5aa73562eb16a1b06ad42c83e1a9d94c3dc/goby-logo.png”,
”width”:142
}>

Parse, Transform and Attach

  • The image’s blob record is an orphan right now, we need to find a way to attach it to something (an ActionText::RichText record).
  • Since we don’t always display our text content inside a Trix editor, it's not a good idea to store those Trix elements directly. So ActionText needs to perform some transformations beforehand.

Parse

# rails/actiontext/app/models/action_text/rich_text.rb 
module ActionText
class RichText < ActiveRecord::Base
serialize :body, ActionText::Content
end
end

Transform

  1. ActionText will go through the content tree and look for elements that matches [data-trix-attachment] selector.
  2. When it finds the node, it’ll parse the value of data-trix-attachment into a series of attributes.
  3. It then uses those attributes to initialize an action-text-attachment node. The node would look like this when serialized into html:
<action-text-attachment 
sgid="BAh7CEkiCGdpZAY6BkVUSSI8Z2lkOi8vYWN0aW9udGV4dG5wbHVzMS9BY3RpdmVTdG9yYWdlOjpCbG9iLzQ_ZXhwaXJlc19pbgY7AFRJIgxwdXJwb3NlBjsAVEkiD2F0dGFjaGFibGUGOwBUSSIPZXhwaXJlc19hdAY7AFQw--683e065a43885a4d71de889936f184999aa4e208"
content-type="image/png"
url="http://localhost:3000/rails/active_storage/blobs/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBDUT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--0e0e2e03765b64517af21a00ca5cc9771b2b2984/goby-logo.png"
filename="goby-logo.png"
filesize="5109"
width="142"
height="142"
presentation="gallery">
</action-text-attachment>

Attach

  1. Assets uploaded via ActiveStorage
  2. Trix partials
  3. Remote images
# rails/actiontext/app/models/action_text/rich_text.rb 
module ActionText
class RichText < ActiveRecord::Base
before_save do
self.embeds = body.attachables.grep(ActiveStorage::Blob).uniq if body.present?
end
end
end

Signed Global IDs

  1. The value is an Global Identification across the app.
  2. It’s generated from the blob object directly.
  3. It contains everything we need to find that blob object, including the model name and the record id.
  4. It’s signed so other people won’t be able to tamper with your data. You can inspect the signed global id in your console like:
sgid = ActiveStorage::Blob.find(id).to_sgid(for: "attachable").to_s
SignedGlobalID.parse(sgid, for: "attachable").model_id #=> "1"
GlobalID::Locator.locate_signed sgid, for: "attachable" #=> #<ActiveStorage::Blob id: 1.....>
# rails/actiontext/lib/action_text/attachable.rb 
module ActionText
module Attachable
class << self
def from_attachable_sgid(sgid, options = {})
method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed
record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME))
record || raise(ActiveRecord::RecordNotFound)
end
end
end
end

Render

  1. Parse the content into a tree
  2. Find all attachment nodes
  3. And use attachment’s sgid to perform a global search to find the image blob.
Message.with_rich_text_content_and_embeds
# rails/actiontext/lib/action_text/attachable.rb 
module ActionText
module Attachable
class << self
def from_attachable_sgid(sgid, options = {})
method = sgid.is_a?(Array) ? :locate_many_signed : :locate_signed
record = GlobalID::Locator.public_send(method, sgid, options.merge(for: LOCATOR_NAME))
record || raise(ActiveRecord::RecordNotFound)
end
end
end
end
ActiveStorage::Blob.find(decoded_id)

Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Stan Lo

Stan Lo

428 Followers

Creator of Goby language(https://github.com/goby-lang/goby), also a Rails/Ruby developer, Rails contributor. Love open source, cats and boxing.