In a past post, we have learned to attach base64 file on ActiveStorage. Now we will learn to attach a file URL to ActiveStorage.
Use case
In my current job, we maintain REST APIs consumed by 3rd party companies with their own developers.
There is an endpoint that needs a valid ID to be accessed, what those 3rd party companies pass to us is a string
of the file URL where it can be downloaded.
Now we would need to make it an attachment to Rails ActiveStorage.
Another use case that I have encountered is when we migrated a CSV of products from a Shopify store to our Rails app, when you export Shopify products as CSV it adds a Image Src
column that contains that product’s image URL.
When migrating we need to attach that image URL in our Rails app ActiveStorage.
Let’s initialize a blank Rails app with a model that has an image attachment.
Step 1: Initialize our Rails app
You may skip this step if you already have a Rails app running.
1
2
3
4
5
$ rails new test-app
$ cd test-app
$ bundle exec rails g scaffold post
$ bundle exec rails active_storage:install
$ bundle exec rails db:migrate
What those commands above do?
rails new test-app
- initializes a blank Rails app namedtest-app
.cd test-app
- navigating into our new Rails app.bundle exec rails g scaffold post
- scaffolded aPost
model CRUD.bundle exec rails active_storage:install
- installingActiveStorage
.bundle exec rails db:migrate
- running all pending database migrations.
Step 2: Add attachment to our Post model
In your app/models/post.rb
add the following association.
1
2
3
class Post < ApplicationRecord
has_one_attached :image
end
This creates an association between our Post
model and ActiveStorage
.
Step 3: Create a sample Post record
Open our Rails console.
1
$ bundle exec rails c
Create a sample Post
record.
1
Post.create
Step 4: Attach an image URL to our Post model
We will use this website’s favicon for our sample image URL to be attached.
Open our Rails console again.
1
$ bundle exec rails c
On our Rails console, we should require OpenURI
module.
1
require 'open-uri'
We’ll now assign the content of the file URL to our file
variable.
1
file = URI.open('https://www.davidangulo.xyz/assets/img/favicons/favicon-16x16.png')
Then, we’ll extract the mime_type
.
1
mime_type = file.content_type
There are cases that this is not served by the server, or the server does not serve that real mime type and spoof us. There are gems that can help us sniff the mime type of those files such as ruby-filemagic and fastimage.
Then we’ll use strict_encode64
encode it in base64.
1
encoded_file = Base64.strict_encode64(file.read)
We used strict_encode64
so that it doesn’t add \n
to our base64 string
unlike encode64
.
Then we’ll decode the encoded_file
.
1
decoded_file = Base64.decode64(encoded_file)
Generate filename with extension. It’s up to you on how you will generate your filename, for simplicity we’ll just use RANDOM-UUID
+ extension
.
1
2
extension = Rack::Mime::MIME_TYPES.invert[mime_type]
filename = [SecureRandom.uuid, extension].join
We used Rack::Mime::MIME_TYPES
to get the extension based on the file’s mime_type
and SecureRandom.uuid
to generate a random UUID.
You may also read: Advantages and Disadvantages of UUID
Now we can attach the sample image to our record.
1
2
3
4
5
Post.last.image.attach(
io: StringIO.new(decoded_file),
filename: filename,
content_type: mime_type
)
Step 5: DRY code.
Instead of copy-pasting the codes above every time, we can DRY our code a bit.
In your app/models/concerns/attachable.rb
, paste the following. (create if it does not exist)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'open-uri'
module Attachable
def attach_file!(file_url, association)
file = URI.open(file_url)
mime_type = file.content_type
encoded_file = Base64.strict_encode64(file.read)
decoded_file = Base64.decode64(encoded_file)
extension = Rack::Mime::MIME_TYPES.invert[mime_type]
filename = [SecureRandom.uuid, extension].join
self.send(association).attach(
io: StringIO.new(decoded_file),
filename: filename,
content_type: mime_type
)
end
end
We just created an Attachable
module that we can include in our models.
In your app/models/post.rb
, we can now do:
1
2
3
4
5
class Post < ApplicationRecord
include Attachable
has_one_attached :image
end
Then we can attach file to our Post
record like the following code:
1
2
3
file_url = 'https://www.davidangulo.xyz/assets/img/favicons/favicon-16x16.png'
post = Post.last # or any Post record
post.attach_file!(file_url, :image)
Bonus
We could also set a custom directory where our files will be saved.
1
2
3
4
5
6
self.send(association).attach(
+ key: ['custom-directory', filename].join('/'),
io: StringIO.new(decoded_file),
filename: filename,
content_type: mime_type
)
That’s it. We now learned to attach a file URL to ActiveStorage in Rails.