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.
- checksums.yaml +7 -0
- data/.github/workflows/ruby.yml +40 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +175 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/gemfiles/rails_52.gemfile +10 -0
- data/gemfiles/rails_52.gemfile.lock +98 -0
- data/gemfiles/rails_60.gemfile +10 -0
- data/gemfiles/rails_60.gemfile.lock +98 -0
- data/lib/mini_paperclip.rb +41 -0
- data/lib/mini_paperclip/attachment.rb +190 -0
- data/lib/mini_paperclip/class_methods.rb +62 -0
- data/lib/mini_paperclip/config.rb +32 -0
- data/lib/mini_paperclip/interpolator.rb +41 -0
- data/lib/mini_paperclip/shoulda/matchers.rb +7 -0
- data/lib/mini_paperclip/shoulda/matchers/have_attached_file_matcher.rb +46 -0
- data/lib/mini_paperclip/shoulda/matchers/validate_attachment_content_type_matcher.rb +84 -0
- data/lib/mini_paperclip/shoulda/matchers/validate_attachment_geometry_matcher.rb +111 -0
- data/lib/mini_paperclip/shoulda/matchers/validate_attachment_presence_matcher.rb +52 -0
- data/lib/mini_paperclip/shoulda/matchers/validate_attachment_size_matcher.rb +62 -0
- data/lib/mini_paperclip/storage.rb +5 -0
- data/lib/mini_paperclip/storage/base.rb +39 -0
- data/lib/mini_paperclip/storage/filesystem.rb +34 -0
- data/lib/mini_paperclip/storage/s3.rb +49 -0
- data/lib/mini_paperclip/validators.rb +7 -0
- data/lib/mini_paperclip/validators/content_type_validator.rb +17 -0
- data/lib/mini_paperclip/validators/file_size_validator.rb +21 -0
- data/lib/mini_paperclip/validators/geometry_validator.rb +46 -0
- data/lib/mini_paperclip/validators/media_type_spoof_validator.rb +25 -0
- data/lib/mini_paperclip/validators/presence_validator.rb +13 -0
- data/lib/mini_paperclip/version.rb +5 -0
- data/mini_paperclip.gemspec +32 -0
- 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,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
|