attachmerb_fu 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. data/LICENSE +22 -0
  2. data/README +166 -0
  3. data/Rakefile +35 -0
  4. data/TODO +5 -0
  5. data/lib/amazon_s3.yml.tpl +14 -0
  6. data/lib/attachment_fu.rb +431 -0
  7. data/lib/attachmerb_fu.rb +446 -0
  8. data/lib/attachmerb_fu/backends/db_file_backend.rb +37 -0
  9. data/lib/attachmerb_fu/backends/file_system_backend.rb +95 -0
  10. data/lib/attachmerb_fu/backends/s3_backend.rb +307 -0
  11. data/lib/attachmerb_fu/merbtasks.rb +6 -0
  12. data/lib/attachmerb_fu/processors/image_science_processor.rb +60 -0
  13. data/lib/attachmerb_fu/processors/mini_magick_processor.rb +54 -0
  14. data/lib/attachmerb_fu/processors/rmagick_processor.rb +51 -0
  15. data/lib/geometry.rb +93 -0
  16. data/lib/tempfile_ext.rb +9 -0
  17. data/lib/test/amazon_s3.yml +6 -0
  18. data/lib/test/backends/db_file_test.rb +16 -0
  19. data/lib/test/backends/file_system_test.rb +80 -0
  20. data/lib/test/backends/remote/s3_test.rb +103 -0
  21. data/lib/test/base_attachment_tests.rb +57 -0
  22. data/lib/test/basic_test.rb +64 -0
  23. data/lib/test/database.yml +18 -0
  24. data/lib/test/extra_attachment_test.rb +57 -0
  25. data/lib/test/fixtures/attachment.rb +127 -0
  26. data/lib/test/fixtures/files/fake/rails.png +0 -0
  27. data/lib/test/fixtures/files/foo.txt +1 -0
  28. data/lib/test/fixtures/files/rails.png +0 -0
  29. data/lib/test/geometry_test.rb +101 -0
  30. data/lib/test/processors/image_science_test.rb +31 -0
  31. data/lib/test/processors/mini_magick_test.rb +31 -0
  32. data/lib/test/processors/rmagick_test.rb +241 -0
  33. data/lib/test/schema.rb +86 -0
  34. data/lib/test/test_helper.rb +142 -0
  35. data/lib/test/validation_test.rb +55 -0
  36. metadata +107 -0
