Downloading and zipping files that were uploaded to S3 with CarrierWave - ruby-on-rails

I have a small Rails 3.2.1 app that uses CarrierWave 0.5.8 for file uploads to S3 (using Fog)
I want users to be able to select some images that they'd like to download, then zip them up and send them a zip. Here is what I've come up with:
def generate_zip
#A collection of Photo objects. The Photo object has a PhotoUploader mounted.
photos = Photo.all
tmp_filename = "#{Rails.root}/tmp/" << Time.now.strftime('%Y-%m-%d-%H%M%S-%N').to_s << ".zip"
zip = Zip::ZipFile.open(tmp_filename, Zip::ZipFile::CREATE)
zip.close
photos.each do |photo|
file_to_add = photo.photo.file
zip = Zip::ZipFile.open(tmp_filename)
zip.add("tmp/", file_to_add.path)
zip.close
end
#do the rest.. like send zip or upload file and e-mail link
end
This doesn't work because photo.photo.file returns an instance of CarrierWave::Storage::Fog::File instead of a regular file.
EDIT: The error this leads to:
Errno::ENOENT: No such file or directory - uploads/photos/name.jpg
I also tried the following:
tmp_filename = "#{Rails.root}/tmp/" << Time.now.strftime('%Y-%m-%d-%H%M%S-%N').to_s << ".zip"
zip = Zip::ZipFile.open(tmp_filename, Zip::ZipFile::CREATE)
zip.close
photos.each do |photo|
processed_uri = URI.parse(URI.escape(URI.unescape(photo.photo.file.authenticated_url)).gsub("[", "%5B").gsub("]", "%5D"))
file_to_add = CarrierWave::Uploader::Download::RemoteFile.new(processed_uri)
zip = Zip::ZipFile.open(tmp_filename)
zip.add("tmp/", file_to_add.path)
zip.close
end
But this gives me a 403. Some help would be greatly appreciated.. It probably is not that hard I'm just Doing it Wrong™

I've managed to solve the problem with help from #ffoeg
The solution offered by #ffoeg didn't work quite so well for me since I was dealing with zip files > 500 MB which caused me problems on Heroku. I've therefor moved the zipping to a background process using resque:
app/workers/photo_zipper.rb:
require 'zip/zip'
require 'zip/zipfilesystem'
require 'open-uri'
class PhotoZipper
#queue = :photozip_queue
#I pass
def self.perform(id_of_object_with_images, id_of_user_to_be_notified)
user_mail = User.where(:id => id_of_user_to_be_notified).pluck(:email)
export = PhotoZipper.generate_zip(id_of_object_with_images, id_of_user_to_be_notified)
Notifications.zip_ready(export.archive_url, user_mail).deliver
end
# Zipfile generator
def self.generate_zip(id_of_object_with_images, id_of_user_to_be_notified)
object = ObjectWithImages.find(id_of_object_with_images)
photos = object.images
# base temp dir
temp_dir = Dir.mktmpdir
# path for zip we are about to create, I find that ruby zip needs to write to a real file
# This assumes the ObjectWithImages object has an attribute title which is a string.
zip_path = File.join(temp_dir, "#{object.title}_#{Date.today.to_s}.zip")
Zip::ZipOutputStream.open(zip_path) do |zos|
photos.each do |photo|
path = photo.photo.path
zos.put_next_entry(path)
zos.write photo.photo.file.read
end
end
#Find the user that made the request
user = User.find(id_of_user_to_be_notified)
#Create an export object associated to the user
export = user.exports.build
#Associate the created zip to the export
export.archive = File.open(zip_path)
#Upload the archive
export.save!
#return the export object
export
ensure
# clean up the tempdir now!
FileUtils.rm_rf temp_dir if temp_dir
end
end
app/controllers/photos_controller.rb:
format.zip do
#pick the last ObjectWithImages.. ofcourse you should include your own logic here
id_of_object_with_images = ObjectWithImages.last.id
#enqueue the Photozipper task
Resque.enqueue(PhotoZipper, id_of_object_with_images, current_user.id)
#don't keep the user waiting and flash a message with information about what's happening behind the scenes
redirect_to some_path, :notice => "Your zip is being created, you will receive an e-mail once this process is complete"
end
Many thanks to #ffoeg for helping me out. If your zips are smaller you could try #ffoeg's solution.

