Downloading S3 files(objects) using aws-sdk for ruby in rails - ruby-on-rails

I am not using paperclip or carrierwave or any other gems for interaction with amazon web services s3. In fact, I am not using any models, just directly interacting with S3 objects.
Can someone please provide some code as to how to directly download objects(files) from AWS S3
This is my code so far :
View:
<h2>Download Files</h2>
<%#root_files.each do |file| %>
<% next if #obj %>
<td>Filename: <b><%= file %></b></td>
<td><%= link_to "Download", download_url_for(file) %></td>
<% end %>
Corresponding Controller:
def xxx
bucket = AWS::S3.new.buckets[ ENV["BUCKET"]]
#root_files = bucket.as_tree.children.select(&:leaf?).collect(&:key)
end
download_url_for method:
def download_url_for(file_key)
s3 = AWS::S3.new
bucket = s3.buckets[ ENV["BUCKET"]]
object = bucket.objects[file_key]
File.open('xxxxx', 'wb') do |file|
object.read do |chunk|
file.write(chunk)
end
end
end
I need to be able to save the downloaded file on any browsable non-pretedetermined location on my desktop.Id really appreciate view on how to modify the download_url_for method to achieve this.
=====================================
I just tried using
<td><%= link_to "Download",object.value.url_for(:read).to_s %></td>
But on downloading I only get a zip file with xml content inside it, not the original file (word document in my case) --- any thoughts on how to modify this so as to get the actual file type and contents? Thanks
=======================Heres my code for uploading the file
View code:
<%= form_tag({:action => 'create_file'}, multipart: true) do %>
<%= file_field_tag 'uploaded_file' %><br />
<%= submit_tag 'Upload' %> <br />
<% end %>
Controller action - create_file action
def create_file
s3 = AWS::S3.new
bucket = s3.buckets[ENV["BUCKET"]]
key = params[:uploaded_file].original_filename
object = bucket.objects[key]
object.write(params[:uploaded_file].read)
end
Can someone suggest as to how I can check the MIME type or preserve the filename while uploading if thats the issue as mentioned by #Frederick Cheung? thanks

You'd probably be better off providing a url for users to download directly from S3 rather than trying to stream the file through your Rails app.
The aws-sdk provides the url_for method to accomplish this: http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/S3Object.html#url_for-instance_method. It takes a bunch of options for expiration, security, caching, etc. A basic example:
<td><%= link_to "Download", file.url_for(:read) %></td>
--EDIT--
To access the url_for method in the view, you'll need references to the s3 objects. So in your controller, you want to collect objects from the leaf nodes, not keys:
#root_files = bucket.as_tree.children.select(&:leaf?).collect(&:object)

Related

Handle raise ActiveStorage::InvariableError after Deleting a file that uploaded with active storage

Hello i uploaded an image with active storage specificly an avatar for my model users, but after of upload this image, i delete this of the folder that contained the file. then, i have one attachment that exists without file.
i am checking user.avatar.blob (the user with the deleted file), and then this return null (with the console), i am try to handle this inside of a view but dont works, any advice for handle this exception?
<% unless !!user.avatar.blob.nil? || user.avatar.attached? %>
#this a simple avatar api that show for default
# an image when dont exist attachement or the file dont works or dont exist
<%= image_tag "https://avatars.dicebear.com/4.5/api/#{user.gender}/#{user.username}.svg?mood[]=happy&mood[]=happy", :size => "100x100" %>
<% else %>
<%= image_tag user.avatar.variant(
resize_to_limit: [100, 100]
)%>
<% end %>

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

Carrierwave multiple image upload

