attachmerb_fu 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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