myfile — using Shrine for file attachments

Tim Clegg
4 min readMar 8, 2022

--

Photo by Craig Adderley from Pexels

Background

We’ve been taking a journey through how several different Ruby gems that handle file uploading/downloading can be used with Oracle Cloud Infrastructure (OCI) Object Storage. This is built around a simple application called myfile, which we built in the first article. It’s not a production-grade application, but does show how different gems can be used.

It’s worth taking a look at the first article, which helps you build the base application that this article builds upon.

Prerequisites

You need an OCI account. If you don’t have one, click here to sign-up for a free OCI account! From within your OCI account, you’ll also need an OCI Object Storage bucket (see this article which provides everything needed to set it up). If you’d like to collaborate with other developers, join the fun on Slack!

Don’t do this

This is not meant to be deployed to production. It’s not hardened. It’s not using best-practices. It’s purpose-built to be minimalistic, keeping the focus on the file upload/download functionality and how each gem can be used to work with OCI Object Storage.

Getting started

Use a copy of the myfile application that was built in this article. Start by adding the necessary gems to the application:

# Gemfilegem 'shrine', '~> 3.0'
gem 'aws-sdk-s3', '~> 1.14

This is copied directly from the documentation (see Shrine and the Shrine AWS S3 docs). Any time a specific version is used, care must be taken to make sure the versions are updated, don’t have vulnerabilities, etc. I copied this as-is from the documentation, not doing typical due-diligence that otherwise should be done (be forewarned).

Go ahead and have Bundler install the gems:

$ bundle

Terrific! Now it’s time to get Shrine setup.

Configuring Shrine

Create a new initializer and populate it with the following:

# config/initializers/shrine.rbrequire 'shrine'
require 'shrine/storage/s3'
require 'shrine/storage/memory'
Shrine.plugin :activerecords3_options = {
access_key_id: ENV["OCI_KEY"],
secret_access_key: ENV["OCI_SECRET"],
bucket: "myfile-#{ Rails.env }",
region: ENV["OCI_REGION"],
endpoint: "https://#{ ENV["OCI_NAMESPACE"] }.compat.objectstorage.#{ ENV["OCI_REGION"] }.oraclecloud.com",
force_path_style: true
}
Shrine.storages = {
cache: Shrine::Storage::Memory.new,
store: Shrine::Storage::S3.new(**s3_options)
}

This looks a lot like what you find in the Shrine Getting Started guide (largely because that’s where part of it came from).

Create an uploader and populate it with the following:

# app/uploaders/DocumentUploader.rbclass DocumentUploader < Shrine
end

Migrating the application

Update the database with a migration:

$ rails g migration RemoveThingsColumns

Make the contents of the file look like:

# db/migrate/<numbers>_remove_things_columns.rbclass RemoveThingsColumns < ActiveRecord::Migration[7.0]
def change
remove_column :things, :file_name, :string
remove_column :things, :content_type, :string
remove_column :things, :file_data, :binary

add_column :things, :document_data, :text
end
end

Then apply the migration:

$ rake db:migrate

Update the model to use Shrine:

# app/models/thing.rbclass Thing < ApplicationRecord
include Shrine::Attachment(:document)
end

Update the controller from this:

# app/controlles/things_controller.rb...  def download
response.set_header('Content-Disposition', "attachment; filename=\"#{@thing.file_name}\"")
response.content_type = @thing.content_type
response.write(@thing.file_data)
end
... def thing_params
ret = params.require(:thing).permit(:description)
ret.merge( { file_data: params[:thing][:file_data].read, file_name: params[:thing][:file_data].original_filename, content_type: params[:thing][:file_data].content_type } )
end
...

Credit to this article for the headers to set in the #download method. Change the controller excerpts to look like this:

# app/controlles/things_controller.rb...  def download
response.set_header('Content-Disposition', "attachment; filename=\"#{@thing.document.metadata['filename']}\"")
response.content_type = @thing.document.metadata['mime_type']
response.write(@thing.document.read)
end
... def thing_params
ret = params.require(:thing).permit(:description, :document)
end
...

Next up are the views. Update the form view from this:

# app/views/things/_form.html.erb...  <div>
<%= form.label :file_data, style: "display: block" %>
<%= form.file_field :file_data %>
</div>
...

to look like this:

# app/views/things/_form.html.erb...  <div>
<%= form.label :file_data, style: "display: block" %>
<%= form.file_field :document %>
</div>
...

Update the view for viewing a thing to look like this:

# app/views/things/_thing.html.erb<div id="<%= dom_id thing %>">
<% if thing.document %>
<p>
<strong>File name:</strong>
<%= link_to thing.document.metadata['filename'], download_thing_path(thing) %>
</p>
<p>
<strong>Content type:</strong>
<%= thing.document.metadata['mime_type'] %>
</p>
<p>
<strong>File data length (bytes):</strong>
<%= thing.document.metadata['size'] %>
</p>
<% end %>

<p>
<strong>Description:</strong>
<%= thing.description %>
</p>
</div>

The usual warning applies: don’t use this in production. It’s ugly. It’s not following best-practices. You get the idea. The same warnings I’ve used with the other articles follow here… the purpose is to show how to get Shrine working with OCI Object Storage — nothing more, nothing less.

Experiencing it

With all of the changes made, we should be ready to check it out for ourselves! Go ahead and run it to see it in action:

$ rails s

When you’re all done playing with it, clean things up by going to a Rails Console (rails c) and running:

Thing.destroy_all

That’s it!

Resources

The following are some of the links that I found helpful and useful:

Conclusion

This was a pretty clean and straightforward implementation, largely due to the usage of the AWS S3 SDK (which was also used by Rails Active Storage — see the sample implementation). The documentation was straightforward and fairly complete, in my opinion. Cleanup was super easy and it didn’t use a lot of extra overhead (again, in my opinion).

Thanks for following along with this series!

--

--

Tim Clegg

Polyglot skillset: software development, cloud/infrastructure architecture, IaC, DevSecOps and more. Employed at Oracle. Views and opinions are my own.