I'm using multiple file uploads from the master branch and PostgreSQL
My product model hast a string field called "images" and I can attach multiple images just fine.
What I can't figure out though, how can I remove one image from the products?
I can remove all images as described in the docs:
product.remove_images!
product.save
but didn't find a way how to remove a single image.
Have you considered using a nested form for to try to delete an image?
Here is a piece of code from carrierwave's github site...
<%= form_for #product, html: { multipart: true } do |f| %>
.
.
.
<label>
<%= f.check_box :remove_images %>
Remove images
</label>
<% end %>
...which I am sure you have seen. Although, of course, calling remove_images! would not work in your case, since that implies a unified action on all images.
Try the following modification of the above and see if it helps you sort this problem and target each image individually...
<%= nested_form_for #product, :html=>{ :multipart => true } do |f| %>
.
.
.
<%= f.fields_for :images do |product_form| %>
<%= product_form.link_to_remove "Remove this image" %>
<% end %>
<% end %>
To make this work make sure to include gem 'nested_form', '~> 0.3.2' in your Gemfile.
Hopefully this helps you out.
I learn from #Cris and use the following code snippet when working with S3.
def remove_image_at_index(index)
remain_images = #product.images # copy the array
deleted_image = remain_images.delete_at(index) # delete the target image
deleted_image.try(:remove!) # delete image from S3
#product.images = remain_images # re-assign back
end
I wrote a blog post about this , and create a sample project with more concrete codes.
You should be able to use remove_image on the specified image (from the array). So you'll need to somehow identify the image first (ex. images[0].remove_image!)
adding something like this to the product model does the trick.
def delete_image(idx)
remaining_images = []
self.images.each_with_index { |img,ii| remaining_images<<File.open(img.path) unless idx == ii}
self.images=remaining_images
end
Carrierwave also takes care of removing the file after saving the record.

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.

file upload without activerecord

How do you handle file upload in rail without attaching them to active record ?
I just want to write the files to the disk.
Thanks,
If I understand correctly what you need then the most simple example would be this:
The controller:
class UploadController < ApplicationController
def new
end
def create
name = params[:upload][:file].original_filename
path = File.join("public", "images", "upload", name)
File.open(path, "wb") { |f| f.write(params[:upload][:file].read) }
flash[:notice] = "File uploaded"
redirect_to "/upload/new"
end
end
The view:
<% flash.each do |key, msg| %>
<%= content_tag :div, msg, :class => [key, " message"], :id => "notice_#{key}" %>
<% end %>
<% form_tag '/upload/create', { :multipart => true } do %>
<p>
<%= file_field_tag 'upload[file]' %>
</p>
<p>
<%= submit_tag "Upload" %>
</p>
<% end %>
This would let you upload any file without any checks or validations which in my opinion isn't that usefull.
If I would do it myself then I would use something like validatable gem or tableless gem just tableless is not supported anymore. These gems would allow you to validate what you're uploading to make it more sane.
You can just move the temporary file to destiny path using FileUtils
tmp = params[:my_file_field].tempfile
destiny_file = File.join('public', 'uploads', params[:my_file_field].original_filename)
FileUtils.move tmp.path, destiny_file
The Tempfile documentation shows an example that's equivalent to Rytis's code, which is fine most of the time. But when you call tempfile.read, Ruby is reading the whole file as a single chunk into memory, which is sub-optimal.
However, FileUtils provides a copy_stream method, and IO, at least in Ruby 2.0, provides a copy_stream implementation that handles writing directly to a filepath (FileUtils.copy_stream requires File-like objects on both sides, or so say the docs).
In my case, I was initiating a large multi-file upload via AJAX, and wanted to avoid reading the whole file(s) into Ruby's memory before writing to disk.
In the example below, params[:files] is an Array of ActionDispatch::Http::UploadedFile instances, and local_filepath is a string pointing to a non-existing file in an existing directory. For brevity, I'll assume I'm only uploading one file:
IO.copy_stream(params[:files][0].tempfile, local_filepath)
The ActionDispatch::Http::UploadedFile instance has a .tempfile field that's just a regular Tempfile instance.
I'm not actually sure that Ruby still isn't reading the whole file into memory—I didn't benchmark anything—but it's a lot more possible than it is with the localfile.write(tempfile.read) syntax.
tl;dr: IO.copy_stream(your_tempfile, your_disk_filepath) is more concise, if not faster.
You could try using the Rails plugin Attachment_fu to handle file uploads. It allows you to save uploads to the file system instead of the database.

Resources