Here is my take. There could be typos but I think this is the gist of it :)
# action method, stream the zip
def download_photos_as_zip # silly name but you get the idea
generate_zip do |zipname, zip_path|
File.open(zip_path, 'rb') do |zf|
# you may need to set these to get the file to stream (if you care about that)
# self.last_modified
# self.etag
# self.response.headers['Content-Length']
self.response.headers['Content-Type'] = "application/zip"
self.response.headers['Content-Disposition'] = "attachment; filename=#{zipname}"
self.response.body = Enumerator.new do |out| # Enumerator is ruby 1.9
while !zf.eof? do
out << zf.read(4096)
end
end
end
end
end
# Zipfile generator
def generate_zip(&block)
photos = Photo.all
# base temp dir
temp_dir = Dir.mktempdir
# path for zip we are about to create, I find that ruby zip needs to write to a real file
zip_path = File.join(temp_dir, 'export.zip')
Zip::ZipFile::open(zip_path, true) do |zipfile|
photos.each do |photo|
zipfile.get_output_stream(photo.photo.identifier) do |io|
io.write photo.photo.file.read
end
end
end
# yield the zipfile to the action
block.call 'export.zip', zip_path
ensure
# clean up the tempdir now!
FileUtils.rm_rf temp_dir if temp_dir
end

Related

Migrating uploaded files from Active Storage to Carrierwave

For a variety of reasons I am migrating my uploads from ActiveStorage (AS) to CarrierWave (CW).
I am making rake task and have the logic sorted out - I am stumped at how to feed the AS blob into the CW file.
I am trying something like ths:
#files.each.with_index(1) do | a, index |
if a.attachment.attached?
a.attachment.download do |file|
a.file = file
end
a.save!
end
end
This is based on these two links:
https://edgeguides.rubyonrails.org/active_storage_overview.html#downloading-files
message.video.open do |file|
system '/path/to/virus/scanner', file.path
# ...
end
and
https://github.com/carrierwaveuploader/carrierwave#activerecord
# like this
File.open('somewhere') do |f|
u.avatar = f
end
I tested this locally and the files are not mounted via the uploader. My question(s) would be:
am I missing something obvious here?
is my approach wrong and needs a new one?
Bonus Karma Question:
I can't seem to see a clear path to set the CW filename when I do this?
Here is my final rack task (based on the accepted answer) - open to tweaks. Does the job for me:
namespace :carrierwave do
desc "Import the old AS files into CW"
task import: :environment do
#files = Attachment.all
puts "#{#files.count} files to be processed"
puts "+" * 50
#files.each.with_index(1) do | a, index |
if a.attachment.attached?
puts "Attachment #{index}: Key: #{a.attachment.blob.key} ID: #{a.id} Filename: #{a.attachment.blob.filename}"
class FileIO < StringIO
def initialize(stream, filename)
super(stream)
#original_filename = filename
end
attr_reader :original_filename
end
a.attachment.download do |file|
a.file = FileIO.new(file, a.attachment.blob.filename.to_s)
end
a.save!
puts "-" * 50
end
end
end
desc "Purge the old AS files"
task purge: :environment do
#files = Attachment.all
puts "#{#files.count} files to be processed"
puts "+" * 50
#files.each.with_index(1) do | a, index |
if a.attachment.attached?
puts "Attachment #{index}: Key: #{a.attachment.blob.key} ID: #{a.id} Filename: #{a.attachment.blob.filename}"
a.attachment.purge
puts "-" * 50
#count = index
end
end
puts "#{#count} files purged"
end
end
Now in my case I am doing this in steps - I have branched my master with this rake task and the associated MCV updates. If my site was in true production would probably run the import rake task first then confirm all went well THEN purge the old AS files.
The file object you get from the attachment.download block is a string. More precisely, the response from .download is the file, "streamed and yielded in chunks" (see documentation). I validated this by calling file.class to make sure the class is what I expected.
So, to solve your issue, you need to provide an object on which .read can be called. Commonly that is done using the Ruby StringIO class.
However, considering Carrierwave also expects a filename, you can solve it using a helper model that inherits StringIO (from blogpost linked above):
class FileIO < StringIO
def initialize(stream, filename)
super(stream)
#original_filename = filename
end
attr_reader :original_filename
end
And then you can replace a.file = file with a.file = FileIO.new(file, 'new_filename')

RubyZip : Unable to find path of file stored in Active Storage

