Download file in service object with ActiveStorage - ruby-on-rails

A user uploads a document and this gets stored in Azure with ActiveStorage. The next step is that the backend processes this and therefore I have a service object to do this. So I need to download the file from Azure to the tmp folder within the Rails app. How do I download the file? I cannot use rails_blob_url because it is not available in a service object, only in controllers and views.
When I still used Paperclip I did something like this:
require 'open-uri'
file = Rails.root.join('tmp', user.attachment_file_name)
name = user.attachment_file_name
download = open(user.attachment.url)
download_result = IO.copy_stream(download, file)
How can I do something similar with ActiveStorage?

You can use ActiveStorage::Blob#open:
Downloads the blob to a tempfile on disk. Yields the tempfile.
Given this example from the guides:
class User < ApplicationRecord
has_one_attached :avatar
end
You can do this with:
user.avatar.open do |tempfile|
# do something with the file
end
If its has_many_attached you of course need to loop through the attachments.
See:
Active Storage Overview

Related

Seeding multiple image attachments from Amazon S3 in Rails

I am trying to seed multiple image attachments to a model. I have been using this link but I am still sort of stuck since what I aim to do differs a little since:
I am trying to attach multiple images to each object (which I seed) in the model
I want to retrieve these images from my S3 bucket and attach them to the objects (is this possible?)
Here's my seed.rb:
shirt = Item.create(name:"Basic Shirt",price:19.99)
skirt = Item.create(name:"Basic Skirt",price:29.99)
sweater = Item.create(name:"Basic Sweater",price:39.99)
kid_hood = Item.create(name:"Basic Kid Hoodie",price:19.99)
# somehow attach images here?
I am using the aws-sdk-s3 gem in order to connect Active Storage to my S3 bucket. Please tell me if any additional files are needed for viewing. I will happily edit this post to include it.
ActiveStorage work on plain byte streams, so you can download the file (using open-uri for instance) and assign the stream as the content of the attachment.
Assuming you have the following (adapt if different)
class Item < ApplicationRecord
has_one_attached :photo
end
you can have your seeds as:
require 'open-uri'
shirt = Item.create(name:"Basic Shirt",price:19.99)
shirt.photo.attach(io: open('your-s3-nonexpiring-url'), filename: 'foo.bar')
# ...
Just a note: as of Ruby 3.0, you will need to call URI.open instead of open. See the reference to open-uri here.

Rails Active Storage set folder to store files

I'm using Active Storage to store files in a Rails 5.2 project. I've got files saving to S3, but they save with random string filenames and directly to the root of the bucket. I don't mind the random filenames (I actually prefer it for my use case) but would like to keep different attachments organized into folders in the bucket.
My model uses has_one_attached :file. I would like to specify to store all these files within a /downloads folder within S3 for example. I can't find any documentation regarding how to set these paths.
Something like has_one_attached :file, folder: '/downloads' would be great if that's possible...
The ultimate solution is to add an initializer. You can add a prefix based on an environment variable or your Rails.env :
# config/initializer/active_storage.rb
Rails.configuration.to_prepare do
ActiveStorage::Blob.class_eval do
before_create :generate_key_with_prefix
def generate_key_with_prefix
self.key = if prefix
File.join prefix, self.class.generate_unique_secure_token
else
self.class.generate_unique_secure_token
end
end
def prefix
ENV["SPACES_ROOT_FOLDER"]
end
end
end
It works perfectly with this. Other people suggest using Shrine.
Credit to for this great workaround : https://dev.to/drnic/how-to-isolate-your-rails-blobs-in-subfolders-1n0c
As of now ActiveStorage doesn't support that kind of functionality. Refer to this link. has_one_attached just accepts name and dependent.
Also in one of the GitHub issues, the maintainer clearly mentioned that they have clearly no idea of implementing something like this.
The workaround that I can imagine is, uploading the file from the front-end and then write a service that updates key field in active_storage_blob_statement
There is no official way to change the path which is determined by ActiveStorage::Blob#key and the source code is:
def key
self[:key] ||= self.class.generate_unique_secure_token
end
And ActieStorage::Blog.generate_unique_secure_token is
def generate_unique_secure_token
SecureRandom.base36(28)
end
So a workaround is to override the key method like the following:
# config/initializers/active_storage.rb
ActiveSupport.on_load(:active_storage_blob) do
def key
self[:key] ||= "my_folder/#{self.class.generate_unique_secure_token}"
end
end
Don't worry, this will not affect existing files. But you must be careful ActiveStorage is very new stuff, its source code is variant. When upgrading Rails version, remind yourself to take look whether this patch causes something wrong.
You can read ActiveStorage source code from here: https://github.com/rails/rails/tree/master/activestorage
Solution using Cloudinary service
If you're using Cloudinary you can set the folder on storage.yml:
cloudinary:
service: Cloudinary
folder: <%= Rails.env %>
With that, Cloudinary will automatically create folders based on your Rails env:
This is a long due issue with Active Storage that seems to have been worked around by the Cloudinary team. Thanks for the amazing work ❤️
# config/initializers/active_storage.rb
ActiveSupport.on_load(:active_storage_blob) do
def key
sql_find_order_id = "select * from active_storage_attachments where blob_id = #{self.id}"
active_storage_attachment = ActiveRecord::Base.connection.select_one(sql_find_order_id)
# this variable record_id contains the id of object association in has_one_attached
record_id = active_storage_attachment['record_id']
self[:key] = "my_folder/#{self.class.generate_unique_secure_token}"
self.save
self[:key]
end
end
Active Storage by default doesn't contain a path/folder feature but you can override the function by
model.file.attach(key: "downloads/filename", io: File.open(file), content_type: file.content_type, filename: "#{file.original_filename}")
Doing this will store the key with the path where you want to store the file in the s3 subdirectory and upload it at the exact place where you want.

