attachment_saver 1.0.0

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 (41) hide show
  1. data/.gitignore +3 -0
  2. data/MIT-LICENSE +19 -0
  3. data/README +137 -0
  4. data/Rakefile +16 -0
  5. data/attachment_saver.gemspec +41 -0
  6. data/init.rb +1 -0
  7. data/lib/attachment_saver.rb +171 -0
  8. data/lib/attachment_saver/version.rb +3 -0
  9. data/lib/attachment_saver_errors.rb +3 -0
  10. data/lib/datastores/file_system.rb +189 -0
  11. data/lib/datastores/in_column.rb +49 -0
  12. data/lib/misc/extended_tempfile.rb +12 -0
  13. data/lib/misc/file_size.rb +5 -0
  14. data/lib/misc/image_science_extensions.rb +102 -0
  15. data/lib/misc/mini_magick_extensions.rb +89 -0
  16. data/lib/processors/image.rb +187 -0
  17. data/lib/processors/image_science.rb +94 -0
  18. data/lib/processors/mini_magick.rb +103 -0
  19. data/lib/processors/r_magick.rb +120 -0
  20. data/test/attachment_saver_test.rb +162 -0
  21. data/test/database.yml +3 -0
  22. data/test/file_system_datastore_test.rb +468 -0
  23. data/test/fixtures/broken.jpg +1 -0
  24. data/test/fixtures/emptyextension. +0 -0
  25. data/test/fixtures/noextension +0 -0
  26. data/test/fixtures/test.jpg +0 -0
  27. data/test/fixtures/test.js +1 -0
  28. data/test/fixtures/wrongextension.png +0 -0
  29. data/test/image_fixtures.rb +69 -0
  30. data/test/image_operations.rb +114 -0
  31. data/test/image_processor_test.rb +67 -0
  32. data/test/image_processor_test_common.rb +81 -0
  33. data/test/image_science_processor_test.rb +20 -0
  34. data/test/in_column_datastore_test.rb +115 -0
  35. data/test/mini_magick_processor_test.rb +20 -0
  36. data/test/model_test.rb +205 -0
  37. data/test/public/.empty +0 -0
  38. data/test/rmagick_processor_test.rb +20 -0
  39. data/test/schema.rb +41 -0
  40. data/test/test_helper.rb +49 -0
  41. metadata +223 -0
