mini_paperclip 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|