CarrierWave: allowing one-by-one file uploads as well as a bulk zip upload

I'm using CarrierWave to upload and manage resources on my ActiveRecord models. I've defined my own Uploader and mounted it to a bunch of properties on one of my models as shown below:
class Theme < ActiveRecord::Base
...
mount_uploader :masthead, ThemeResourceUploader
mount_uploader :background, ThemeResourceUploader
mount_uploader :footer, ThemeResourceUploader
...
end
This works as expected when creating a new Theme from the params in my Rails controller, but in addition to allowing the user to upload one image at a time I also want to allow them to upload an zip file containing all these images and then use this zip to construct the Theme.
To try and accomplish this I created a new Uploader for the zip file and a controller method which uses Rubyzip to extract the uploaded zip in memory and then tries to assign the resultant stream to my ActiveRecord model's properties.
def import
require 'zip'
#theme = Theme.new
zip_upload = params.require(:theme).require(:zip)
uploader = ThemeImportUploader.new
uploader.cache!(zip_upload)
Zip::File.open(uploader.file.path) do |zip_file|
#theme.masthead = zip_file.get_input_stream('masthead.png')
#theme.background = zip_file.get_input_stream('background.png')
#theme.footer = zip_file.get_input_stream('footer.png')
end
#theme.save
end
Unfortunately this doesn't work. I don't receive any error or failure, but the Theme is saved with empty values for the resources and the files are not created in my upload folder.
I believe I can get this working by extracting the zip to temporary files and then reading those files into the CarrierWave properties, but this seems like a very round the bush way of solving the problem.
How can I upload and extract a zip in memory and assign its contents to my CarrierWave enhanced models?

Carrierwave + Fog + S3 remove file without going through a model

I am building an application that has a chat component to it. The application allows users to upload files to the chat. The chat is all javascript but i wanted to use Carrierwave for the uploads because i am using it elsewhere in the application. I am doing the handling of the uploads through AJAX so that i can get into Rails land and let Carrierwave take over.
I have been able to get the chat to successfully upload the files to the correct location in my S3 bucket. The thing i can't figure out is how to delete the files. Here is my code the uploads the files - this is the method that is called from the route that the AJAX call hits.
def upload
file = File.open(params[:file_0].tempfile)
uploader = ChatUploader.new
uploader.store!(file)
end
There is little to no documentation with Carrierwave on how to upload files without going through a model and basically NO documentation on how to remove files without going through a model. I assume it is possible though - i just need to know what to call. So i guess my question is how do i delete files?
UPDATE (11/23)
I got the code to save and delete files from S3 using these methods:
# code to save the file
def upload
file = File.open(params[:file_0].tempfile)
uploader = ChatUploader.new
uploader.store!(file)
uploader.store_path()
end
# code to remove files
def remove_file
file = params[:file]
uploader = ChatUploader.new
uploader.retrieve_from_store!(file)
uploader.remove!
end
My only issue now is that the filename for the uploaded file is not correct. It saves all files with a "RackMultipart" and then some numbers which look like a date, time, and identifier? (example: RackMultipart20141123-17740-1tq4j1g) Need to try and use the original filename plus maybe a timestamp for uniqueness.
I believe it has something to do with these two lines:
file = File.open(params[:file_0].tempfile)
and
uploader.store!(file)

Rails - Run an external program on a Paperclip attachment for processing and save the output attachment back to the model

In my rails project, I need the user to upload a file (input_file) which I will process using an external application. Once, it is completed, I want to attach the processed file to the same model as a different attachment (output file).
I have been able to create a form and use paperclip to allow the user to upload the input_file to my model FileProcessor. Im not sure on the next step as to how do I call an executable on the input_file and save it as output_file.
Based on paperclip, once the file is upload, I can access the path via input_file.path
output_file = %w{external_app input_file.path out_file_name}
Class FileProcessor
has_attached_file :input_file
has_attached_file :output_file
Im confused as to where this call to run the external app be placed? in the model or in the controller (def create). Also, how do I work with paperclip to associate the output_file with the model without actually uploading.
The location for such code depends on what kind of business your external process does. With the requirements as depicted in the question, it would be as simple as this:
class FileProcessor < ActiveRecord
...
after_validation do |fp|
tmp_file = "/tmp/#{rand}"
system "/usr/bin/awesome.sh #{fp.input_file.path} > #{tmp_file}"
fp.output_file = File.open(tmp_file)
end
...
end
I hope, this is what you are looking for.

Resources