@@ -0,0 +1,54 @@
1
+ require 'mini_magick'
2
+ module AttachmerbFu # :nodoc:
3
+ module Processors
4
+ module MiniMagickProcessor
5
+ def self.included(base)
6
+ base.send :extend, ClassMethods
7
+ base.alias_method_chain :process_attachment, :processing
8
+ end
9
+
10
+ module ClassMethods
11
+ # Yields a block containing an MiniMagick Image for the given binary data.
12
+ def with_image(file, &block)
13
+ begin
14
+ binary_data = file.is_a?(MiniMagick::Image) ? file : MiniMagick::Image.from_file(file) unless !Object.const_defined?(:MiniMagick)
15
+ rescue
16
+ # Log the failure to load the image.
17
+ logger.debug("Exception working with image: #{$!}")
18
+ binary_data = nil
19
+ end
20
+ block.call binary_data if block && binary_data
21
+ ensure
22
+ !binary_data.nil?
23
+ end
24
+ end
25
+
26
+ protected
27
+ def process_attachment_with_processing
28
+ return unless process_attachment_without_processing
29
+ with_image do |img|
30
+ resize_image_or_thumbnail! img
31
+ self.width = img[:width] if respond_to?(:width)
32
+ self.height = img[:height] if respond_to?(:height)
33
+ callback_with_args :after_resize, img
34
+ end if image?
35
+ end
36
+
37
+ # Performs the actual resizing operation for a thumbnail
38
+ def resize_image(img, size)
39
+ size = size.first if size.is_a?(Array) && size.length == 1
40
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
41
+ if size.is_a?(Fixnum)
42
+ size = [size, size]
43
+ img.resize(size.join('x'))
44
+ else
45
+ img.resize(size.join('x') + '!')
46
+ end
47
+ else
48
+ img.resize(size.to_s)
49
+ end
50
+ self.temp_path = img
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ require 'RMagick'
2
+ module AttachmerbFu # :nodoc:
3
+ module Processors
4
+ module RmagickProcessor
5
+ def self.included(base)
6
+ base.send :extend, ClassMethods
7
+ base.alias_method_chain :process_attachment, :processing
8
+ end
9
+
10
+ module ClassMethods
11
+ # Yields a block containing an RMagick Image for the given binary data.
12
+ def with_image(file, &block)
13
+ begin
14
+ binary_data = file.is_a?(Magick::Image) ? file : Magick::Image.read(file).first unless !Object.const_defined?(:Magick)
15
+ rescue
16
+ # Log the failure to load the image. This should match ::Magick::ImageMagickError
17
+ # but that would cause acts_as_attachment to require rmagick.
18
+ logger.debug("Exception working with image: #{$!}")
19
+ binary_data = nil
20
+ end
21
+ block.call binary_data if block && binary_data
22
+ ensure
23
+ !binary_data.nil?
24
+ end
25
+ end
26
+
27
+ protected
28
+ def process_attachment_with_processing
29
+ return unless process_attachment_without_processing
30
+ with_image do |img|
31
+ resize_image_or_thumbnail! img
32
+ self.width = img.columns if respond_to?(:width)
33
+ self.height = img.rows if respond_to?(:height)
34
+ callback_with_args :after_resize, img
35
+ end if image?
36
+ end
37
+
38
+ # Performs the actual resizing operation for a thumbnail
39
+ def resize_image(img, size)
40
+ size = size.first if size.is_a?(Array) && size.length == 1 && !size.first.is_a?(Fixnum)
41
+ if size.is_a?(Fixnum) || (size.is_a?(Array) && size.first.is_a?(Fixnum))
42
+ size = [size, size] if size.is_a?(Fixnum)
43
+ img.thumbnail!(*size)
44
+ else
45
+ img.change_geometry(size.to_s) { |cols, rows, image| image.resize!(cols, rows) }
46
+ end
47
+ self.temp_path = write_to_temp_file(img.to_blob)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,93 @@
1
+ # This Geometry class was yanked from RMagick. However, it lets ImageMagick handle the actual change_geometry.
2
+ # Use #new_dimensions_for to get new dimensons
3
+ # Used so I can use spiffy RMagick geometry strings with ImageScience
4
+ class Geometry
5
+ # ! and @ are removed until support for them is added
6
+ FLAGS = ['', '%', '<', '>']#, '!', '@']
7
+ RFLAGS = { '%' => :percent,
8
+ '!' => :aspect,
9
+ '<' => :>,
10
+ '>' => :<,
11
+ '@' => :area }
12
+
13
+ attr_accessor :width, :height, :x, :y, :flag
14
+
15
+ def initialize(width=nil, height=nil, x=nil, y=nil, flag=nil)
16
+ # Support floating-point width and height arguments so Geometry
17
+ # objects can be used to specify Image#density= arguments.
18
+ raise ArgumentError, "width must be >= 0: #{width}" if width < 0
19
+ raise ArgumentError, "height must be >= 0: #{height}" if height < 0
20
+ @width = width.to_f
21
+ @height = height.to_f
22
+ @x = x.to_i
23
+ @y = y.to_i
24
+ @flag = flag
25
+ end
26
+
27
+ # Construct an object from a geometry string
28
+ RE = /\A(\d*)(?:x(\d+))?([-+]\d+)?([-+]\d+)?([%!<>@]?)\Z/
29
+
30
+ def self.from_s(str)
31
+ raise(ArgumentError, "no geometry string specified") unless str
32
+
33
+ if m = RE.match(str)
34
+ new(m[1].to_i, m[2].to_i, m[3].to_i, m[4].to_i, RFLAGS[m[5]])
35
+ else
36
+ raise ArgumentError, "invalid geometry format"
37
+ end
38
+ end
39
+
40
+ # Convert object to a geometry string
41
+ def to_s
42
+ str = ''
43
+ str << "%g" % @width if @width > 0
44
+ str << 'x' if (@width > 0 || @height > 0)
45
+ str << "%g" % @height if @height > 0
46
+ str << "%+d%+d" % [@x, @y] if (@x != 0 || @y != 0)
47
+ str << FLAGS[@flag.to_i]
48
+ end
49
+
50
+ # attempts to get new dimensions for the current geometry string given these old dimensions.
51
+ # This doesn't implement the aspect flag (!) or the area flag (@). PDI
52
+ def new_dimensions_for(orig_width, orig_height)
53
+ new_width = orig_width
54
+ new_height = orig_height
55
+
56
+ case @flag
57
+ when :percent
58
+ scale_x = @width.zero? ? 100 : @width
59
+ scale_y = @height.zero? ? @width : @height
60
+ new_width = scale_x.to_f * (orig_width.to_f / 100.0)
61
+ new_height = scale_y.to_f * (orig_height.to_f / 100.0)
62
+ when :<, :>, nil
63
+ scale_factor =
64
+ if new_width.zero? || new_height.zero?
65
+ 1.0
66
+ else
67
+ if @width.nonzero? && @height.nonzero?
68
+ [@width.to_f / new_width.to_f, @height.to_f / new_height.to_f].min
69
+ else
70
+ @width.nonzero? ? (@width.to_f / new_width.to_f) : (@height.to_f / new_height.to_f)
71
+ end
72
+ end
73
+ new_width = scale_factor * new_width.to_f
74
+ new_height = scale_factor * new_height.to_f
75
+ new_width = orig_width if @flag && orig_width.send(@flag, new_width)
76
+ new_height = orig_height if @flag && orig_height.send(@flag, new_height)
77
+ end
78
+
79
+ [new_width, new_height].collect! { |v| v.round }
80
+ end
81
+ end
82
+
83
+ class Array
84
+ # allows you to get new dimensions for the current array of dimensions with a given geometry string
85
+ #
86
+ # [50, 64] / '40>' # => [40, 51]
87
+ def /(geometry)
88
+ raise ArgumentError, "Only works with a [width, height] pair" if size != 2
89
+ raise ArgumentError, "Must pass a valid geometry string or object" unless geometry.is_a?(String) || geometry.is_a?(Geometry)
90
+ geometry = Geometry.from_s(geometry) if geometry.is_a?(String)
91
+ geometry.new_dimensions_for first, last
92
+ end
93
+ end
@@ -0,0 +1,9 @@
1
+ require 'tempfile'
2
+
3
+ Tempfile.class_eval do
4
+ # overwrite so tempfiles use the extension of the basename. important for rmagick and image science
5
+ def make_tmpname(basename, n)
6
+ ext = nil
7
+ sprintf("%s%d-%d%s", basename.to_s.gsub(/\.\w+$/) { |s| ext = s; '' }, $$, n, ext)
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ test:
2
+ bucket_name: afu
3
+ access_key_id: YOURACCESSKEY
4
+ secret_access_key: YOURSECRETACCESSKEY
5
+ server: 127.0.0.1
6
+ port: 3002
@@ -0,0 +1,16 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
2
+
3
+ class DbFileTest < Test::Unit::TestCase
4
+ include BaseAttachmentTests
5
+ attachment_model Attachment
6
+
7
+ def test_should_call_after_attachment_saved(klass = Attachment)
8
+ attachment_model.saves = 0
9
+ assert_created do
10
+ upload_file :filename => '/files/rails.png'
11
+ end
12
+ assert_equal 1, attachment_model.saves
13
+ end
14
+
15
+ test_against_subclass :test_should_call_after_attachment_saved, Attachment
16
+ end
@@ -0,0 +1,80 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'test_helper'))
2
+
3
+ class FileSystemTest < Test::Unit::TestCase
4
+ include BaseAttachmentTests
5
+ attachment_model FileAttachment
6
+
7
+ def test_filesystem_size_for_file_attachment(klass = FileAttachment)
8
+ attachment_model klass
9
+ assert_created 1 do
10
+ attachment = upload_file :filename => '/files/rails.png'
11
+ assert_equal attachment.size, File.open(attachment.full_filename).stat.size
12
+ end
13
+ end
14
+
15
+ test_against_subclass :test_filesystem_size_for_file_attachment, FileAttachment
16
+
17
+ def test_should_not_overwrite_file_attachment(klass = FileAttachment)
18
+ attachment_model klass
19
+ assert_created 2 do
20
+ real = upload_file :filename => '/files/rails.png'
21
+ assert_valid real
22
+ assert !real.new_record?, real.errors.full_messages.join("\n")
23
+ assert !real.size.zero?
24
+
25
+ fake = upload_file :filename => '/files/fake/rails.png'
26
+ assert_valid fake
27
+ assert !fake.size.zero?
28
+
29
+ assert_not_equal File.open(real.full_filename).stat.size, File.open(fake.full_filename).stat.size
30
+ end
31
+ end
32
+
33
+ test_against_subclass :test_should_not_overwrite_file_attachment, FileAttachment
34
+
35
+ def test_should_store_file_attachment_in_filesystem(klass = FileAttachment)
36
+ attachment_model klass
37
+ attachment = nil
38
+ assert_created do
39
+ attachment = upload_file :filename => '/files/rails.png'
40
+ assert_valid attachment
41
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
42
+ end
43
+ attachment
44
+ end
45
+
46
+ test_against_subclass :test_should_store_file_attachment_in_filesystem, FileAttachment
47
+
48
+ def test_should_delete_old_file_when_updating(klass = FileAttachment)
49
+ attachment_model klass
50
+ attachment = upload_file :filename => '/files/rails.png'
51
+ old_filename = attachment.full_filename
52
+ assert_not_created do
53
+ use_temp_file 'files/rails.png' do |file|
54
+ attachment.filename = 'rails2.png'
55
+ attachment.temp_path = File.join(fixture_path, file)
56
+ attachment.save!
57
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
58
+ assert !File.exists?(old_filename), "#{old_filename} still exists"
59
+ end
60
+ end
61
+ end
62
+
63
+ test_against_subclass :test_should_delete_old_file_when_updating, FileAttachment
64
+
65
+ def test_should_delete_old_file_when_renaming(klass = FileAttachment)
66
+ attachment_model klass
67
+ attachment = upload_file :filename => '/files/rails.png'
68
+ old_filename = attachment.full_filename
69
+ assert_not_created do
70
+ attachment.filename = 'rails2.png'
71
+ attachment.save
72
+ assert File.exists?(attachment.full_filename), "#{attachment.full_filename} does not exist"
73
+ assert !File.exists?(old_filename), "#{old_filename} still exists"
74
+ assert !attachment.reload.size.zero?
75
+ assert_equal 'rails2.png', attachment.filename
76
+ end
77
+ end
78
+
79
+ test_against_subclass :test_should_delete_old_file_when_renaming, FileAttachment
80
+ end
@@ -0,0 +1,103 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'test_helper'))
2
+ require 'net/http'
3
+
4
+ class S3Test < Test::Unit::TestCase
5
+ if File.exist?(File.join(File.dirname(__FILE__), '../../amazon_s3.yml'))
6
+ include BaseAttachmentTests
7
+ attachment_model S3Attachment
8
+
9
+ def test_should_create_correct_bucket_name(klass = S3Attachment)
10
+ attachment_model klass
11
+ attachment = upload_file :filename => '/files/rails.png'
12
+ assert_equal attachment.s3_config[:bucket_name], attachment.bucket_name
13
+ end
14
+
15
+ test_against_subclass :test_should_create_correct_bucket_name, S3Attachment
16
+
17
+ def test_should_create_default_path_prefix(klass = S3Attachment)
18
+ attachment_model klass
19
+ attachment = upload_file :filename => '/files/rails.png'
20
+ assert_equal File.join(attachment_model.table_name, attachment.attachment_path_id), attachment.base_path
21
+ end
22
+
23
+ test_against_subclass :test_should_create_default_path_prefix, S3Attachment
24
+
25
+ def test_should_create_custom_path_prefix(klass = S3WithPathPrefixAttachment)
26
+ attachment_model klass
27
+ attachment = upload_file :filename => '/files/rails.png'
28
+ assert_equal File.join('some/custom/path/prefix', attachment.attachment_path_id), attachment.base_path
29
+ end
30
+
31
+ test_against_subclass :test_should_create_custom_path_prefix, S3WithPathPrefixAttachment
32
+
33
+ def test_should_create_valid_url(klass = S3Attachment)
34
+ attachment_model klass
35
+ attachment = upload_file :filename => '/files/rails.png'
36
+ assert_equal "#{s3_protocol}#{s3_hostname}#{s3_port_string}/#{attachment.bucket_name}/#{attachment.full_filename}", attachment.s3_url
37
+ end
38
+
39
+ test_against_subclass :test_should_create_valid_url, S3Attachment
40
+
41
+ def test_should_create_authenticated_url(klass = S3Attachment)
42
+ attachment_model klass
43
+ attachment = upload_file :filename => '/files/rails.png'
44
+ assert_match /^http.+AWSAccessKeyId.+Expires.+Signature.+/, attachment.authenticated_s3_url(:use_ssl => true)
45
+ end
46
+
47
+ test_against_subclass :test_should_create_authenticated_url, S3Attachment
48
+
49
+ def test_should_save_attachment(klass = S3Attachment)
50
+ attachment_model klass
51
+ assert_created do
52
+ attachment = upload_file :filename => '/files/rails.png'
53
+ assert_valid attachment
54
+ assert attachment.image?
55
+ assert !attachment.size.zero?
56
+ assert_kind_of Net::HTTPOK, http_response_for(attachment.s3_url)
57
+ end
58
+ end
59
+
60
+ test_against_subclass :test_should_save_attachment, S3Attachment
61
+
62
+ def test_should_delete_attachment_from_s3_when_attachment_record_destroyed(klass = S3Attachment)
63
+ attachment_model klass
64
+ attachment = upload_file :filename => '/files/rails.png'
65
+
66
+ urls = [attachment.s3_url] + attachment.thumbnails.collect(&:s3_url)
67
+
68
+ urls.each {|url| assert_kind_of Net::HTTPOK, http_response_for(url) }
69
+ attachment.destroy
70
+ urls.each do |url|
71
+ begin
72
+ http_response_for(url)
73
+ rescue Net::HTTPForbidden, Net::HTTPNotFound
74
+ nil
75
+ end
76
+ end
77
+ end
78
+
79
+ test_against_subclass :test_should_delete_attachment_from_s3_when_attachment_record_destroyed, S3Attachment
80
+
81
+ protected
82
+ def http_response_for(url)
83
+ url = URI.parse(url)
84
+ Net::HTTP.start(url.host, url.port) {|http| http.request_head(url.path) }
85
+ end
86
+
87
+ def s3_protocol
88
+ Technoweenie::AttachmentFu::Backends::S3Backend.protocol
89
+ end
90
+
91
+ def s3_hostname
92
+ Technoweenie::AttachmentFu::Backends::S3Backend.hostname
93
+ end
94
+
95
+ def s3_port_string
96
+ Technoweenie::AttachmentFu::Backends::S3Backend.port_string
97
+ end
98
+ else
99
+ def test_flunk_s3
100
+ puts "s3 config file not loaded, tests not running"
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,57 @@
1
+ module BaseAttachmentTests
2
+ def test_should_create_file_from_uploaded_file
3
+ assert_created do
4
+ attachment = upload_file :filename => '/files/foo.txt'
5
+ assert_valid attachment
6
+ assert !attachment.db_file.new_record? if attachment.respond_to?(:db_file)
7
+ assert attachment.image?
8
+ assert !attachment.size.zero?
9
+ #assert_equal 3, attachment.size
10
+ assert_nil attachment.width
11
+ assert_nil attachment.height
12
+ end
13
+ end
14
+
15
+ def test_reassign_attribute_data
16
+ assert_created 1 do
17
+ attachment = upload_file :filename => '/files/rails.png'
18
+ assert_valid attachment
19
+ assert attachment.size > 0, "no data was set"
20
+
21
+ attachment.temp_data = 'wtf'
22
+ assert attachment.save_attachment?
23
+ attachment.save!
24
+
25
+ assert_equal 'wtf', attachment_model.find(attachment.id).send(:current_data)
26
+ end
27
+ end
28
+
29
+ def test_no_reassign_attribute_data_on_nil
30
+ assert_created 1 do
31
+ attachment = upload_file :filename => '/files/rails.png'
32
+ assert_valid attachment
33
+ assert attachment.size > 0, "no data was set"
34
+
35
+ attachment.temp_data = nil
36
+ assert !attachment.save_attachment?
37
+ end
38
+ end
39
+
40
+ def test_should_overwrite_old_contents_when_updating
41
+ attachment = upload_file :filename => '/files/rails.png'
42
+ assert_not_created do # no new db_file records
43
+ use_temp_file 'files/rails.png' do |file|
44
+ attachment.filename = 'rails2.png'
45
+ attachment.temp_path = File.join(fixture_path, file)
46
+ attachment.save!
47
+ end
48
+ end
49
+ end
50
+
51
+ def test_should_save_without_updating_file
52
+ attachment = upload_file :filename => '/files/foo.txt'
53
+ assert_valid attachment
54
+ assert !attachment.save_attachment?
55
+ assert_nothing_raised { attachment.save! }
56
+ end
57
+ end