I am using Rails 5.2 and ActiveStorage to let my users upload files in my app. I have all the uploaded files displayed in a table and I can choose to select which ones I want to download. After selecting, I will be able to download all of them in a zip file. To zip the files, I am using Rubyzip and I can't get to work properly.
I have tried two ways :
1 - I tried this way and faced this error No such file or directory # rb_sysopen - /rails/active_storage/blobs/..../2234.pyThis is my controller :
def batch_download
if params["record"].present?
ids = params["record"].to_unsafe_h.map(&:first)
if ids.present?
folder_path = "#{Rails.root}/public/downloads/"
zipfile_name = "#{Rails.root}/public/archive.zip"
FileUtils.remove_dir(folder_path) if Dir.exist?(folder_path)
FileUtils.remove_entry(zipfile_name) if File.exist?(zipfile_name)
Dir.mkdir("#{Rails.root}/public/downloads")
Record.where(id: ids).each do |attachment|
open(folder_path + "#{attachment.file.filename}", 'wb') do |file|
file << open("#{rails_blob_path(attachment.file)}").read
end
end
input_filenames = Dir.entries(folder_path).select {|f| !File.directory? f}
Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
input_filenames.each do |attachment|
zipfile.add(attachment,File.join(folder_path,attachment))
end
end
send_file(File.join("#{Rails.root}/public/", 'archive.zip'), :type => 'application/zip', :filename => "#{Time.now.to_date}.zip")
end
else
redirect_back fallback_location: root_path
end
end
2 - Secondly I tried to follow the rubyzip documentation and the error was a bit different.
No such file or directory # rb_file_s_lstat - /rails/active_storage/blobs/..../2234.py
if ids.present?
folder = []
input_filenames = []
Record.where(id: ids).each do |attachment|
input_filenames.push("#{attachment.file.filename}")
pre_path = "/rails/active_storage/blobs/"
path_find = "#{rails_blob_path(attachment.file)}"
folder.push(pre_path + path_find.split('/')[4])
end
container = Hash[folder.zip(input_filenames)]
zipfile_name = "/Users/fahimabdullah/Documents/archive.zip"
Zip::File.open(zipfile_name, Zip::File::CREATE) do |zipfile|
# input_filenames.each do |filename|
container.map do |path, filename|
zipfile.add(filename, File.join(path, filename))
end
zipfile.get_output_stream("myFile") { |f| f.write "myFile contains just this" }
end
I expect it to download a zip file containing all the files inside. And this is my first question so please excuse me if the question was too long. Thank you.
I just had a similar problem but saw this hasn't been answered yet. Even though it's a little older and you probably already solved this, here is my approach. The trick here was to create a tempfile in between
def whatever
zip_file = Tempfile.new('invoices.zip')
Zip::File.open(zip_file.path, Zip::File::CREATE) do |zipfile|
invoices.each do |invoice|
next unless invoice.attachment.attached?
overlay = Tempfile.new(['overlay', '.pdf'])
overlay.binmode
overlay.write(invoice.attachment.download)
overlay.close
overlay.path
zipfile.add(invoice.filename, File.join(overlay.path))
end
end
invoices_zip = File.read(zip_file.path)
UserMailer.with(user: user).invoice_export(invoices_zip, 'invoices.zip').deliver_now
ensure
zip_file.close
zip_file.unlink
end

Carrierwave pdf local upload duplicates on :fog. Not doing it on :file

I have a very strange bug i been fighting with for a day now.
I'm trying to make an automatic uploader for uploading hundreds of PDF-files and putting them into categories from my local computer to my remote Amazon s3 server using Carrierwave.
When i run it with :file in my pdf_uploader everything is working as it should be, but when i run it with fog trying to save them on the Amazon s3 server everything is duplicated. And not like 1,1,2,2,3,3 but 1,2,3,1,2,3
(i already have two image_uploaders and i tried uploading from a local directory earlier on with images, so the server and connection should be set up right)
Here are my files:
#routes.rb
get "upload_pdfs/now" => "pdf_categories#upload_pdfs", as: "upload_pdfs"
#pdf_categories_controller.rb
def upload_pdfs
path = "/Users/bruger/Desktop/DENHER"
Dir.foreach(path) do |category_path|
next if folder_or_file_excluded? category_path
pdf_category = PdfCategory.create!(title: category_path)
Dir.foreach("#{path}/#{category_path}") do |file_path|
next if folder_or_file_excluded? file_path
pdf_file_without_file_ending = remove_last_obj_in_arr file_path.split(".")
new_pdf = Pdf.new(
title: pdf_file_without_ending,
pdf_category_id: pdf_category.id,
)
#### SOMETHING IS HAPPENING HERE! ###
File.open(path + "/" + category_path + "/" + file_path) do |file_from_path|
new_pdf.file = file_from_path
new_pdf.save!
end
#### SOMETHING IS HAPPENING HERE! ###
end # file_path
end # category_path
redirect_to root_path
end
def is_integer? thing
thing.to_i.to_s == thing
end
def remove_last_obj_in_arr array
new_array = array.first array.size - 1
new_array.join(".")
end
def folder_or_file_excluded? folder_file
list = %w(Fravalgt Fravalg Valgt fra fravalg fravalgt JPEG . .git DS_Store .. .DS_Store image_upload.rb)
list.include? folder_file
end
#pdf_uploader.rb
class PdfUploader < CarrierWave::Uploader::Base
storage :fog
def store_dir
"pdf/#{mounted_as}/#{model.id}"
end
def extension_whitelist
%w(pdf PDF)
end
end
Have you experienced anything like this or do you have a clue?
Please. Any help will be appreciated!

