Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

first pass at temp file usage

git-svn-id: http://svn.techno-weenie.net/projects/plugins/attachment_fu@2566 567b1171-46fb-0310-a4c9-b4bef9110e78
  • Loading branch information...
commit 88f42fbb11cdfb36b08b66b7033ef3c8ff9854ea 1 parent 650dda6
technoweenie authored
View
106 lib/technoweenie/attachment_fu.rb
@@ -1,10 +1,19 @@
require File.join(File.dirname(__FILE__), 'attachment_fu', 'backends')
require File.join(File.dirname(__FILE__), 'attachment_fu', 'processors')
+require 'tempfile'
+
+class Tempfile
+ # overwrite so tempfiles have no extension
+ def make_tmpname(basename, n)
+ sprintf("%s%d-%d", basename, $$, n)
+ end
+end
module Technoweenie # :nodoc:
module AttachmentFu # :nodoc:
+ @@temp_path = File.join(RAILS_ROOT, 'tmp', 'attachment_fu')
@@content_types = ['image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png']
- mattr_reader :content_types
+ mattr_reader :content_types, :temp_path
class ThumbnailError < StandardError; end
class AttachmentError < StandardError; end
@@ -46,7 +55,8 @@ def has_attachment(options = {})
# only need to define these once on a class
unless included_modules.include? InstanceMethods
class_inheritable_accessor :attachment_options
-
+ attr_accessor :temp_path
+
options[:processor] ||= :rmagick
options[:storage] ||= options[:file_system_path] ? :file_system : :db_file
options[:file_system_path] ||= File.join("public", table_name)
@@ -57,6 +67,8 @@ def has_attachment(options = {})
m.belongs_to :parent, :class_name => base_class.to_s
end
+ before_validation :set_size_from_temp_path
+ after_save :after_process_attachment
after_destroy :destroy_file
extend ClassMethods
include InstanceMethods
@@ -127,9 +139,29 @@ def thumbnail_class
attachment_options[:thumbnail_class] = attachment_options[:thumbnail_class].constantize unless attachment_options[:thumbnail_class].is_a?(Class)
attachment_options[:thumbnail_class]
end
+
+ def copy_to_temp_file(file, temp_base_name)
+ path = nil
+ Tempfile.open temp_base_name, Technoweenie::AttachmentFu.temp_path do |f|
+ path = f.path
+ end
+ FileUtils.cp file, path
+ path
+ end
+
+ def write_to_temp_file(data, temp_base_name)
+ path = nil
+ Tempfile.open temp_base_name, Technoweenie::AttachmentFu.temp_path do |f|
+ path = f.path
+ f.write data
+ end
+ path
+ end
end
module InstanceMethods
+ attr_accessor :thumbnail_resize_options
+
# Checks whether the attachment's content type is an image content type
def image?
self.class.image?(content_type)
@@ -170,49 +202,33 @@ def uploaded_data() nil; end
# TODO: Allow it to work with Merb tempfiles too.
def uploaded_data=(file_data)
return nil if file_data.nil? || file_data.size == 0
- self.content_type = file_data.content_type
- self.filename = file_data.original_filename if respond_to?(:filename)
- self.attachment_data = file_data.read
+ self.content_type = file_data.content_type
+ self.filename = file_data.original_filename if respond_to?(:filename)
+ self.temp_path = file_data.path
end
# returns true if the attachment data will be written to the storage system on the next save
def save_attachment?
- @save_attachment == true
+ File.file?(@temp_path.to_s)
end
- # Sets the actual binary data. This is typically called by uploaded_data=, but you can call this
- # manually if you're creating from the console. This is also where the resizing occurs.
- def attachment_data=(data)
- @attachment_data = nil
- @save_attachment = false
- self.size = 0
-
- if data
- self.size = data.length
- @save_attachment = true
- @attachment_data = data
- end
+ def temp_data
+ save_attachment? ? File.read(@temp_path) : nil
end
- # sets a temporary location to the asset. Use this if the file is already on the local file system
- # and if you do not need to load it into memory.
- def attachment_file=(file)
- @attachment_file = nil
- @save_attachment = false
- self.size = 0
-
- if file && File.file?(file)
- file_stat = File.stat(file)
- self.size = file_stat.size
- @save_attachment = true
- @attachment_file = file
- end
+ def temp_data=(data)
+ self.temp_path = write_to_temp_file data unless data.nil?
end
-
- # Retrieve the temporary attachment file data if it exists, or return nil
- def attachment_file_data
- (@attachment_file && File.file?(@attachment_file)) ? File.read(@attachment_file) : nil
+
+ def copy_to_temp_file(file)
+ self.class.copy_to_temp_file file, random_tempfile_filename
+ end
+
+ def write_to_temp_file(data)
+ self.class.write_to_temp_file data, random_tempfile_filename
end
+
+ def create_temp_file!() end
# Sets the content type.
def content_type=(new_type)
@@ -230,6 +246,10 @@ def image_size
end
protected
+ def random_tempfile_filename
+ "#{filename || 'attachment'}#{rand Time.now.to_i}"
+ end
+
@@filename_basename_regex = /^.*(\\|\/)/
@@filename_character_regex = /[^\w\.\-]/
def sanitize_filename(filename)
@@ -243,6 +263,11 @@ def sanitize_filename(filename)
end
end
+ def set_size_from_temp_path
+ return unless save_attachment?
+ self.size = File.size(temp_path)
+ end
+
# validates the size and content_type attributes according to the current model's options
def attachment_attributes_valid?
[:size, :content_type].each do |attr_name|
@@ -252,7 +277,16 @@ def attachment_attributes_valid?
end
# Stub for a #process_attachment method in a processor
- def process_attachment() end
+ def process_attachment
+ @saved_attachment = save_attachment?
+ end
+
+ def after_process_attachment
+ save_to_storage
+ @temp_path = nil
+ @saved_attachment = nil
+ callback :after_attachment_saved
+ end
# Yanked from ActiveRecord::Callbacks, modified so I can pass args to the callbacks besides self.
# Only accept blocks, however
View
47 lib/technoweenie/attachment_fu/backends.rb
@@ -4,18 +4,6 @@ module AttachmentFu # :nodoc:
module FileSystemBackend
def self.included(base) #:nodoc:
base.before_update :rename_file
- base.after_save :save_to_storage # so the id can be part of the url
- end
-
- # Gets the attachment data
- def attachment_data
- return @attachment_data if @attachment_data
-
- filename = @attachment_file || full_filename
- File.open(filename, 'rb') do |file|
- @attachment_data = file.read
- end if File.file?(filename)
- @attachment_data
end
# Gets the full path to the filename in this format:
@@ -52,6 +40,10 @@ def filename=(value)
write_attribute :filename, sanitize_filename(value)
end
+ def create_temp_file!
+ copy_to_temp_file full_filename
+ end
+
# Destroys the file. Called in the after_destroy callback
def destroy_file
FileUtils.rm full_filename rescue nil
@@ -59,7 +51,7 @@ def destroy_file
def rename_file
return unless @old_filename && @old_filename != full_filename
- if @save_attachment && File.exists?(@old_filename)
+ if save_attachment? && File.exists?(@old_filename)
FileUtils.rm @old_filename
elsif File.exists?(@old_filename)
FileUtils.mv @old_filename, full_filename
@@ -70,20 +62,18 @@ def rename_file
# Saves the file to the file system
def save_to_storage
- if @save_attachment
+ if save_attachment?
# TODO: This overwrites the file if it exists, maybe have an allow_overwrite option?
FileUtils.mkdir_p(File.dirname(full_filename))
-
- # TODO Convert to streaming storage to prevent excessive memory usage
- # FileUtils.copy_stream is very efficient in regards to copies
- # OR - get the tmp filename for large files and do FileUtils.cp ? *agile*
- File.open(full_filename, "wb") do |file|
- file.write(attachment_data)
- end
+ FileUtils.mv @temp_path, full_filename
end
@old_filename = nil
true
end
+
+ def current_data
+ File.file?(full_filename) ? File.read(full_filename) : nil
+ end
end
# Methods for DB backed attachments
@@ -94,9 +84,8 @@ def self.included(base) #:nodoc:
base.before_save :save_to_storage # so the db_file_id can be set
end
- # Gets the attachment data
- def attachment_data
- @attachment_data ||= (attachment_file_data || db_file.data)
+ def create_temp_file!
+ write_to_temp_file db_file.data
end
# Destroys the file. Called in the after_destroy callback
@@ -106,13 +95,17 @@ def destroy_file
# Saves the data to the DbFile model
def save_to_storage
- if @save_attachment
- (db_file || build_db_file).data = attachment_data
+ if save_attachment?
+ (db_file || build_db_file).data = temp_data
db_file.save!
- self.db_file_id = db_file.id # needed for my own sanity, k thx
+ self.class.update_all ['db_file_id = ?', self.db_file_id = db_file.id], ['id = ?', id]
end
true
end
+
+ def current_data
+ db_file.data
+ end
end
end
end
View
72 lib/technoweenie/attachment_fu/processors.rb
@@ -7,15 +7,16 @@ def self.included(base)
rescue LoadError
# boo hoo no rmagick
end
- base.after_save :create_attachment_thumbnails # allows thumbnails with parent_id to be created
base.send :extend, ClassMethods
+ base.alias_method_chain :process_attachment, :processing
+ base.alias_method_chain :after_process_attachment, :processing
end
module ClassMethods
# Yields a block containing an RMagick Image for the given binary data.
- def with_image(data, &block)
+ def with_image(file, &block)
begin
- binary_data = data.is_a?(Magick::Image) ? data : Magick::Image::from_blob(data).first unless !Object.const_defined?(:Magick)
+ binary_data = file.is_a?(Magick::Image) ? file : Magick::Image.read(file).first unless !Object.const_defined?(:Magick)
rescue
# Log the failure to load the image. This should match ::Magick::ImageMagickError
# but that would cause acts_as_attachment to require rmagick.
@@ -34,44 +35,47 @@ def with_image(data, &block)
# self.data = img.thumbnail(100, 100).to_blob
# end
#
- def with_image(data = self.attachment_data, &block)
- self.class.with_image(data, &block)
+ def with_image(&block)
+ self.class.with_image(temp_path, &block)
end
# Creates or updates the thumbnail for the current attachment.
- def create_or_update_thumbnail(file_name_suffix, *size)
+ def create_or_update_thumbnail(temp_file, file_name_suffix, *size)
thumbnailable? || raise(ThumbnailError.new("Can't create a thumbnail if the content type is not an image or there is no parent_id column"))
returning find_or_initialize_thumbnail(file_name_suffix) do |thumb|
- resized_image = resize_image_to(size)
- return if resized_image.nil?
thumb.attributes = {
- :content_type => content_type,
- :filename => thumbnail_name_for(file_name_suffix),
- :attachment_data => resized_image.to_blob
+ :content_type => content_type,
+ :filename => thumbnail_name_for(file_name_suffix),
+ :temp_path => temp_file,
+ :thumbnail_resize_options => size
}
callback_with_args :before_thumbnail_saved, thumb
thumb.save!
end
end
- # Resizes a thumbnail.
- def resize_image_to(size)
- thumb = nil
- with_image do |img|
- thumb = thumbnail_for_image(img, size)
+ protected
+ def process_attachment_with_processing
+ return unless process_attachment_without_processing
+ with_image do |img|
+ if !respond_to?(:parent_id) || parent_id.nil? # parent image
+ thumbnail_for_image(img, attachment_options[:resize_to]) if attachment_options[:resize_to]
+ else # thumbnail
+ thumbnail_for_image(img, thumbnail_resize_options) if thumbnail_resize_options
+ end
+ self.width = img.columns if respond_to?(:width)
+ self.height = img.rows if respond_to?(:height)
+ callback_with_args :after_resize, img
+ end if image?
end
- thumb
- end
- protected
- def create_attachment_thumbnails
- if thumbnailable? && @save_attachment && !attachment_options[:thumbnails].blank? && parent_id.nil?
- attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(suffix, size) }
- end
- if @save_attachment
- @save_attachment = nil
- callback :after_attachment_saved
+ def after_process_attachment_with_processing
+ return unless @saved_attachment
+ if thumbnailable? && !attachment_options[:thumbnails].blank? && parent_id.nil?
+ temp_file = temp_path || create_temp_file!
+ attachment_options[:thumbnails].each { |suffix, size| create_or_update_thumbnail(temp_file, suffix, *size) }
end
+ after_process_attachment_without_processing
end
# Performs the actual resizing operation for a thumbnail
@@ -79,10 +83,11 @@ def thumbnail_for_image(img, size)
size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum)
if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
size = [size, size] if size.is_a?(Fixnum)
- img.thumbnail(size.first, size[1])
+ img.thumbnail!(*size)
else
- img.change_geometry(size.to_s) { |cols, rows, image| image.resize(cols, rows) }
+ img.change_geometry(size.to_s) { |cols, rows, image| image.resize!(cols, rows) }
end
+ self.temp_path = write_to_temp_file(img.to_blob)
end
def find_or_initialize_thumbnail(file_name_suffix)
@@ -90,17 +95,6 @@ def find_or_initialize_thumbnail(file_name_suffix)
thumbnail_class.find_or_initialize_by_thumbnail_and_parent_id(file_name_suffix.to_s, id) :
thumbnail_class.find_or_initialize_by_thumbnail(file_name_suffix.to_s)
end
-
- def process_attachment
- with_image do |img|
- resized_img = (attachment_options[:resize_to] && (!respond_to?(:parent_id) || parent_id.nil?)) ?
- thumbnail_for_image(img, attachment_options[:resize_to]) : img
- self.width = resized_img.columns if respond_to?(:width)
- self.height = resized_img.rows if respond_to?(:height)
- self.attachment_data = resized_img.to_blob
- callback_with_args :after_resize, resized_img
- end if image?
- end
end
end
end
View
12 test/backends/file_system_test.rb
@@ -50,11 +50,13 @@ def test_should_delete_old_file_when_updating(klass = FileAttachment)
attachment = upload_file :filename => '/files/rails.png'
old_filename = attachment.full_filename
assert_not_created do
- attachment.filename = 'rails2.png'
- attachment.attachment_data = IO.read(File.join(Test::Unit::TestCase.fixture_path, 'files/rails.png'))
- attachment.save
- assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
- assert !File.exists?(old_filename), "#{old_filename} still exists"
+ use_temp_file 'files/rails.png' do |file|
+ attachment.filename = 'rails2.png'
+ attachment.temp_path = File.join(Test::Unit::TestCase.fixture_path, file)
+ attachment.save!
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
+ assert !File.exists?(old_filename), "#{old_filename} still exists"
+ end
end
end
View
21 test/base_attachment_tests.rb
@@ -16,12 +16,13 @@ def test_reassign_attribute_data
assert_created 1 do
attachment = upload_file :filename => '/files/rails.png'
assert_valid attachment
- assert attachment.attachment_data.size > 0, "no data was set"
+ assert attachment.size > 0, "no data was set"
- attachment.attachment_data = 'wtf'
- attachment.save
+ attachment.temp_data = 'wtf'
+ assert attachment.save_attachment?
+ attachment.save!
- assert_equal 'wtf', attachment_model.find(attachment.id).attachment_data
+ assert_equal 'wtf', attachment_model.find(attachment.id).current_data
end
end
@@ -29,9 +30,9 @@ def test_no_reassign_attribute_data_on_nil
assert_created 1 do
attachment = upload_file :filename => '/files/rails.png'
assert_valid attachment
- assert attachment.attachment_data.size > 0, "no data was set"
+ assert attachment.size > 0, "no data was set"
- attachment.attachment_data = nil
+ attachment.temp_data = nil
assert !attachment.save_attachment?
end
end
@@ -39,9 +40,11 @@ def test_no_reassign_attribute_data_on_nil
def test_should_overwrite_old_contents_when_updating
attachment = upload_file :filename => '/files/rails.png'
assert_not_created do # no new db_file records
- attachment.filename = 'rails2.png'
- attachment.attachment_data = IO.read(File.join(Test::Unit::TestCase.fixture_path, 'files', 'rails.png'))
- attachment.save
+ use_temp_file 'files/rails.png' do |file|
+ attachment.filename = 'rails2.png'
+ attachment.temp_path = File.join(Test::Unit::TestCase.fixture_path, file)
+ attachment.save!
+ end
end
end
end
View
14 test/basic_test.rb
@@ -54,18 +54,4 @@ def test_should_convert_thumbnail_name
@attachment.filename = 'foo.bar.baz'
assert_equal 'foo.bar_blah.baz', @attachment.thumbnail_name_for(:blah)
end
-
- def test_should_set_attachment_data
- @attachment = FileAttachment.new :filename => 'foo.bar'
- assert_nil @attachment.attachment_data
- assert !@attachment.save_attachment?
-
- @attachment.attachment_data = 'blah'
- assert @attachment.save_attachment?
- assert_equal 4, @attachment.size
-
- @attachment.attachment_data = nil
- assert !@attachment.save_attachment?
- assert_equal 0, @attachment.size
- end
end
View
36 test/processors/rmagick_test.rb
@@ -88,15 +88,17 @@ def test_should_automatically_create_thumbnails(klass = ImageWithThumbsAttachmen
def test_should_use_thumbnail_subclass(klass = ImageWithThumbsClassFileAttachment)
attachment_model klass
+ attachment = nil
assert_difference ImageThumbnail, :count do
attachment = upload_file :filename => '/files/rails.png'
- assert_kind_of ImageThumbnail, attachment.thumbnails.first
- assert_equal attachment.id, attachment.thumbnails.first.parent.id
- assert_kind_of FileAttachment, attachment.thumbnails.first.parent
- assert_equal 'rails_thumb.png', attachment.thumbnails.first.filename
- assert_equal attachment.thumbnails.first.full_filename, attachment.full_filename(attachment.thumbnails.first.thumbnail),
- "#full_filename does not use thumbnail class' path."
+ assert_valid attachment
end
+ assert_kind_of ImageThumbnail, attachment.thumbnails.first
+ assert_equal attachment.id, attachment.thumbnails.first.parent.id
+ assert_kind_of FileAttachment, attachment.thumbnails.first.parent
+ assert_equal 'rails_thumb.png', attachment.thumbnails.first.filename
+ assert_equal attachment.thumbnails.first.full_filename, attachment.full_filename(attachment.thumbnails.first.thumbnail),
+ "#full_filename does not use thumbnail class' path."
end
test_against_subclass :test_should_use_thumbnail_subclass, ImageWithThumbsClassFileAttachment
@@ -111,12 +113,14 @@ def test_should_remove_old_thumbnail_files_when_updating(klass = ImageWithThumbs
old_filenames = [attachment.full_filename] + attachment.thumbnails.collect(&:full_filename)
assert_not_created do
- attachment.filename = 'rails2.png'
- attachment.attachment_data = IO.read(File.join(File.dirname(__FILE__), '../fixtures/files/rails.png'))
- attachment.save
- new_filenames = [attachment.reload.full_filename] + attachment.thumbnails.collect { |t| t.reload.full_filename }
- new_filenames.each { |f| assert File.exists?(f), "#{f} does not exist" }
- old_filenames.each { |f| assert !File.exists?(f), "#{f} still exists" }
+ use_temp_file "files/rails.png" do |file|
+ attachment.filename = 'rails2.png'
+ attachment.temp_path = File.join(fixture_path, file)
+ attachment.save
+ new_filenames = [attachment.reload.full_filename] + attachment.thumbnails.collect { |t| t.reload.full_filename }
+ new_filenames.each { |f| assert File.exists?(f), "#{f} does not exist" }
+ old_filenames.each { |f| assert !File.exists?(f), "#{f} still exists" }
+ end
end
end
@@ -140,9 +144,11 @@ def test_should_overwrite_old_thumbnail_records_when_updating(klass = ImageWithT
attachment = upload_file :filename => '/files/rails.png'
end
assert_not_created do # no new db_file records
- attachment.filename = 'rails2.png'
- attachment.attachment_data = IO.read(File.join(File.dirname(__FILE__), '../fixtures/files/rails.png'))
- attachment.save
+ use_temp_file "files/rails.png" do |file|
+ attachment.filename = 'rails2.png'
+ attachment.temp_path = File.join(fixture_path, file)
+ attachment.save!
+ end
end
end
View
19 test/test_helper.rb
@@ -54,11 +54,22 @@ def self.test_against_subclass(test_method, klass)
protected
def upload_file(options = {})
- att = attachment_model.create :uploaded_data => fixture_file_upload(options[:filename], options[:content_type] || 'image/png')
- att.reload unless att.new_record?
- att
+ use_temp_file options[:filename] do |file|
+ att = attachment_model.create :uploaded_data => fixture_file_upload(file, options[:content_type] || 'image/png')
+ att.reload unless att.new_record?
+ return att
+ end
end
-
+
+ def use_temp_file(fixture_filename)
+ temp_path = File.join('/tmp', File.basename(fixture_filename))
+ FileUtils.mkdir_p File.join(fixture_path, 'tmp')
+ FileUtils.cp File.join(fixture_path, fixture_filename), File.join(fixture_path, temp_path)
+ yield temp_path
+ ensure
+ FileUtils.rm_rf File.join(fixture_path, 'tmp')
+ end
+
def assert_created(num = 1)
assert_difference attachment_model.base_class, :count, num do
if attachment_model.included_modules.include? DbFile
Please sign in to comment.
Something went wrong with that request. Please try again.