@@ -0,0 +1 @@
1
+ test corrupt 'image'
Binary file
Binary file
@@ -0,0 +1 @@
1
+ This is a test file.
@@ -0,0 +1,69 @@
1
+ class ImageFixtures
2
+ def self.fixture_path(filename)
3
+ File.join(File.dirname(__FILE__), 'fixtures', filename)
4
+ end
5
+
6
+ def self.valid
7
+ { :path => fixture_path('test.jpg'),
8
+ :content_type => 'image/jpeg',
9
+ :original_filename => 'test.jpg',
10
+ :width => 448,
11
+ :height => 600,
12
+ :expected_content_type => 'image/jpeg',
13
+ :expected_extension => 'jpg' }
14
+ end
15
+
16
+ def self.wrong_extension
17
+ { :path => fixture_path('wrongextension.png'),
18
+ :content_type => 'image/png',
19
+ :original_filename => 'wrongextension.png',
20
+ :width => 448,
21
+ :height => 600,
22
+ :expected_content_type => 'image/jpeg',
23
+ :expected_extension => 'jpg' }
24
+ end
25
+
26
+ def self.no_extension
27
+ { :path => fixture_path('noextension'),
28
+ :content_type => 'application/octet-stream',
29
+ :original_filename => 'noextension',
30
+ :width => 448,
31
+ :height => 600,
32
+ :expected_content_type => 'image/jpeg',
33
+ :expected_extension => 'jpg' }
34
+ end
35
+
36
+ def self.empty_extension
37
+ { :path => fixture_path('emptyextension.'),
38
+ :content_type => 'application/octet-stream',
39
+ :original_filename => 'emptyextension.',
40
+ :width => 448,
41
+ :height => 600,
42
+ :expected_content_type => 'image/jpeg',
43
+ :expected_extension => 'jpg' }
44
+ end
45
+
46
+ def self.corrupt
47
+ { :path => fixture_path('broken.jpg'),
48
+ :content_type => 'image/jpeg',
49
+ :original_filename => 'broken.jpg',
50
+ :expected_content_type => 'image/jpeg',
51
+ :expected_extension => 'jpg' }
52
+ end
53
+
54
+ def self.non_image_file
55
+ { :path => fixture_path('test.js'),
56
+ :content_type => 'application/javascript',
57
+ :original_filename => 'test.js',
58
+ :expected_content_type => 'application/javascript',
59
+ :expected_extension => 'js' }
60
+ end
61
+
62
+ def self.all_readable
63
+ [valid, wrong_extension, no_extension, empty_extension]
64
+ end
65
+
66
+ def self.all_unreadable
67
+ [corrupt, non_image_file]
68
+ end
69
+ end
@@ -0,0 +1,114 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'image_fixtures'))
2
+
3
+ class ImageOperations
4
+ def self.original_image
5
+ { :path => ImageFixtures::fixture_path('test.jpg'),
6
+ :content_type => 'image/jpeg',
7
+ :original_filename => 'test.jpg',
8
+ :width => 448,
9
+ :height => 600 }
10
+ end
11
+
12
+ def self.resize_operations
13
+ {
14
+ :squish1 => [:squish, 100, 50],
15
+ :squish2 => [:squish, 10, 50],
16
+
17
+ :scaleby1 => [:scale_by, 0.125, 0.125],
18
+ :scaleby2 => [:scale_by, 0.5, 1.0],
19
+
20
+ :scaletosize1 => [:scale_to_fit, 200, 300],
21
+ :scaletosize2 => [:scale_to_fit, 720, 900],
22
+ :shrinktosize1 => [:shrink_to_fit, 200, 300],
23
+ :shrinktosize2 => [:shrink_to_fit, 720, 900],
24
+ :expandtosize1 => [:expand_to_fit, 200, 300],
25
+ :expandtosize2 => [:expand_to_fit, 720, 900],
26
+ :expandtosize3 => [:expand_to_fit, 200, 900],
27
+ :expandtosize4 => [:expand_to_fit, 720, 300],
28
+
29
+ :scaletowidth1 => [:scale_to_fit, 200, nil],
30
+ :scaletowidth2 => [:scale_to_fit, 720, nil],
31
+ :shrinktowidth1 => [:shrink_to_fit, 200, nil],
32
+ :shrinktowidth2 => [:shrink_to_fit, 720, nil],
33
+
34
+ :scaletoheight1 => [:scale_to_fit, nil, 300],
35
+ :scaletoheight2 => [:scale_to_fit, nil, 900],
36
+ :shrinktoheight1 => [:shrink_to_fit, nil, 300],
37
+ :shrinktoheight2 => [:shrink_to_fit, nil, 900],
38
+
39
+ :scaletocover1 => [:scale_to_cover, 200, 300],
40
+ :scaletocover2 => [:scale_to_cover, 720, 900],
41
+
42
+ :coverandcrop1 => [:cover_and_crop, 200, 300],
43
+ :coverandcrop2 => [:cover_and_crop, 720, 900],
44
+ }
45
+ end
46
+
47
+ def self.expected_results
48
+ {
49
+ :squish1 => [100, 50],
50
+ :squish2 => [10, 50],
51
+
52
+ :scaleby1 => [56, 75],
53
+ :scaleby2 => [224, 600],
54
+
55
+ :scaletosize1 => [200, 200*600/448],
56
+ :scaletosize2 => [900*448/600, 900],
57
+ :shrinktosize1 => [200, 200*600/448],
58
+ :shrinktosize2 => [448, 600],
59
+ :expandtosize1 => [448, 600],
60
+ :expandtosize2 => [900*448/600, 900],
61
+ :expandtosize3 => [448, 600],
62
+ :expandtosize4 => [448, 600],
63
+
64
+ :scaletowidth1 => [200, 200*600/448],
65
+ :scaletowidth2 => [720, 720*600/448],
66
+ :shrinktowidth1 => [200, 200*600/448],
67
+ :shrinktowidth2 => [448, 600],
68
+
69
+ :scaletoheight1 => [300*448/600, 300],
70
+ :scaletoheight2 => [900*448/600, 900],
71
+ :shrinktoheight1 => [300*448/600, 300],
72
+ :shrinktoheight2 => [448, 600],
73
+
74
+ :scaletocover1 => [300*448/600, 300],
75
+ :scaletocover2 => [720, 720*600/448],
76
+
77
+ :coverandcrop1 => [200, 300],
78
+ :coverandcrop2 => [720, 900],
79
+ }
80
+ end
81
+
82
+ def self.geometry_strings
83
+ {
84
+ '110x50' => [:scale_to_fit, 110, 50],
85
+ '209' => [:scale_to_fit, 209, 209],
86
+ '91x' => [:scale_to_fit, 91, nil],
87
+ 'x48' => [:scale_to_fit, nil, 48],
88
+
89
+ '110x50!' => [:squish, 110, 50],
90
+ '209!' => [:squish, 209, 209],
91
+ '91x!' => [:scale_to_fit, 91, nil], # ! flag has no meaning/effect here
92
+ 'x48!' => [:scale_to_fit, nil, 48], # same
93
+
94
+ '110.5x50.2%' => [:scale_by, 1.105, 0.502],
95
+ '209.84%' => [:scale_by, 2.0984, 2.0984],
96
+ '91.0x%' => [:scale_by, 0.910, 0.910],
97
+ 'x48.%' => [:scale_by, 0.480, 0.480],
98
+
99
+ '110x50>' => [:shrink_to_fit, 110, 50],
100
+ '209>' => [:shrink_to_fit, 209, 209],
101
+ '91x>' => [:shrink_to_fit, 91, nil],
102
+ 'x48>' => [:shrink_to_fit, nil, 48],
103
+
104
+ '110x50<' => [:expand_to_fit, 110, 50],
105
+ '209<' => [:expand_to_fit, 209, 209],
106
+ '91x<' => [:expand_to_fit, 91, nil],
107
+ 'x48<' => [:expand_to_fit, nil, 48],
108
+
109
+ '110x50#' => [:cover_and_crop, 110, 50],
110
+ '91x#' => [:cover_and_crop, 91, nil],
111
+ 'x48#' => [:cover_and_crop, nil, 48],
112
+ }
113
+ end
114
+ end
@@ -0,0 +1,67 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+ require 'mocha'
3
+ require 'processors/image'
4
+ require File.expand_path(File.join(File.dirname(__FILE__), 'image_operations'))
5
+
6
+ class ImageProcessorTest < Test::Unit::TestCase
7
+ class SomeModel
8
+ include AttachmentSaver::Processors::Image
9
+
10
+ cattr_accessor :attachment_options
11
+ attr_accessor :content_type
12
+
13
+ def initialize(content_type)
14
+ self.content_type = content_type
15
+ end
16
+ end
17
+
18
+ def test_image?
19
+ [nil, '', '/', 'foo', 'foo/' '/bar', 'text/plain', 'x-image/invalid', 'image-x/invalid'].each do |mime|
20
+ assert_equal false, SomeModel.new(mime).image?
21
+ end
22
+
23
+ ['image/jpeg', 'image/png', 'image/gif'].each do |mime|
24
+ assert_equal true, SomeModel.new(mime).image?
25
+ end
26
+ end
27
+
28
+ def test_process_attachment?
29
+ SomeModel.attachment_options = {}
30
+ assert_equal false, SomeModel.new('text/plain').process_attachment?
31
+ assert_equal false, SomeModel.new('image/jpeg').process_attachment?
32
+
33
+ SomeModel.attachment_options = {:formats => {'thumb' => '50x50'}}
34
+ assert_equal false, SomeModel.new('text/plain').process_attachment?
35
+ assert_equal true, SomeModel.new('image/jpeg').process_attachment?
36
+ end
37
+
38
+ def test_before_validate_attachment
39
+ model_without_upload = SomeModel.new('application/octet-stream') # note the content type doesn't have to be an image for examine_attachment to be called, since examine_attachment is supposed to fix up incorrect client-supplied content-types
40
+ model_without_upload.expects(:uploaded_file).times(1).returns(nil)
41
+ model_without_upload.expects(:examine_attachment).times(0)
42
+ model_without_upload.before_validate_attachment
43
+
44
+ model_with_upload = SomeModel.new('application/octet-stream')
45
+ model_with_upload.expects(:uploaded_file).times(1).returns('dummy')
46
+ model_with_upload.expects(:examine_attachment).times(1)
47
+ model_with_upload.before_validate_attachment
48
+
49
+ model_with_non_image_upload = SomeModel.new('text/plain')
50
+ model_with_non_image_upload.expects(:uploaded_file).times(1).returns('dummy')
51
+ model_with_non_image_upload.expects(:examine_attachment).times(1).raises(ImageProcessorError)
52
+ model_with_non_image_upload.before_validate_attachment
53
+ assert_equal 'text/plain', model_with_non_image_upload.content_type
54
+
55
+ model_with_mislabelled_non_image_upload = SomeModel.new('image/png')
56
+ model_with_mislabelled_non_image_upload.expects(:uploaded_file).times(1).returns('dummy')
57
+ model_with_mislabelled_non_image_upload.expects(:examine_attachment).times(1).raises(ImageProcessorError)
58
+ model_with_mislabelled_non_image_upload.before_validate_attachment
59
+ assert_equal 'application/octet-stream', model_with_mislabelled_non_image_upload.content_type
60
+ end
61
+
62
+ def test_from_geometry_string
63
+ ImageOperations::geometry_strings.each do |geometry, expected_result|
64
+ assert_equal expected_result, AttachmentSaver::Processors::Image.from_geometry_string(geometry), "geometry parse of #{geometry} produced incorrect results"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,81 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+ require 'attachment_saver'
3
+ require File.expand_path(File.join(File.dirname(__FILE__), 'image_fixtures'))
4
+ require File.expand_path(File.join(File.dirname(__FILE__), 'image_operations'))
5
+
6
+ module ImageProcessorTestModel
7
+ attr_accessor :content_type, :original_filename, :width, :height
8
+
9
+ def self.included(base)
10
+ base.cattr_accessor :attachment_options
11
+ end
12
+
13
+ def initialize(uploaded_data)
14
+ self.uploaded_data = uploaded_data
15
+ end
16
+
17
+ def new_record?
18
+ true
19
+ end
20
+
21
+ def build_derived(attrs)
22
+ @derived_attributes ||= {}
23
+ raise "already have a derived image named #{attrs[:format_name]}" if @derived_attributes[attrs[:format_name].to_s]
24
+ @derived_attributes[attrs[:format_name].to_s] = attrs # and it would build a record, not just save the attributes
25
+ end
26
+
27
+ def find_derived(format_name)
28
+ @derived_attributes[format_name.to_s]
29
+ end
30
+ end
31
+
32
+ module ImageProcessorTests
33
+ def test_attributes_from_valid
34
+ processor_model.attachment_options = {}
35
+ ImageFixtures.all_readable.each do |fixture|
36
+ model = processor_model.new(File.open(fixture[:path], 'rb'))
37
+ model.content_type = fixture[:content_type]
38
+ model.original_filename = fixture[:original_filename]
39
+ model.examine_attachment
40
+ assert_equal fixture[:expected_content_type], model.content_type
41
+ assert_equal fixture[:width], model.width
42
+ assert_equal fixture[:height], model.height
43
+ assert_equal "#{fixture[:width]}x#{fixture[:height]}", model.image_size
44
+ assert_equal fixture[:expected_extension], model.file_extension
45
+ end
46
+ end
47
+
48
+ def test_attributes_from_invalid
49
+ processor_model.attachment_options = {}
50
+ ImageFixtures.all_unreadable.each do |fixture|
51
+ model = processor_model.new(File.open(fixture[:path], 'rb'))
52
+ model.content_type = fixture[:content_type]
53
+ assert_raises(processor_exception) { model.examine_attachment }
54
+ assert_equal fixture[:expected_content_type], model.content_type
55
+ assert_equal nil, model.width
56
+ assert_equal nil, model.height
57
+ assert_equal nil, model.image_size
58
+ assert_equal 'bin', model.file_extension
59
+ model.original_filename = fixture[:original_filename]
60
+ assert_equal fixture[:expected_extension], model.file_extension
61
+ end
62
+ end
63
+
64
+ def test_derived_attributes_from_valid
65
+ processor_model.attachment_options = {:formats => ImageOperations.resize_operations}
66
+
67
+ fixture = ImageOperations.original_image
68
+ model = processor_model.new(File.open(fixture[:path], 'rb'))
69
+ model.content_type = fixture[:content_type]
70
+ model.original_filename = fixture[:original_filename]
71
+ model.process_attachment(model.uploaded_file_path)
72
+
73
+ ImageOperations.expected_results.each do |format_name, size|
74
+ derived = model.find_derived(format_name)
75
+ assert !derived.nil?, "no derived image named #{format_name} generated"
76
+ assert_equal size.first, derived[:width], "#{format_name} width incorrect"
77
+ assert_equal size.last, derived[:height], "#{format_name} height incorrect"
78
+ assert_equal model.file_extension, derived[:file_extension], "#{format_name} file_extension incorrect"
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'image_processor_test_common'))
2
+ require 'processors/image_science'
3
+
4
+ class ImageScienceProcessorTest < Test::Unit::TestCase
5
+ class ImageScienceTestModel
6
+ include AttachmentSaver::InstanceMethods
7
+ include AttachmentSaver::Processors::ImageScience
8
+ include ImageProcessorTestModel
9
+ end
10
+
11
+ def processor_model
12
+ ImageScienceTestModel
13
+ end
14
+
15
+ def processor_exception
16
+ ImageScienceProcessorError
17
+ end
18
+
19
+ include ImageProcessorTests
20
+ end
@@ -0,0 +1,115 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper'))
2
+ require 'mocha'
3
+ require 'datastores/in_column'
4
+
5
+ class InColumnDatastoreTest < Test::Unit::TestCase
6
+ attr_accessor :data, :original_filename, :content_type
7
+
8
+ DEFAULT_ATTACHMENT_OPTIONS = {}
9
+
10
+ def self.attachment_options
11
+ @@attachment_options ||= DEFAULT_ATTACHMENT_OPTIONS
12
+ end
13
+
14
+ include AttachmentSaver::DataStores::InColumn
15
+
16
+ def uploaded_data
17
+ if @uploaded_data.nil?
18
+ if @uploaded_file.nil?
19
+ nil
20
+ else
21
+ @uploaded_file.rewind
22
+ @uploaded_file.read
23
+ end
24
+ else
25
+ @uploaded_data
26
+ end
27
+ end
28
+
29
+ def setup
30
+ @test_filename = File.join(RAILS_ROOT, "tmp", "test#{$$}.dat")
31
+ @uploaded_data = nil
32
+ @uploaded_file = nil
33
+ @saved_to = nil
34
+ self.data = nil
35
+ self.original_filename = 'myfile.dat'
36
+ self.content_type = 'application/octet-stream'
37
+ end
38
+
39
+ def random_data(length = nil)
40
+ length = 512 + rand(2048) if length.nil?
41
+ Array.new(length).collect {rand(256)} .pack('C*')
42
+ end
43
+
44
+ def save_attachment_test(expected_data)
45
+ expects(:process_attachment?).times(1).returns(false)
46
+ save_attachment
47
+
48
+ assert !data.nil? && data != "", "no data saved"
49
+ data.force_encoding("ascii-8bit") if data.respond_to?(:force_encoding)
50
+ assert expected_data == data, "data stored doesn't match"
51
+ end
52
+
53
+
54
+ def test_save_attachment_for_data
55
+ @uploaded_data = data = random_data
56
+ @save_upload = true
57
+ save_attachment_test(data)
58
+ end
59
+
60
+ def test_save_attachment_for_file
61
+ data = random_data
62
+ FileUtils.mkdir_p(File.dirname(@test_filename))
63
+ File.open(@test_filename + '.src_file', 'wb+') do |file|
64
+ file.write(data)
65
+ @uploaded_file = file
66
+ @save_upload = true
67
+ save_attachment_test(data)
68
+ end
69
+ end
70
+
71
+ def test_save_attachment_for_tempfile
72
+ data = random_data
73
+ FileUtils.mkdir_p(File.dirname(@test_filename))
74
+ Tempfile.open('src_file', File.dirname(@test_filename)) do |file|
75
+ file.write(data)
76
+ @uploaded_file = file
77
+ @save_upload = true
78
+ save_attachment_test(data)
79
+ end
80
+ end
81
+
82
+
83
+ def test_save_attachment_calls_processing
84
+ @uploaded_data = expected_data = random_data
85
+ @save_upload = true
86
+ expects(:process_attachment?).times(1).returns(true)
87
+ expects(:process_attachment_with_wrapping).times(1).returns do |filename|
88
+ assert expected_data == File.read(filename)
89
+ end
90
+
91
+ save_attachment
92
+
93
+ assert expected_data == data
94
+ end
95
+
96
+
97
+ def test_save_attachment_without_upload
98
+ expects(:process_attachment?).times(0)
99
+ expects(:process_attachment).times(0)
100
+ save_attachment
101
+ assert_equal nil, data
102
+ end
103
+
104
+
105
+ def test_in_storage?
106
+ @uploaded_data = random_data
107
+ @save_upload = true
108
+ expects(:process_attachment?).times(1).returns(false)
109
+ save_attachment
110
+
111
+ assert_equal true, in_storage?
112
+ self.data = nil
113
+ assert_equal false, in_storage?
114
+ end
115
+ end