mini_paperclip 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ruby.yml +40 -0
  3. data/.gitignore +14 -0
  4. data/.rspec +3 -0
  5. data/CODE_OF_CONDUCT.md +74 -0
  6. data/Gemfile +12 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +175 -0
  9. data/Rakefile +6 -0
  10. data/bin/console +14 -0
  11. data/bin/setup +8 -0
  12. data/gemfiles/rails_52.gemfile +10 -0
  13. data/gemfiles/rails_52.gemfile.lock +98 -0
  14. data/gemfiles/rails_60.gemfile +10 -0
  15. data/gemfiles/rails_60.gemfile.lock +98 -0
  16. data/lib/mini_paperclip.rb +41 -0
  17. data/lib/mini_paperclip/attachment.rb +190 -0
  18. data/lib/mini_paperclip/class_methods.rb +62 -0
  19. data/lib/mini_paperclip/config.rb +32 -0
  20. data/lib/mini_paperclip/interpolator.rb +41 -0
  21. data/lib/mini_paperclip/shoulda/matchers.rb +7 -0
  22. data/lib/mini_paperclip/shoulda/matchers/have_attached_file_matcher.rb +46 -0
  23. data/lib/mini_paperclip/shoulda/matchers/validate_attachment_content_type_matcher.rb +84 -0
  24. data/lib/mini_paperclip/shoulda/matchers/validate_attachment_geometry_matcher.rb +111 -0
  25. data/lib/mini_paperclip/shoulda/matchers/validate_attachment_presence_matcher.rb +52 -0
  26. data/lib/mini_paperclip/shoulda/matchers/validate_attachment_size_matcher.rb +62 -0
  27. data/lib/mini_paperclip/storage.rb +5 -0
  28. data/lib/mini_paperclip/storage/base.rb +39 -0
  29. data/lib/mini_paperclip/storage/filesystem.rb +34 -0
  30. data/lib/mini_paperclip/storage/s3.rb +49 -0
  31. data/lib/mini_paperclip/validators.rb +7 -0
  32. data/lib/mini_paperclip/validators/content_type_validator.rb +17 -0
  33. data/lib/mini_paperclip/validators/file_size_validator.rb +21 -0
  34. data/lib/mini_paperclip/validators/geometry_validator.rb +46 -0
  35. data/lib/mini_paperclip/validators/media_type_spoof_validator.rb +25 -0
  36. data/lib/mini_paperclip/validators/presence_validator.rb +13 -0
  37. data/lib/mini_paperclip/version.rb +5 -0
  38. data/mini_paperclip.gemspec +32 -0
  39. metadata +152 -0
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_paperclip/shoulda/matchers/have_attached_file_matcher"
4
+ require "mini_paperclip/shoulda/matchers/validate_attachment_content_type_matcher"
5
+ require "mini_paperclip/shoulda/matchers/validate_attachment_geometry_matcher"
6
+ require "mini_paperclip/shoulda/matchers/validate_attachment_presence_matcher"
7
+ require "mini_paperclip/shoulda/matchers/validate_attachment_size_matcher"
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniPaperclip
4
+ module Shoulda
5
+ module Matchers
6
+ # Ensures that the given instance or class has an attachment with the
7
+ # given name.
8
+ #
9
+ # Example:
10
+ # describe User do
11
+ # it { should have_attached_file(:avatar) }
12
+ # end
13
+ def have_attached_file name
14
+ HaveAttachedFileMatcher.new(name)
15
+ end
16
+
17
+ class HaveAttachedFileMatcher
18
+ def initialize attachment_name
19
+ @attachment_name = attachment_name
20
+ end
21
+
22
+ def matches? subject
23
+ @subject = subject.class == Class ? subject.new : subject
24
+
25
+ @subject.respond_to?(@attachment_name) &&
26
+ @subject.respond_to?("#{@attachment_name}=") &&
27
+ @subject.public_send(@attachment_name).kind_of?(MiniPaperclip::Attachment) &&
28
+ @subject.class.column_names.include?("#{@attachment_name}_file_name")
29
+ end
30
+
31
+ def failure_message
32
+ "Should have an attachment named #{@attachment_name}"
33
+ end
34
+
35
+ def failure_message_when_negated
36
+ "Should not have an attachment named #{@attachment_name}"
37
+ end
38
+ alias negative_failure_message failure_message_when_negated
39
+
40
+ def description
41
+ "have an attachment named #{@attachment_name}"
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniPaperclip
4
+ module Shoulda
5
+ module Matchers
6
+ # Ensures that the given instance or class validates the content type of
7
+ # the given attachment as specified.
8
+ #
9
+ # Example:
10
+ # describe User do
11
+ # it { should validate_attachment_content_type(:icon).
12
+ # allowing('image/png', 'image/gif').
13
+ # rejecting('text/plain', 'text/xml') }
14
+ # end
15
+ def validate_attachment_content_type(attachment_name)
16
+ ValidateAttachmentContentTypeMatcher.new(attachment_name)
17
+ end
18
+
19
+ class ValidateAttachmentContentTypeMatcher
20
+ def initialize(attachment_name)
21
+ @attachment_name = attachment_name.to_sym
22
+ @allowings = []
23
+ @rejectings = []
24
+ @fails = []
25
+ end
26
+
27
+ def allowing(*allowings)
28
+ @allowings.concat(allowings)
29
+ self
30
+ end
31
+
32
+ def rejecting(*rejectings)
33
+ @rejectings.concat(rejectings)
34
+ self
35
+ end
36
+
37
+ def matches?(subject)
38
+ @subject = subject.class == Class ? subject.new : subject
39
+
40
+ begin
41
+ fails = @allowings.reject do |allowing|
42
+ @subject.write_attribute("#{@attachment_name}_content_type", allowing)
43
+ @subject.write_attribute("#{@attachment_name}_updated_at", Time.now)
44
+ @subject.valid?
45
+ @subject.errors[:"#{@attachment_name}_content_type"].empty?
46
+ end
47
+ @fails.concat(fails).empty?
48
+ end && begin
49
+ fails = @rejectings.reject do |rejecting|
50
+ @subject.write_attribute("#{@attachment_name}_content_type", rejecting)
51
+ @subject.write_attribute("#{@attachment_name}_updated_at", Time.now)
52
+ @subject.valid?
53
+ @subject.errors[:"#{@attachment_name}_content_type"].present?
54
+ end
55
+ @fails.concat(fails).empty?
56
+ end
57
+ end
58
+
59
+ def failure_message
60
+ [
61
+ "Attachment :#{@attachment_name} expected to",
62
+ " allowing #{@allowings}",
63
+ " rejecting #{@rejectings}",
64
+ " but failed #{@fails}"
65
+ ].join("\n")
66
+ end
67
+
68
+ def failure_message_when_negated
69
+ [
70
+ "Attachment :#{@attachment_name} NOT expected to",
71
+ " allowing #{@allowings}",
72
+ " rejecting #{@rejectings}",
73
+ " but failed #{@fails}"
74
+ ].join("\n")
75
+ end
76
+ alias negative_failure_message failure_message_when_negated
77
+
78
+ def description
79
+ "validate the content types allowed on attachment :#{@attachment_name}"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniPaperclip
4
+ module Shoulda
5
+ module Matchers
6
+ # Ensures that the given instance or class validates the geometry of
7
+ # the given attachment as specified.
8
+ #
9
+ # Example:
10
+ # describe User do
11
+ # it { should validate_attachment_geometry(:icon)
12
+ # .format(:png)
13
+ # .width(less_than_or_equal_to: 100)
14
+ # .height(less_than_or_equal_to: 100)
15
+ # end
16
+ def validate_attachment_geometry(attachment_name)
17
+ ValidateAttachmentGeometryMatcher.new(attachment_name)
18
+ end
19
+
20
+ class ValidateAttachmentGeometryMatcher
21
+ CallError = Class.new(StandardError)
22
+
23
+ def initialize(attachment_name)
24
+ @attachment_name = attachment_name.to_sym
25
+ @width = {}
26
+ @height = {}
27
+ @format = nil
28
+ end
29
+
30
+ def format(format)
31
+ @format = format
32
+ self
33
+ end
34
+
35
+ def width(less_than_or_equal_to:)
36
+ @width[:less_than_or_equal_to] = less_than_or_equal_to
37
+ self
38
+ end
39
+
40
+ def height(less_than_or_equal_to:)
41
+ @height[:less_than_or_equal_to] = less_than_or_equal_to
42
+ self
43
+ end
44
+
45
+ def matches?(subject)
46
+ @subject = subject.class == Class ? subject.new : subject
47
+
48
+ unless @format && !@width.empty? && !@height.empty?
49
+ raise CallError, [
50
+ "should call like this",
51
+ " it { should validate_attachment_geometry(:image)",
52
+ " .format(:png)",
53
+ " .width(less_than_or_equal_to: 3000)",
54
+ " .height(less_than_or_equal_to: 3000) }"
55
+ ].join("\n")
56
+ end
57
+
58
+ when_valid && when_invalid
59
+ end
60
+
61
+ def failure_message
62
+ [
63
+ "Attachment :#{@attachment_name} got details",
64
+ @subject.errors.details[@attachment_name]
65
+ ].join("\n")
66
+ end
67
+
68
+ def failure_message_when_negated
69
+ [
70
+ "Attachment :#{@attachment_name} got details",
71
+ @subject.errors.details[@attachment_name]
72
+ ].join("\n")
73
+ end
74
+
75
+ private
76
+
77
+ def when_valid
78
+ create_dummy_image(width: @width[:less_than_or_equal_to], height: @height[:less_than_or_equal_to]) do |f|
79
+ @subject.public_send("#{@attachment_name}=", f)
80
+ end
81
+ @subject.valid?
82
+ @subject.errors.details[:image]&.find { |d| d[:error] == :geometry }.nil?
83
+ end
84
+
85
+ def when_invalid
86
+ create_dummy_image(width: @width[:less_than_or_equal_to] + 1, height: @height[:less_than_or_equal_to] + 1) do |f|
87
+ @subject.public_send("#{@attachment_name}=", f)
88
+ end
89
+ @subject.valid?
90
+ detail = @subject.errors.details[:image]&.find { |d| d[:error] == :geometry }
91
+ return false unless detail
92
+ return false unless detail[:expected_width_less_than_or_equal_to] == @width[:less_than_or_equal_to]
93
+ return false unless detail[:expected_height_less_than_or_equal_to] == @height[:less_than_or_equal_to]
94
+ true
95
+ end
96
+
97
+ def create_dummy_image(width:, height:)
98
+ Tempfile.create(['MiniPaperclip::Shoulda::Matchers::ValidateAttachmentGeometryMatcher', ".#{@format}"]) do |f|
99
+ MiniMagick::Tool::Convert.new do |convert|
100
+ convert.size("#{width}x#{height}")
101
+ convert.xc("none")
102
+ convert.strip
103
+ convert << f.path
104
+ end
105
+ yield f
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniPaperclip
4
+ module Shoulda
5
+ module Matchers
6
+ # Ensures that the given instance or class validates the presence of the
7
+ # given attachment.
8
+ #
9
+ # describe User do
10
+ # it { should validate_attachment_presence(:avatar) }
11
+ # end
12
+ def validate_attachment_presence(attachment_name)
13
+ ValidateAttachmentPresenceMatcher.new(attachment_name)
14
+ end
15
+
16
+ class ValidateAttachmentPresenceMatcher
17
+ def initialize(attachment_name)
18
+ @attachment_name = attachment_name.to_sym
19
+ end
20
+
21
+ def matches?(subject)
22
+ @subject = subject.class == Class ? subject.new : subject
23
+
24
+ begin
25
+ @subject.write_attribute("#{@attachment_name}_file_name", 'hello.png')
26
+ @subject.write_attribute("#{@attachment_name}_updated_at", Time.now)
27
+ @subject.valid?
28
+ @subject.errors[@attachment_name].empty?
29
+ end && begin
30
+ @subject.write_attribute("#{@attachment_name}_file_name", nil)
31
+ @subject.write_attribute("#{@attachment_name}_updated_at", Time.now)
32
+ @subject.valid?
33
+ @subject.errors[@attachment_name].present?
34
+ end
35
+ end
36
+
37
+ def failure_message
38
+ "Attachment :#{@attachment_name} should be required"
39
+ end
40
+
41
+ def failure_message_when_negated
42
+ "Attachment :#{@attachment_name} should not be required"
43
+ end
44
+ alias negative_failure_message failure_message_when_negated
45
+
46
+ def description
47
+ "require presence of attachment :#{@attachment_name}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniPaperclip
4
+ module Shoulda
5
+ module Matchers
6
+ # Ensures that the given instance or class validates the size of the
7
+ # given attachment as specified.
8
+ #
9
+ # Examples:
10
+ # it { should validate_attachment_size(:avatar).
11
+ # less_than(2.megabytes) }
12
+ def validate_attachment_size(attachment_name)
13
+ ValidateAttachmentSizeMatcher.new(attachment_name)
14
+ end
15
+
16
+ class ValidateAttachmentSizeMatcher
17
+ def initialize(attachment_name)
18
+ @attachment_name = attachment_name.to_sym
19
+ @less_than_size = nil
20
+ end
21
+
22
+ def less_than(less_than_size)
23
+ @less_than_size = less_than_size
24
+ self
25
+ end
26
+
27
+ def matches?(subject)
28
+ @subject = subject.class == Class ? subject.new : subject
29
+
30
+ begin
31
+ @subject.write_attribute("#{@attachment_name}_file_size", @less_than_size - 1)
32
+ @subject.write_attribute("#{@attachment_name}_updated_at", Time.now)
33
+ @subject.valid?
34
+ @subject.errors[:"#{@attachment_name}_file_size"].empty?
35
+ end && begin
36
+ @subject.write_attribute("#{@attachment_name}_file_size", @less_than_size)
37
+ @subject.write_attribute("#{@attachment_name}_updated_at", Time.now)
38
+ @subject.valid?
39
+ @subject.errors[:"#{@attachment_name}_file_size"].present?
40
+ end
41
+ end
42
+
43
+ def failure_message
44
+ "Attachment :#{@attachment_name} should be less than #{human_size}"
45
+ end
46
+
47
+ def failure_message_when_negated
48
+ "Attachment :#{@attachment_name} should not be less than #{human_size}"
49
+ end
50
+ alias negative_failure_message failure_message_when_negated
51
+
52
+ def description
53
+ "validate the size of attachment :#{@attachment_name}"
54
+ end
55
+
56
+ def human_size
57
+ ActiveSupport::NumberHelper.number_to_human_size(@less_than_size)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mini_paperclip/storage/base"
4
+ require "mini_paperclip/storage/filesystem"
5
+ require "mini_paperclip/storage/s3"
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniPaperclip
4
+ module Storage
5
+ class Base
6
+ attr_reader :config
7
+
8
+ def initialize(record, attachment_name, config)
9
+ @record = record
10
+ @attachment_name = attachment_name
11
+ @config = config
12
+ @interpolator = Interpolator.new(record, attachment_name, config)
13
+ end
14
+
15
+ def url_for_read(style)
16
+ "#{@config.url_scheme}://#{host}/#{path_for(style)}"
17
+ end
18
+
19
+ def path_for(style)
20
+ template = if @record.public_send(@attachment_name)&.file?
21
+ @config.url_path
22
+ else
23
+ @config.url_missing_path
24
+ end
25
+ interpolate(template, style)
26
+ end
27
+
28
+ def interpolate(template, style)
29
+ @interpolator.interpolate(template, style)
30
+ end
31
+
32
+ private
33
+
34
+ def debug(str)
35
+ MiniPaperclip.config.logger.debug(str)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniPaperclip
4
+ module Storage
5
+ class Filesystem < Base
6
+ def write(style, file)
7
+ path = file_path(style)
8
+ debug("writing by filesystem to #{path}")
9
+ FileUtils.mkdir_p(File.dirname(path))
10
+ FileUtils.cp(file.path, path)
11
+ end
12
+
13
+ def copy(style, from_attachment)
14
+ raise "not supported" unless from_attachment.storage.instance_of?(Filesystem)
15
+ to_path = file_path(style)
16
+ from_path = from_attachment.storage.file_path(style)
17
+ debug("copying by filesystem from:#{from_path} to:#{to_path}")
18
+ FileUtils.cp(from_path, to_path)
19
+ end
20
+
21
+ def file_path(style)
22
+ interpolate(@config.filesystem_path, style)
23
+ end
24
+
25
+ def host
26
+ @config.url_host
27
+ end
28
+
29
+ def exists?(style)
30
+ File.exists?(file_path(style))
31
+ end
32
+ end
33
+ end
34
+ end