active storage - multiple services - direct upload not working as expected - ruby-on-rails

I have two services defined in the storage.yml
amazon:
service: S3
bucket: bucket1
region: eu-central-1
access_key_id: 321
secret_access_key: 321
unsafe_files:
service: S3
bucket: unsafe-files
region: eu-central-1
access_key_id: 123
secret_access_key: 123
I use the amazon service for some files and I wanted to use the unsafe_files service for other files so that I can put the into another s3 bucket.
The models that use the unsafe_files service look like that (very simple):
class Customer < ActiveRecord::Base
belongs_to :customer_image
end
class CustomerImage < Image
has_one_attached :file, service: Rails.configuration.settings[:unsafe_files_service]
end
Rails.configuration.settings[:unsafe_files_service] is just unsafe_files
in rails application.rb I'm setting
config.active_storage.service = :amazon so that by default it uses the amazon service defined in storage.yml
Now I want to direct upload the files using form_with, it's also quite simple:
<%= form_with model: [#customer, CustomerImage.new] do |form| %>
<%=
form.file_field :file,
accept: 'image/jpeg',
direct_upload: true,
multipart: true
%>
<%= form.submit "submit" %>
<% end %>
now I know (or I think I know) how direct upload works under the hood in rails when using active_storage.
Some javascript listens for the on form submit event and first takes what's in the file_field and sends it to the DirectUploadsController which checks the image, generates the direct upload url and then we directly upload the file to the specified service. It's all here: https://github.com/rails/rails/blob/6ecf1065da57360bdc9f1d85e2c2d9314dcb79e0/activestorage/app/controllers/active_storage/direct_uploads_controller.rb#L14
The service responds with the file key or id.
Afterwards the form submission continues and we save the id that we have received and that's how the relationship between the file.
But when we hit the DirectUploadsController and we create the blob it gets the default service_name (see: https://github.com/rails/rails/blob/6ecf1065da57360bdc9f1d85e2c2d9314dcb79e0/activestorage/app/models/active_storage/blob.rb#L115)
So long story short, when using direct upload I can't choose a service I have to rely on rails default service.
Is there a workaround for that? Or maybe I have missed something?

It's a known issue. This PR is for the issue, but it was reverted due to another topic.
My workaround for this issue is to override blob_args method in DirectUploadsController.
# app/overrides/direct_uploads_controller_override.rb
ActiveStorage::DirectUploadsController.class_eval do
alias_method :old_blob_args, :blob_args
def blob_args
old_blob_args.merge(service_name: params[:service])
end
end
Load the override:
# config/application.rb
# ...
overrides = "#{Rails.root}/app/overrides"
Rails.autoloaders.main.ignore(overrides)
config.to_prepare do
Dir.glob("#{overrides}/**/*_override.rb").each do |override|
load override
end
end
Instead of using the direct_upload option, manually set the data attribute data-direct-upload-url. For example:
<%= form.file_field :file, multipart: true,
"data-direct-upload-url" => rails_direct_uploads_url(service: "unsafe_files") %>

If you want to call create_before_direct_upload! yourself (so that you can specify the service_name) then I think you must override DirectUploadsController somehow. You could write your own controller, or (advanced) try to re-open and patch DirectUploadsController. My app does the former.
Update from the OP:
I ended up re-opening DirectUploadsController (and also adding an option to the form file_field to pass a custom direct_upload_url It works nicely although I don't like that I had to mingle with internals soo much. I've checked active_storage gem and it seems that one would have to just add this feature cause with current implementation there is no workaround. – beniutek

Related

Rails Active Storage without model

Can Rails Active Storage be used without a model backing it? I have a form where I need a file to be uploaded but I don't wish it to be attached to a model. I just need the file uploaded so I can process it with a background job and then delete it.
Yes.
When ActiveStorage is backed by a model, there are two parts involved:
An ActiveStorage::Blob record that holds the file info in the active_storage_blobs table
An ActiveStorage::Attachment record that connects the blob to the model using the active_storage_attachments table
You can skip creating the ActiveStorage::Attachment record if you call create_and_upload! directly. This will create a unique key, determine the content type, compute a checksum, store an entry in active_storage_blobs, and upload the file:
filename = 'local_file.txt'
file = File.open(filename)
blob = ActiveStorage::Blob.create_and_upload!(io: file, filename: filename)
You can download later with:
blob.download
And delete with:
blob.purge
If you want to skip the storage of the ActiveStorage::Blob entirely, you would need to go go directly to the storage service that manages uploading and downloading files. For example, if you are using Disk storage you'd see something like this:
ActiveStorage::Blob.service
=> #<ActiveStorage::Service::DiskService ...
Then you'd have to generate your own key and do something like:
service = ActiveStorage::Blob.service
key = 'some_unique_key'
service.upload(key, file)
service.download(key)
service.delete(key)
To upload a file without a model you can use form_with and specify a url like:
<%= form_with url: "/uploads", multipart: true do |form| %>
<%= form.file_field :picture %>
<%= form.submit %>
<% end %>
Then on the server you can get the file with:
file = params[:picture]
blob = ActiveStorage::Blob.create_and_upload!(io: file, filename: file.original_filename)
...

Active storage has_many_attached is purging previous uploads

I'm trying to add more files to a has_many_attached, but when I upload a new file the previous file is purged. Uploading multiple files does add multiple files, but they are all purged on the next upload as well. Is this intended behavior? if so, how do I prevent the purging?
log.rb
class Log < ApplicationRecord
has_many_attached :uploads
end
_form.html.erb
<%= form_for #log, remote: true do |f| %>
<%= f.file_field :uploads, multiple: true %>
<% end %>
You can prevent overwriting the existing attachments by adding the following line to config/environments/development.rb, config/environments/test.rb and config/environments/production.rb, as kindly indicated by quantavi in this issue: https://github.com/richardvenneman/trestle-active_storage/issues/41
config.active_storage.replace_on_assign_to_many = false
Apparently in Rails 6 the default behavior when uploading files again is to purge the previously uploaded files. You can find a longer thread about it in this Rails issue which Aarthi linked in a comment. The line above changes this setting so that consecutive uploads append files instead of overwriting the old ones.
(As you may have inferred from the link to the issue I ran into the same issue when using the Trestle admin panel with the complementary trestle-active_storage gem, that adds active storage field support.)
I kept my old assets with an hidden_field tag like this:
<% #product.photos.each do |ph| %>
<%= f.hidden_field :photos, multiple: true, value: ph.signed_id %>
<% end %>
This worked for me

Get path to ActiveStorage file on disk

I need to get the path to the file on disk which is using ActiveStorage. The file is stored locally.
When I was using paperclip, I used the path method on the attachment which returned the full path.
Example:
user.avatar.path
While looking at the Active Storage Docs, it looked like rails_blob_path would do the trick. After looking at what it returned though, it does not provide the path to the document. Thus, it returns this error:
No such file or directory # rb_sysopen -
Background
I need the path to the document because I am using the combine_pdf gem in order to combine multiple pdfs into a single pdf.
For the paperclip implementation, I iterated through the full_paths of the selected pdf attachments and load them into the combined pdf:
attachment_paths.each {|att_path| report << CombinePDF.load(att_path)}
Use:
ActiveStorage::Blob.service.path_for(user.avatar.key)
You can do something like this on your model:
class User < ApplicationRecord
has_one_attached :avatar
def avatar_on_disk
ActiveStorage::Blob.service.path_for(avatar.key)
end
end
I'm not sure why all the other answers use send(:url_for, key). I'm using Rails 5.2.2 and path_for is a public method, therefore, it's way better to avoid send, or simply call path_for:
class User < ApplicationRecord
has_one_attached :avatar
def avatar_path
ActiveStorage::Blob.service.path_for(avatar.key)
end
end
Worth noting that in the view you can do things like this:
<p>
<%= image_tag url_for(#user.avatar) %>
<br>
<%= link_to 'View', polymorphic_url(#user.avatar) %>
<br>
Stored at <%= #user.image_path %>
<br>
<%= link_to 'Download', rails_blob_path(#user.avatar, disposition: :attachment) %>
<br>
<%= f.file_field :avatar %>
</p>
Thanks to the help of #muistooshort in the comments, after looking at the Active Storage Code, this works:
active_storage_disk_service = ActiveStorage::Service::DiskService.new(root: Rails.root.to_s + '/storage/')
active_storage_disk_service.send(:path_for, user.avatar.blob.key)
# => returns full path to the document stored locally on disk
This solution feels a bit hacky to me. I'd love to hear of other solutions. This does work for me though.
You can download the attachment to a local dir and then process it.
Supposing you have in your model:
has_one_attached :pdf_attachment
You can define:
def process_attachment
# Download the attached file in temp dir
pdf_attachment_path = "#{Dir.tmpdir}/#{pdf_attachment.filename}"
File.open(pdf_attachment_path, 'wb') do |file|
file.write(pdf_attachment.download)
end
# process the downloaded file
# ...
end

How to view/download file under a custom subdir in Rails 4?

After uploading a file, we would like to make the file available for view and download. The file could be in common format such as pdf, jpg or xls. After a user clicks the file name, the file is downloaded and opened by local app on user's PC. Here is what we did:
<%= link_to r.file_name, r.storage_subdir + '/' + r.file_name %>
r.storage_subdir returns storage/multi_item_orderx_order/15. storage is a subdir under root. r.file_name returns IMG_3002.jpg
When clicking the file name, there pops up an error:
No route matches [GET] "/upload/storage/multi_item_orderx_order/15/IMG_3002.jpg"
(Not sure how upload was put in front of storage). Used save link as to download and the file is only a partial in size and there shows nothing after clicking it. What is the right way to both view and download a file? Or at least to download a file.
If you don't understand how to render uploaded file then you uploading it wrong. Here is a small tutorial for you.
How to implement file upload function in your rails application
First of all add this gems to your Gemfile and then execute bundle install in console.
gem "rmagick"
gem "carrierwave"
Let's imagine that you have model User and you need to give users ability to upload profile picture. When bundler install successfuly installed new gems we need to create Uploader via generator.
rails g uploader avatar
And let's configure it:
class AvatarUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
storage :file
def store_dir
"uploads/#{mode.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
version :thumb do
process resize_to_limit: [64, 64]
end
end
Few words about this configuration: we said carrierwave to use RMagick gem, said which storage we would like to use and where data must be stored and implemented resize for thumbnails.
Now need to add column avatar to users table. Let's create migration:
rails g migration add_avatar_to_users avatar:string
And now lets mount our uploader in User model:
class User < ActiveRecord::Base
mount_uploader :avatar, AvatarUploader
end
That's all! Carrierwave configured and ready to use in your application! Now you can create view like:
<%= form_for #user, html: {multipart: true} do |f| %>
<%= f.file_field :avatar %>
<%= f.submit %>
<% end %>
This form will give you ability to upload avatar. In controller you need to pass params[:user][:avatar] to model to create\update user's avatar.
And if you need to render image to view you can use something like this:
<%= image_tag #user.avatar_url(:thumb) %>
or
<%= image_tag #user.avatar.thumb.url %>
Don't forget to install imagemagick headers via system's package manager (apt-get \ yum \ pacman \ brew \ etc) because if you will not do it, bundler will fall.
If you got any questions, check out carrierwave github page and carrierwave documentation.

File upload to box.com without writing to file system

I am trying to upload a file to Box.com using its API REST call and the httmultiparty gem. The code is working and uploads to Box.com but does that after writing the uploaded file to the server file system as in f.write(data.read) then capturing the file path for the written file as the input parameter to the Box.com API REST call as in :filename => File.new(path). The app will be running on Heroku, so we can't save any files (read only) on Heroku's server so I would like to directly upload the file to Box.com while bypassing the writing of the file on the server but can't figure that out given that the Box.com REST call requires an object of type "File". Any help is appreciated. Thanks.
The model and view code is:
###
#The Model
###
class BoxUploader
require 'httmultiparty'
include HTTMultiParty
#base_uri 'https://api.box.com/2.0'
end
class File < ActiveRecord::Base
attr_accessible :file
attr_accessor :boxResponse
FILE_STORE = File.join Rails.root, 'public', 'files'
API_KEY = #myBoxApiKey
AUTH_TOKEN = #myBoxAuthToken
def file=(data) #uploaded file
filename = data.original_filename
path = File.join FILE_STORE, filename
#### would like to bypass the file writing step
File.open(path, "wb") do |f|
f.write(data.read)
end
#############
File.open(path, "wb") do |f|
boxResponse = BoxUploader.post('https://api.box.com/2.0/files/content',
:headers => { 'authorization' => 'BoxAuth api_key={API_KEY&auth_token=AUTH_TOKEN' },
:body => { :folder_id => '911', :filename => File.new(path)}
)
end
end
###
# The View
###
<!-- Invoke the Controller's "create" action -->
<h1>File Upload</h1>
<%= form_for #file, :html => {:multipart=>true} do |f| %>
<p>
<%= f.label :file %>
<%= f.file_field :file %>
</p>
<p>
<%= f.submit 'Create' %>
<% end %>
To upload a file from memory with HTTMultiParty, you need to supply it with an UploadIO object in place of the File object you'd normally give it. The UploadIO object can be populated using StringIO. It seems HTTMultiParty handles UploadIO objects in a special way, so you can't use the StringIO directly:
class Uploader
include HTTMultiParty
base_uri "http://foo.com"
end
string_io = StringIO.new('some stuff that pretends to be in a file')
upload_io = UploadIO.new(string_io, 'text/plain', 'bar.txt')
Uploader.post("/some/path", query: {file: upload_io})
You are aiming at a non-common use pattern, so your best shot could be to extend the existant gem, to provide the functionality you need.
There is a gem ruby-box to use with Box service at the 2.0 version of their API.
The gem is well supported and pretty easy to use.
You'll need to dig on the source code and create a new upload method.

Resources