Export large mount of data in a zip

I'm exporting some data from my server to the client.
It's an zip archive but when the amount of data is to big : TimeOut !
#On my controller
def export
filename = 'my_archive.zip'
temp_file = Tempfile.new(filename)
begin
Zip::OutputStream.open(temp_file) { |zos| }
Zip::File.open(temp_file.path, Zip::File::CREATE) do |zip|
#videos.each do |v|
video_file_name = v.title + '.mp4'
zip.add(video_file_name, v.source.file.path(:original))
end
end
zip_data = File.read(temp_file.path)
send_data(zip_data, :type => 'application/zip', :filename => filename)
ensure
temp_file.close
temp_file.unlink
end
end
I'm using PaperClip to attach my video on my app.
Is there any way to create and upload the zip (with a stream?) without a too long wait?
You could try the zipline gem. It claims to be "Hacks on Hacks on Hacks" so heads up! Looks very easy to use though, worth a shot.

How to update/rename a carrierwave uploaded file?

I cant figure out how to update/rename a file uploaded/managed with Carrierwave-mongoid in rails 3.2.6. I want to rename the file in the db as well as on the filesystem.
Something like this maybe...
def rename( id , new_name )
f = UploadedFile.find(id)
if f.update_attributes({ f.file.original_filename: new_name }) # this is WRONG, what is right???
new_path = File.join( File.dirname( f.file.current_path ) , new_name ))
FileUtils.mv( f.file.current_path , new_path )
end
return f
end
Let me add this is after it has been uploaded already.
I was able to get the following working, although I'm sure there is a more elegant way. I'd appreciate any comments on the following
*add this to app/uploaders/file_uploader.rb
def rename(new_name)
sf = model.file.file
new_path = File.join( File.dirname( sf.file ) , "#{new_name}#{File.extname( sf.file )}")
new_sf = CarrierWave::SanitizedFile.new sf.move_to(new_path)
model.file.cache!(new_sf)
model.save!
return model
end
Thanks!
The most efficient way to do this is to just move the existing S3 object (assuming your storage layer is S3):
def rename(new_name)
bucket_name = "yourapp-#{Rails.env}"
resource = Aws::S3::Resource.new
bucket = resource.bucket(bucket_name)
object = bucket.object(path)
new_filename = "#{new_name}#{File.extname(path)}"
new_path = File.join(File.dirname(path), new_filename)
object.move_to(bucket: bucket_name, key: new_path)
model.update_column(mounted_as, new_filename)
model.reload
# Now call `recreate_versions!(*versions.keys)`
# if you want versions updated. Explicitly passing
# versions will prevent the base version getting
# reuploaded.
model
end
This is using the aws-sdk-s3 gem.
I store image files -- and derivative versions -- in an S3-compatible solution. I use Carrierwave (1.2.2) with the "fog-aws" gem (3.0.0) on Rails 5.1. The following public method works for me when added to the "uploader" file (eg, app/uploaders/example_uploader.rb):
class ExampleUploader < CarrierWave::Uploader::Base
<snip>
# Renames original file and versions to match given filename
#
# Options:
# * +:keep_original+ - Do not remove original file and versions (ie, copy only)
def rename(new_filename, options = {})
return if !file || new_filename == file.filename
target = File.join(store_path, new_filename)
file.copy_to(target)
versions.keys.each do |k|
target = File.join(store_path, "#{k}_#{new_filename}")
version = send(k).file
version.copy_to(target)
end
remove! unless options[:keep_original]
model.update_column(mounted_as, new_filename) && model.reload
end
<snip>
end
I used this rake task for reprocessing uploaded images after modifying version settings (filename and image size) in my uploader file:
# Usage: rake carrierwave:reprocess class=Model
namespace :carrierwave do
task :reprocess => :environment do
CLASS = ENV['class'].capitalize
MODEL = Kernel.const_get(CLASS)
records = MODEL.all
records.each do |record|
record.photo.recreate_versions! if record.photo?
end
end
end
Notes:
Replace "photo" with whatever you named your uploader.
Rake tasks go in the lib/tasks folder.
This is using Active Record, not sure if Mongoid needs something
different.
Based on #user892583, I worked on it and came up with a simpler solution:
def rename!(new_name)
new_path = File.join(File.dirname(file.file), new_name)
file.move_to(new_path)
end
I did this with this way:
def filename
if !cached? && file.present?
new_filename = 'foobar'
new_path = File.join(File.dirname(file.path), new_filename)
file.move_to(new_path)
recreate_versions!
new_filename
else
super
end
end
I think this is only right way to rename file.

Resources