Rails Active Storage without model - ruby-on-rails

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)
...

Related

active storage - multiple services - direct upload not working as expected

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

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 can I parse a local CSV file with Rails?

I'm trying to use smarter_csv to parse csv files with my Rails app. But the documentation only explains how to parse a file that already belongs to the app.
I want to parse a file that's stored locally on my computer. So I think I have to upload the file, parse it, and then delete it.
This is how far I got:
<%= form_tag({action: :upload}, multipart: true) do %>
<%= file_field :csv %>
<%= submit_tag 'Submit' %>
<% end %>
So then how can I reference and use the uploaded file in my controller action?
def upload
#save file temporarily to app
filename = #filename
#parse file with smarter_csv
#File.delete(filename)
end
To get the file path as a string you need to do the following:
filename = params[:csv].path
as params[:csv] is a UploadedFile object. You don't need handle the temp file yourself, i.e. storing and deleting it. Rails would do that for you. As per documentation:
Uploaded files are temporary files whose lifespan is one request. When the object is finalized Ruby unlinks the file, so there is no need to clean them with a separate maintenance task.

CarrierWaveDirect Without Processing

I have a document model that consists of PDF files. Because I don't need to manipulate the PDFs in any way, I'm attempting to use CarrierWaveDirect without its processing step, which I believe is downloading and re-uploading the files.
My uploader looks like this:
class DocumentUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
include CarrierWave::MimeTypes
include CarrierWaveDirect::Uploader
def will_include_content_type
true
end
default_content_type 'application/pdf'
allowed_content_types = %w(application/pdf)
def store_dir
prefix = Rails.env.production? ? '' : 'tmp/'
"#{prefix}files/documents"
end
def extension_white_list
%w(pdf)
end
end
I define the document (in the controller) as:
#document = Document.new.filename
#document.success_action_redirect = new_document_url(:step => 2)
I'm using the direct upload form to upload the file itself, which is working fine.
<%= direct_upload_form_for #document do |f| %>
<%= f.file_field :filename, :required => true %>
<%= f.submit "Upload Document" %>
<% end %>
When I get the key back, I create an attribute called filename_key, and a callback in my model looks for this attribute to update the column.
Controller:
key = params[:key].split('/').last(2).join('/')
#document = Document.new(:filename_key => key)
Model:
after_save :check_for_file
def check_for_file
unless self.filename_key.blank?
update_columns(:filename => self.filename_key.to_s)
end
end
This actually all works fine. The problem is when I go to save the record again, I get this error:
ActiveRecord::StatementInvalid: Mysql2::Error: Data too long for column 'filename' at row 1: UPDATE `documents` SET `filename` = '--- &1 !ruby/object:DocumentUploader\nmodel: !ruby/object:Document\n attributes:\n id: 92\n ...
It's trying to set the entire contents of the attribute as the attribute itself. My first guess is that I am bypassing vital after_create or after_save callbacks, but I can't get to saving a file without processing unless I avoid these callbacks.
Any suggestions of where to look next are appreciated in advance!
I found I had a asked a similar question almost a year ago. It turns out the solution was the same for both.
CarrierWaveDirect uses filename as a method to (you guessed it!) work with the filename of the uploaded file. For me, changing the name of the column to document solved my issue.
I should also note that I've found ignoring the processing step to work fine. However, you do lose anything that would be called during that step, for example, setting the content type. As I've shown in the question, I'm setting the content type automatically, which enables me to still view the PDF files in the browser, since they are set as application/pdf vs. binary/octet-stream.

Carrierwave Temporary File Without Model

I need to be able to attach a file to a mail (using Mailer) for a recently uploaded file which is not linked to any Model.
In the code that goes for the upload form:
<%= form_for(:mail, :url => {:action => 'send_mail'}, :html => {:multipart => true}) do |f| %>
<table summary="send_table">
<tr>
<th>Attachment</th>
<td><%= f.file_field(:attachment) %><a id="attachment"></a></td>
</tr>
</table>
<%= submit_tag "Send!" %>
Now, what I am looking into doing in the send_mail action is something like:
MyMailer.send_mail(params[:mail][:attachment]).deliver
with params[:mail][:attachment] being the path to the temp file uploaded with the form. How can one do that?
This also implies another question: Should I manually delete the file from the temp once the mail is sent? If yes, how?
Copying the answer from the comments in order to remove this question from the "Unanswered" filter:
Finally nailed it:
unless (params[:mail][:attachment]).nil?
uploader = AttachmentUploader.new
uploader.cache!(params[:mail][:attachment])
#file_name = uploader.filename
#file_data = uploader.file.read()
end
and then
MyMailer.send_mail(#file_name,#file_data)
~ answer per user1563325
I'd like to add that for the scenario described here, maybe you don't need CarrierWave for such a temporary upload situation. When uploading using file_field, Rails saves it into a Tempfile, whose path you can access like this:
params[:mail][:attachment].path
These files are deleted automatically so you shouldn't need to worry about them, as the Rails docs explain:
Uploaded files are temporary files whose lifespan is one request. When the object is finalized Ruby unlinks the file, so there is no need to clean them with a separate maintenance task.

Resources