mini_paperclip 0.1.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 (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