Rails 2.3.8 Render a html as pdf - ruby-on-rails

I am using Prawn pdf library however i am doing a complex design, So I need a quick solution as to convert a html to pdf file.
Thanks in advance

I would use wkhtmltopdf shell tool
together with the wicked_pdf ruby gem, its free, and uses qtwebkit to render your html to pdf. Also executes javascript as well, for charts for example. You can find more info about installation: https://github.com/mileszs/wicked_pdf

I have one Rails app that's been using PrinceXML in production for a few years. It is pricey - around $4K for a server license - but does a very good job of rendering HTML+CSS in PDF files. I haven't looked at newer solutions since this one is paid for and working quite well.
For what it's worth, here's some code I adapted from Subimage Interactive to make the conversion simple:
lib/prince.rb
# Prince XML Ruby interface.
# http://www.princexml.com
#
# Library by Subimage Interactive - http://www.subimage.com
#
#
# USAGE
# -----------------------------------------------------------------------------
# prince = Prince.new()
# html_string = render_to_string(:template => 'some_document')
# send_data(
# prince.pdf_from_string(html_string),
# :filename => 'some_document.pdf'
# :type => 'application/pdf'
# )
#
class Prince
attr_accessor :exe_path, :style_sheets, :log_file
# Initialize method
#
def initialize()
# Finds where the application lives, so we can call it.
#exe_path = '/usr/local/bin/prince'
case Rails.env
when 'production', 'staging'
# use default hard-coded path
else
if File.exist?(#exe_path)
# use default hard-coded path
else
#exe_path = `which prince`.chomp
end
end
#style_sheets = ''
#log_file = "#{::Rails.root}/log/prince.log"
end
# Sets stylesheets...
# Can pass in multiple paths for css files.
#
def add_style_sheets(*sheets)
for sheet in sheets do
#style_sheets << " -s #{sheet} "
end
end
# Returns fully formed executable path with any command line switches
# we've set based on our variables.
#
def exe_path
# Add any standard cmd line arguments we need to pass
#exe_path << " --input=html --server --log=#{#log_file} "
#exe_path << #style_sheets
return #exe_path
end
# Makes a pdf from a passed in string.
#
# Returns PDF as a stream, so we can use send_data to shoot
# it down the pipe using Rails.
#
def pdf_from_string(string)
path = self.exe_path()
# Don't spew errors to the standard out...and set up to take IO
# as input and output
path << ' --silent - -o -'
# Show the command used...
#logger.info "\n\nPRINCE XML PDF COMMAND"
#logger.info path
#logger.info ''
# Actually call the prince command, and pass the entire data stream back.
pdf = IO.popen(path, "w+")
pdf.puts(string)
pdf.close_write
output = pdf.gets(nil)
pdf.close_read
return output
end
end
lib/pdf_helper.rb
module PdfHelper
require 'prince'
private
def make_pdf(template_path, pdf_name, stylesheets = [], skip_base_pdf_stylesheet = false)
# application notices should never be included in PDFs, pull them from session here
notices = nil
if !flash.now[:notice].nil?
notices = flash.now[:notice]
flash.now[:notice] = nil
end
if !flash[:notice].nil?
notices = '' if notices.nil?
notices << flash[:notice]
flash[:notice] = nil
end
prince = Prince.new()
# Sets style sheets on PDF renderer.
stylesheet_base = "#{::Rails.root}/public/stylesheets"
prince.add_style_sheets(
"#{stylesheet_base}/application.css",
"#{stylesheet_base}/print.css"
)
prince.add_style_sheets("#{stylesheet_base}/pdf.css") unless skip_base_pdf_stylesheet
if 0 < stylesheets.size
stylesheets.each { |s| prince.add_style_sheets("#{stylesheet_base}/#{s}.css") }
end
# Set RAILS_ASSET_ID to blank string or rails appends some time after
# to prevent file caching, messing up local - disk requests.
ENV['RAILS_ASSET_ID'] = ''
html_string = render_to_string(:template => template_path, :layout => 'application')
# Make all paths relative, on disk paths...
html_string.gsub!("src=\"", "src=\"#{::Rails.root}/public")
html_string.gsub!("src=\"#{::Rails.root}/public#{::Rails.root}", "src=\"#{::Rails.root}")
# re-insert any application notices into the session
if !notices.nil?
flash[:notice] = notices
end
return prince.pdf_from_string(html_string)
end
def make_and_send_pdf(template_path, pdf_name, stylesheets = [], skip_base_pdf_stylesheet = false)
send_data(
make_pdf(template_path, pdf_name, stylesheets, skip_base_pdf_stylesheet),
:filename => pdf_name,
:type => 'application/pdf'
)
end
end
sample controller action
include PdfHelper
def pdf
index
make_and_send_pdf '/ads/index', "#{filename}.pdf"
end

You can directly convert your existing HTML to PDF using acts_as_flying_saucer library.For header and footer you can refer
https://github.com/amardaxini/acts_as_flying_saucer/wiki/PDF-Header-Footer

Related

What could cause performance issue(s) when using roadie-rails for our case?

Originally posted as
https://github.com/Mange/roadie-rails/issues/75
We are seeing performance issue for our daily email jobs
By using NewRelic custom instrumentation,
we found out that most time is spent in calling Roadies
Screenshot of our NewRelic data for an example worker:
The integration code:
# frozen_string_literal: true
require "rails"
require "action_controller"
require "contracts"
require "memoist"
require "roadie"
require "roadie-rails"
require "new_relic/agent/method_tracer"
module Shared::MailerMixins
module WithRoadieIntegration
# I don't want to include the constants into the class as well
module Concern
def self.included(base)
base.extend ClassMethods
end
include ::NewRelic::Agent::MethodTracer
def mail(*args, &block)
super.tap do |m|
options = roadie_options
next unless options
trace_execution_scoped(
[
[
"WithRoadieIntegration",
"Roadie::Rails::MailInliner.new(m, options).execute",
].join("/"),
],
) do
Roadie::Rails::MailInliner.new(m, options).execute
end
end
end
private
def roadie_options
::Rails.application.config.roadie.tap do |options|
options.asset_providers = [UserAssetsProvider.new]
options.external_asset_providers = [UserAssetsProvider.new]
options.keep_uninlinable_css = false
options.url_options = url_options.slice(*[
:host,
:port,
:path,
:protocol,
:scheme,
])
end
end
add_method_tracer(
:roadie_options,
"WithRoadieIntegration/roadie_options",
)
end
class UserAssetsProvider
extend(
::Memoist,
)
include(
::Contracts::Core,
::Contracts::Builtin,
)
include ::NewRelic::Agent::MethodTracer
ABSOLUTE_ASSET_PATH_REGEXP = /\A#{Regexp.escape("//")}.+#{Regexp.escape("/assets/")}/i
Contract String => Maybe[Roadie::Stylesheet]
def find_stylesheet(name)
return nil unless file_exists?(name)
Roadie::Stylesheet.new("whatever", stylesheet_content(name))
end
add_method_tracer(
:find_stylesheet,
"UserAssetsProvider/find_stylesheet",
)
Contract String => Roadie::Stylesheet
def find_stylesheet!(name)
stylesheet = find_stylesheet(name)
if stylesheet.nil?
raise Roadie::CssNotFound.new(
name,
"does not exists",
self,
)
end
stylesheet
end
add_method_tracer(
:find_stylesheet!,
"UserAssetsProvider/find_stylesheet!",
)
private
def file_exists?(name)
if assets_precompiled?
File.exists?(local_file_path(name))
else
sprockets_asset(name)
end
end
memoize :file_exists?
# If on-the-fly asset compilation is disabled, we must be precompiling assets.
def assets_precompiled?
!Rails.configuration.assets.compile
rescue
false
end
def local_file_path(name)
asset_path = asset_path(name)
if asset_path.match(ABSOLUTE_ASSET_PATH_REGEXP)
asset_path.gsub!(ABSOLUTE_ASSET_PATH_REGEXP, "assets/")
end
File.join(Rails.public_path, asset_path)
end
memoize :local_file_path
add_method_tracer(
:local_file_path,
"UserAssetsProvider/local_file_path",
)
def sprockets_asset(name)
asset_path = asset_path(name)
if asset_path.match(ABSOLUTE_ASSET_PATH_REGEXP)
asset_path.gsub!(ABSOLUTE_ASSET_PATH_REGEXP, "")
end
# Strange thing is since rails 4.2
# name is passed in like
# `/assets/mailer-a9c96bd713d0b091297b82053ccd9155b933c00a53595812d755825d1747f42d.css`
# Before any processing
# And since `sprockets_asset` is used for preview
# We just "fix" the name by removing the
#
# Regexp taken from gem `asset_sync`
# https://github.com/AssetSync/asset_sync/blob/v1.2.1/lib/asset_sync/storage.rb#L142
#
# Modified to match what we need here (we need `.css` suffix)
if asset_path =~ /-[0-9a-fA-F]{32,}\.css$/
asset_path.gsub!(/-[0-9a-fA-F]{32,}\.css$/, ".css")
end
Rails.application.assets.find_asset(asset_path)
end
add_method_tracer(
:sprockets_asset,
"UserAssetsProvider/sprockets_asset",
)
def asset_path(name)
name.gsub(%r{^[/]?assets/}, "")
end
Contract String => String
def stylesheet_content(name)
if assets_precompiled?
File.read(local_file_path(name))
else
# This will compile and return the asset
sprockets_asset(name).to_s
end.strip
end
memoize :stylesheet_content
add_method_tracer(
:stylesheet_content,
"UserAssetsProvider/stylesheet_content",
)
end
end
end
I would like to report my own findings
With NewRelic data, we think most of the time is spent on
Roadies::Inliner/selector_elements => Roadie::Inliner/elements_matching_selector
And it seems a stylesheet with more style rules will make the style inlining takes longer
Benchmark code will be something like:
# frozen_string_literal: true
require "benchmark/ips"
class TestMailer < ::ActionMailer::Base
def show(benchmark_file_path:)
return mail(
from: "somewhere#test.com",
to: ["somewhere#test.com"],
subject: "some subject",
# This is trying to workaround a strange bug in `mail` gem
# https://github.com/mikel/mail/issues/912#issuecomment-156186383
content_type: "text/html",
) do |format|
format.html do
render(
file: benchmark_file_path,
layout: false,
)
end
end
end
end
Benchmark.ips do |x|
x.warmup = 5
x.time = 60
options = Roadie::Rails::Options.new(
# Use your own provider or use built-in providers
# I use a custom provider which can be used inside a rails app,
# See https://github.com/Mange/roadie for built-in providers
#
# options.asset_providers = [UserAssetsProvider.new]
# options.external_asset_providers = [UserAssetsProvider.new]
options.keep_uninlinable_css = false
)
# Need to prepare html_file yourself with
# different stylesheet tag pointing to two different stylesheet files
x.report("fat") do
message = ::TestMailer.
show(
benchmark_file_path: "benchmark-fat-stylesheet.html",
).message.tap do |m|
Roadie::Rails::MailInliner.new(m, options).execute
end
if message.body.to_s =~ /stylesheet/
raise "stylesheet not processed"
end
end
x.report("slim") do
message = ::TestMailer.
show(
benchmark_file_path: "benchmark-slim-stylesheet.html",
).message
if message.body.to_s =~ /stylesheet/
raise "stylesheet not processed"
end
end
# Compare the iterations per second of the various reports!
x.compare!
end

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.

Downloading and zipping files that were uploaded to S3 with CarrierWave

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

Migrating paperclip S3 images to new url/path format

Is there a recommended technique for migrating a large set of paperclip S3 images to a new :url and :path format?
The reason for this is because after upgrading to rails 3.1, new versions of thumbs are not being shown after cropping (previously cached version is shown). This is because the filename no longer changes (since asset_timestamp was removed in rails 3.1). I'm using :fingerprint in the url/path format, but this is generated from the original, which doesn't change when cropping.
I was intending to insert :updated_at in the url/path format, and update attachment.updated_at during cropping, but after implementing that change all existing images would need to be moved to their new location. That's around half a million images to rename over S3.
At this point I'm considering copying them to their new location first, then deploying the code change, then moving any images which were missed (ie uploaded after the copy), but I'm hoping there's an easier way... any suggestions?
I had to change my paperclip path in order to support image cropping, I ended up creating a rake task to help out.
namespace :paperclip_migration do
desc 'Migrate data'
task :migrate_s3 => :environment do
# Make sure that all of the models have been loaded so any attachments are registered
puts 'Loading models...'
Dir[Rails.root.join('app', 'models', '**/*')].each { |file| File.basename(file, '.rb').camelize.constantize }
# Iterate through all of the registered attachments
puts 'Migrating attachments...'
attachment_registry.each_definition do |klass, name, options|
puts "Migrating #{klass}: #{name}"
klass.find_each(batch_size: 100) do |instance|
attachment = instance.send(name)
unless attachment.blank?
attachment.styles.each do |style_name, style|
old_path = interpolator.interpolate(old_path_option, attachment, style_name)
new_path = interpolator.interpolate(new_path_option, attachment, style_name)
# puts "#{style_name}:\n\told: #{old_path}\n\tnew: #{new_path}"
s3_copy(s3_bucket, old_path, new_path)
end
end
end
end
puts 'Completed migration.'
end
#############################################################################
private
# Paperclip Configuration
def attachment_registry
Paperclip::AttachmentRegistry
end
def s3_bucket
ENV['S3_BUCKET']
end
def old_path_option
':class/:id_partition/:attachment/:hash.:extension'
end
def new_path_option
':class/:attachment/:id_partition/:style/:filename'
end
def interpolator
Paperclip::Interpolations
end
# S3
def s3
AWS::S3.new(access_key_id: ENV['S3_KEY'], secret_access_key: ENV['S3_SECRET'])
end
def s3_copy(bucket, source, destination)
source_object = s3.buckets[bucket].objects[source]
destination_object = source_object.copy_to(destination, {metadata: source_object.metadata.to_h})
destination_object.acl = source_object.acl
puts "Copied #{source}"
rescue Exception => e
puts "*Unable to copy #{source} - #{e.message}"
end
end
Didn't find a feasible method for migrating to a new url format. I ended up overriding Paperclip::Attachment#generate_fingerprint so it appends :updated_at.

Storing image using open URI and paperclip having size less than 10kb

I want to import some icons from my old site. The size of those icons is less than 10kb. So when I am trying to import the icons its returning stringio.txt file.
require "open-uri"
class Category < ActiveRecord::Base
has_attached_file :icon, :path => ":rails_root/public/:attachment/:id/:style/:basename.:extension"
def icon_from_url(url)
self.icon = open(url)
end
end
In rake task.
category = Category.new
category.icon_from_url "https://xyz.com/images/dog.png"
category.save
Try:
def icon_from_url(url)
extname = File.extname(url)
basename = File.basename(url, extname)
file = Tempfile.new([basename, extname])
file.binmode
open(URI.parse(url)) do |data|
file.write data.read
end
file.rewind
self.icon = file
end
To override the default filename of a "fake file upload" in Paperclip (stringio.txt on small files or an almost random temporary name on larger files) you have 2 main possibilities:
Define an original_filename on the IO:
def icon_from_url(url)
io = open(url)
io.original_filename = "foo.png"
self.icon = io
end
You can also get the filename from the URI:
io.original_filename = File.basename(URI.parse(url).path)
Or replace :basename in your :path:
has_attached_file :icon, :path => ":rails_root/public/:attachment/:id/:style/foo.png", :url => "/:attachment/:id/:style/foo.png"
Remember to alway change the :url when you change the :path, otherwise the icon.url method will be wrong.
You can also define you own custom interpolations (e.g. :rails_root/public/:whatever).
You are almost there I think, try opening parsed uri, not the string.
require "open-uri"
class Category < ActiveRecord::Base
has_attached_file :icon, :path =>:rails_root/public/:attachment/:id/:style/:basename.:extension"
def icon_from_url(url)
self.icon = open(URI.parse(url))
end
end
Of course this doesn't handle errors
You can also disable OpenURI from ever creating a StringIO object, and force it to create a temp file instead. See this SO answer:
Why does Ruby open-uri's open return a StringIO in my unit test, but a FileIO in my controller?
In the past, I found the most reliable way to retrieve remote files was by using the command line tool "wget". The following code is mostly copied straight from an existing production (Rails 2.x) app with a few tweaks to fit with your code examples:
class CategoryIconImporter
def self.download_to_tempfile (url)
system(wget_download_command_for(url))
##tempfile.path
end
def self.clear_tempfile
##tempfile.delete if ##tempfile && ##tempfile.path && File.exist?(##tempfile.path)
##tempfile = nil
end
def self.set_wget
# used for retrieval in NrlImage (and in future from other sies?)
if !##wget
stdin, stdout, stderr = Open3.popen3('which wget')
##wget = stdout.gets
##wget ||= '/usr/local/bin/wget'
##wget.strip!
end
end
def self.wget_download_command_for (url)
set_wget
##tempfile = Tempfile.new url.sub(/\?.+$/, '').split(/[\/\\]/).last
command = [ ##wget ]
command << '-q'
if url =~ /^https/
command << '--secure-protocol=auto'
command << '--no-check-certificate'
end
command << '-O'
command << ##tempfile.path
command << url
command.join(' ')
end
def self.import_from_url (category_params, url)
clear_tempfile
filename = url.sub(/\?.+$/, '').split(/[\/\\]/).last
found = MIME::Types.type_for(filename)
content_type = !found.empty? ? found.first.content_type : nil
download_to_tempfile url
nicer_path = RAILS_ROOT + '/tmp/' + filename
File.copy ##tempfile.path, nicer_path
Category.create(category_params.merge({:icon => ActionController::TestUploadedFile.new(nicer_path, content_type, true)}))
end
end
The rake task logic might look like:
[
['Cat', 'cat'],
['Dog', 'dog'],
].each do |name, icon|
CategoryIconImporter.import_from_url {:name => name}, "https://xyz.com/images/#{icon}.png"
end
This uses the mime-types gem for content type discovery:
gem 'mime-types', :require => 'mime/types'

Resources