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,98 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
mini_paperclip (0.1.0)
|
5
|
+
activemodel
|
6
|
+
activesupport
|
7
|
+
aws-sdk-s3
|
8
|
+
mimemagic
|
9
|
+
mini_magick
|
10
|
+
|
11
|
+
GEM
|
12
|
+
remote: https://rubygems.org/
|
13
|
+
specs:
|
14
|
+
activemodel (6.0.3.4)
|
15
|
+
activesupport (= 6.0.3.4)
|
16
|
+
activerecord (6.0.3.4)
|
17
|
+
activemodel (= 6.0.3.4)
|
18
|
+
activesupport (= 6.0.3.4)
|
19
|
+
activesupport (6.0.3.4)
|
20
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
21
|
+
i18n (>= 0.7, < 2)
|
22
|
+
minitest (~> 5.1)
|
23
|
+
tzinfo (~> 1.1)
|
24
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
25
|
+
addressable (2.7.0)
|
26
|
+
public_suffix (>= 2.0.2, < 5.0)
|
27
|
+
aws-eventstream (1.1.0)
|
28
|
+
aws-partitions (1.399.0)
|
29
|
+
aws-sdk-core (3.109.3)
|
30
|
+
aws-eventstream (~> 1, >= 1.0.2)
|
31
|
+
aws-partitions (~> 1, >= 1.239.0)
|
32
|
+
aws-sigv4 (~> 1.1)
|
33
|
+
jmespath (~> 1.0)
|
34
|
+
aws-sdk-kms (1.39.0)
|
35
|
+
aws-sdk-core (~> 3, >= 3.109.0)
|
36
|
+
aws-sigv4 (~> 1.1)
|
37
|
+
aws-sdk-s3 (1.85.0)
|
38
|
+
aws-sdk-core (~> 3, >= 3.109.0)
|
39
|
+
aws-sdk-kms (~> 1)
|
40
|
+
aws-sigv4 (~> 1.1)
|
41
|
+
aws-sigv4 (1.2.2)
|
42
|
+
aws-eventstream (~> 1, >= 1.0.2)
|
43
|
+
concurrent-ruby (1.1.7)
|
44
|
+
crack (0.4.4)
|
45
|
+
diff-lcs (1.4.4)
|
46
|
+
hashdiff (1.0.1)
|
47
|
+
i18n (1.8.5)
|
48
|
+
concurrent-ruby (~> 1.0)
|
49
|
+
jmespath (1.4.0)
|
50
|
+
mimemagic (0.3.5)
|
51
|
+
mini_magick (4.11.0)
|
52
|
+
minitest (5.14.2)
|
53
|
+
public_suffix (4.0.6)
|
54
|
+
rack (2.2.3)
|
55
|
+
rack-test (1.1.0)
|
56
|
+
rack (>= 1.0, < 3)
|
57
|
+
rake (12.3.3)
|
58
|
+
rspec (3.10.0)
|
59
|
+
rspec-core (~> 3.10.0)
|
60
|
+
rspec-expectations (~> 3.10.0)
|
61
|
+
rspec-mocks (~> 3.10.0)
|
62
|
+
rspec-core (3.10.0)
|
63
|
+
rspec-support (~> 3.10.0)
|
64
|
+
rspec-expectations (3.10.0)
|
65
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
66
|
+
rspec-support (~> 3.10.0)
|
67
|
+
rspec-mocks (3.10.0)
|
68
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
69
|
+
rspec-support (~> 3.10.0)
|
70
|
+
rspec-support (3.10.0)
|
71
|
+
sqlite3 (1.4.2)
|
72
|
+
tapp (1.5.1)
|
73
|
+
thor
|
74
|
+
thor (1.0.1)
|
75
|
+
thread_safe (0.3.6)
|
76
|
+
tzinfo (1.2.8)
|
77
|
+
thread_safe (~> 0.1)
|
78
|
+
webmock (3.10.0)
|
79
|
+
addressable (>= 2.3.6)
|
80
|
+
crack (>= 0.3.2)
|
81
|
+
hashdiff (>= 0.4.0, < 2.0.0)
|
82
|
+
zeitwerk (2.4.1)
|
83
|
+
|
84
|
+
PLATFORMS
|
85
|
+
ruby
|
86
|
+
|
87
|
+
DEPENDENCIES
|
88
|
+
activerecord (~> 6.0.0)
|
89
|
+
mini_paperclip!
|
90
|
+
rack-test
|
91
|
+
rake (~> 12.0)
|
92
|
+
rspec (~> 3.0)
|
93
|
+
sqlite3
|
94
|
+
tapp
|
95
|
+
webmock
|
96
|
+
|
97
|
+
BUNDLED WITH
|
98
|
+
2.1.4
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "net/http"
|
5
|
+
require "active_model/validator"
|
6
|
+
require "active_support"
|
7
|
+
require "active_support/number_helper"
|
8
|
+
require "mini_magick"
|
9
|
+
require "mimemagic"
|
10
|
+
require "aws-sdk-s3"
|
11
|
+
|
12
|
+
require "mini_paperclip/attachment"
|
13
|
+
require "mini_paperclip/class_methods"
|
14
|
+
require "mini_paperclip/config"
|
15
|
+
require "mini_paperclip/interpolator"
|
16
|
+
require "mini_paperclip/storage"
|
17
|
+
require "mini_paperclip/validators"
|
18
|
+
require "mini_paperclip/version"
|
19
|
+
|
20
|
+
module MiniPaperclip
|
21
|
+
class << self
|
22
|
+
def config
|
23
|
+
@config ||= Config.new(
|
24
|
+
# defaults
|
25
|
+
interpolates: {
|
26
|
+
/:class/ => ->(*) { class_result },
|
27
|
+
/:attachment/ => ->(*) { attachment_result },
|
28
|
+
/:hash/ => ->(_, style) { hash_key(style) },
|
29
|
+
/:extension/ => ->(*) { File.extname(@record.read_attribute("#{@attachment_name}_file_name"))[1..-1] },
|
30
|
+
/:id/ => ->(*) { @record.id },
|
31
|
+
/:updated_at/ => ->(*) { @record.read_attribute("#{@attachment_name}_updated_at").to_i },
|
32
|
+
/:style/ => ->(_, style) { style }
|
33
|
+
},
|
34
|
+
hash_data: ":class/:attribute/:id/:style/:updated_at",
|
35
|
+
url_missing_path: ":attachment/:style/missing.png",
|
36
|
+
read_timeout: 60,
|
37
|
+
logger: Logger.new($stdout),
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MiniPaperclip
|
4
|
+
class Attachment
|
5
|
+
UnsupporedError = Class.new(StandardError)
|
6
|
+
|
7
|
+
attr_reader :record, :attachment_name, :config, :storage,
|
8
|
+
:waiting_write_file, :meta_content_type
|
9
|
+
|
10
|
+
def initialize(record, attachment_name, overwrite_config = {})
|
11
|
+
@record = record
|
12
|
+
@attachment_name = attachment_name
|
13
|
+
@config = MiniPaperclip.config.merge(overwrite_config)
|
14
|
+
@waiting_write_file = nil
|
15
|
+
@meta_content_type = nil
|
16
|
+
@dirty = false
|
17
|
+
@storage = Storage.const_get(@config.storage.to_s.camelcase)
|
18
|
+
.new(record, attachment_name, @config)
|
19
|
+
end
|
20
|
+
|
21
|
+
def original_filename
|
22
|
+
@record.read_attribute("#{@attachment_name}_file_name")
|
23
|
+
end
|
24
|
+
|
25
|
+
def content_type
|
26
|
+
@record.read_attribute("#{@attachment_name}_content_type")
|
27
|
+
end
|
28
|
+
|
29
|
+
def size
|
30
|
+
@record.read_attribute("#{@attachment_name}_file_size")
|
31
|
+
end
|
32
|
+
|
33
|
+
def updated_at
|
34
|
+
@record.read_attribute("#{@attachment_name}_updated_at").to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
def file?
|
38
|
+
original_filename.present?
|
39
|
+
end
|
40
|
+
alias_method :present?, :file?
|
41
|
+
|
42
|
+
def blank?
|
43
|
+
!file?
|
44
|
+
end
|
45
|
+
|
46
|
+
def exists?(style = :original)
|
47
|
+
file? && @storage.exists?(style)
|
48
|
+
end
|
49
|
+
|
50
|
+
def url(style = :original)
|
51
|
+
@storage.url_for_read(style)
|
52
|
+
end
|
53
|
+
|
54
|
+
def dirty?
|
55
|
+
@dirty
|
56
|
+
end
|
57
|
+
|
58
|
+
def assign(file)
|
59
|
+
@dirty = true
|
60
|
+
@waiting_copy_attachment = nil
|
61
|
+
@waiting_write_file = nil
|
62
|
+
@meta_content_type = nil
|
63
|
+
|
64
|
+
if file.nil?
|
65
|
+
# clear
|
66
|
+
@record.write_attribute("#{@attachment_name}_file_name", nil)
|
67
|
+
@record.write_attribute("#{@attachment_name}_content_type", nil)
|
68
|
+
@record.write_attribute("#{@attachment_name}_file_size", nil)
|
69
|
+
@record.write_attribute("#{@attachment_name}_updated_at", nil)
|
70
|
+
elsif file.instance_of?(Attachment)
|
71
|
+
# copy
|
72
|
+
@record.write_attribute("#{@attachment_name}_file_name", file.record.read_attribute("#{@attachment_name}_file_name"))
|
73
|
+
@record.write_attribute("#{@attachment_name}_content_type", file.record.read_attribute("#{@attachment_name}_content_type"))
|
74
|
+
@record.write_attribute("#{@attachment_name}_file_size", file.record.read_attribute("#{@attachment_name}_file_size"))
|
75
|
+
@record.write_attribute("#{@attachment_name}_updated_at", Time.current)
|
76
|
+
@waiting_copy_attachment = file
|
77
|
+
elsif file.respond_to?(:original_filename)
|
78
|
+
# e.g. ActionDispatch::Http::UploadedFile
|
79
|
+
@record.write_attribute("#{@attachment_name}_file_name", file.original_filename)
|
80
|
+
@record.write_attribute("#{@attachment_name}_content_type", strict_content_type(file.to_io))
|
81
|
+
@record.write_attribute("#{@attachment_name}_file_size", file.size)
|
82
|
+
@record.write_attribute("#{@attachment_name}_updated_at", Time.current)
|
83
|
+
@waiting_write_file = build_tempfile(file.tap(&:rewind))
|
84
|
+
@meta_content_type = file.content_type
|
85
|
+
elsif file.respond_to?(:path)
|
86
|
+
# e.g. File
|
87
|
+
@record.write_attribute("#{@attachment_name}_file_name", File.basename(file.path))
|
88
|
+
@record.write_attribute("#{@attachment_name}_content_type", strict_content_type(file))
|
89
|
+
@record.write_attribute("#{@attachment_name}_file_size", file.size)
|
90
|
+
@record.write_attribute("#{@attachment_name}_updated_at", Time.current)
|
91
|
+
@waiting_write_file = build_tempfile(file.tap(&:rewind))
|
92
|
+
elsif file.instance_of?(String)
|
93
|
+
if file.empty?
|
94
|
+
# do nothing
|
95
|
+
elsif file.start_with?('http')
|
96
|
+
# download from url
|
97
|
+
open_uri_option = {
|
98
|
+
read_timeout: MiniPaperclip.config.read_timeout || 60
|
99
|
+
}
|
100
|
+
uri = URI.parse(file)
|
101
|
+
uri.open(open_uri_option) do |io|
|
102
|
+
@record.write_attribute("#{@attachment_name}_file_name", File.basename(uri.path))
|
103
|
+
@record.write_attribute("#{@attachment_name}_content_type", strict_content_type(io))
|
104
|
+
@record.write_attribute("#{@attachment_name}_file_size", io.size)
|
105
|
+
@record.write_attribute("#{@attachment_name}_updated_at", Time.current)
|
106
|
+
@waiting_write_file = build_tempfile(io.tap(&:rewind))
|
107
|
+
@meta_content_type = io.meta["content-type"]
|
108
|
+
end
|
109
|
+
elsif file.start_with?('data:')
|
110
|
+
# data-uri
|
111
|
+
match_data = file.match(/\Adata:([-\w]+\/[-\w\+\.]+)?;base64,(.*)/m)
|
112
|
+
if match_data.nil?
|
113
|
+
raise UnsupporedError, "attachment for \"#{file[0..100]}\" is not supported"
|
114
|
+
end
|
115
|
+
raw = Base64.decode64(match_data[2])
|
116
|
+
@record.write_attribute("#{@attachment_name}_file_name", nil)
|
117
|
+
@record.write_attribute("#{@attachment_name}_content_type", strict_content_type(StringIO.new(raw)))
|
118
|
+
@record.write_attribute("#{@attachment_name}_file_size", raw.bytesize)
|
119
|
+
@record.write_attribute("#{@attachment_name}_updated_at", Time.current)
|
120
|
+
@waiting_write_file = build_tempfile(StringIO.new(raw))
|
121
|
+
@meta_content_type = match_data[1]
|
122
|
+
else
|
123
|
+
raise UnsupporedError, "attachment for \"#{file[0..100]}\" is not supported"
|
124
|
+
end
|
125
|
+
else
|
126
|
+
raise UnsupporedError, "attachment for #{file.class} is not supported"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def process_and_store
|
131
|
+
return unless file?
|
132
|
+
|
133
|
+
if @waiting_copy_attachment
|
134
|
+
debug("start attachment copy")
|
135
|
+
@storage.copy(:original, @waiting_copy_attachment)
|
136
|
+
@config.styles&.each do |style, size_arg|
|
137
|
+
@storage.copy(style, @waiting_copy_attachment)
|
138
|
+
end
|
139
|
+
@waiting_copy_attachment = nil
|
140
|
+
return
|
141
|
+
end
|
142
|
+
|
143
|
+
return if @waiting_write_file.nil?
|
144
|
+
|
145
|
+
begin
|
146
|
+
debug("start attachment styles process")
|
147
|
+
@storage.write(:original, @waiting_write_file)
|
148
|
+
@config.styles&.each do |style, size_arg|
|
149
|
+
Tempfile.create([style.to_s, File.extname(@waiting_write_file.path)]) do |temp|
|
150
|
+
MiniMagick::Tool::Convert.new do |convert|
|
151
|
+
convert << @waiting_write_file.path
|
152
|
+
if size_arg.end_with?('#')
|
153
|
+
# crop option
|
154
|
+
convert.resize("#{size_arg[0..-2]}^")
|
155
|
+
convert.gravity("center")
|
156
|
+
convert.extent(size_arg[0..-2])
|
157
|
+
else
|
158
|
+
convert.resize(size_arg)
|
159
|
+
end
|
160
|
+
convert << temp.path
|
161
|
+
end
|
162
|
+
@storage.write(style, temp)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
ensure
|
166
|
+
@waiting_write_file.close!
|
167
|
+
end
|
168
|
+
@waiting_write_file = nil
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def strict_content_type(io)
|
174
|
+
MimeMagic.by_magic(io)&.type
|
175
|
+
end
|
176
|
+
|
177
|
+
def build_tempfile(io)
|
178
|
+
temp = Tempfile.new(['MiniPaperclip'])
|
179
|
+
temp.binmode
|
180
|
+
debug("copying by tempfile from:#{io.class} to:#{temp.path}")
|
181
|
+
IO.copy_stream(io, temp)
|
182
|
+
temp.rewind
|
183
|
+
temp
|
184
|
+
end
|
185
|
+
|
186
|
+
def debug(str)
|
187
|
+
MiniPaperclip.config.logger.debug(str)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MiniPaperclip
|
4
|
+
module ClassMethods
|
5
|
+
def has_attached_file(attachment_name, option_config = {})
|
6
|
+
define_method(attachment_name) do
|
7
|
+
instance_variable_get("@#{attachment_name}") or
|
8
|
+
instance_variable_set("@#{attachment_name}", Attachment.new(self, attachment_name, option_config))
|
9
|
+
end
|
10
|
+
define_method("#{attachment_name}=") do |file|
|
11
|
+
a = Attachment.new(self, attachment_name, option_config)
|
12
|
+
a.assign(file)
|
13
|
+
instance_variable_set("@#{attachment_name}", a)
|
14
|
+
end
|
15
|
+
after_save do
|
16
|
+
if valid?
|
17
|
+
instance_variable_get("@#{attachment_name}")&.tap do |a|
|
18
|
+
a.dirty? && a.process_and_store
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
validates_with Validators::MediaTypeSpoofValidator, {
|
23
|
+
attributes: attachment_name,
|
24
|
+
if: -> { instance_variable_get("@#{attachment_name}")&.dirty? }
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def validates_attachment(attachment_name, content_type: nil, size: nil, presence: nil, geometry: nil, **opts)
|
29
|
+
if content_type
|
30
|
+
validates_with Validators::ContentTypeValidator, {
|
31
|
+
attributes: attachment_name.to_sym,
|
32
|
+
**content_type,
|
33
|
+
**opts,
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
if size
|
38
|
+
validates_with Validators::FileSizeValidator, {
|
39
|
+
attributes: attachment_name.to_sym,
|
40
|
+
**size,
|
41
|
+
**opts,
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
if !presence.nil?
|
46
|
+
validates_with Validators::PresenceValidator, {
|
47
|
+
attributes: attachment_name.to_sym,
|
48
|
+
presence: presence,
|
49
|
+
**opts,
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
if geometry
|
54
|
+
validates_with Validators::GeometryValidator, {
|
55
|
+
attributes: attachment_name.to_sym,
|
56
|
+
**geometry,
|
57
|
+
**opts,
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MiniPaperclip
|
4
|
+
class Config < Struct.new(
|
5
|
+
:storage,
|
6
|
+
:filesystem_path,
|
7
|
+
:hash_secret,
|
8
|
+
:hash_data,
|
9
|
+
:styles,
|
10
|
+
:url_scheme,
|
11
|
+
:url_host,
|
12
|
+
:url_path,
|
13
|
+
:url_missing_path,
|
14
|
+
:s3_host_alias,
|
15
|
+
:s3_bucket_name,
|
16
|
+
:s3_acl,
|
17
|
+
:s3_cache_control,
|
18
|
+
:interpolates,
|
19
|
+
:read_timeout,
|
20
|
+
:logger,
|
21
|
+
keyword_init: true,
|
22
|
+
)
|
23
|
+
def merge(hash)
|
24
|
+
dup.merge!(hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
def merge!(hash)
|
28
|
+
to_h.deep_merge(hash).each { |k, v| self[k] = v }
|
29
|
+
self
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MiniPaperclip
|
4
|
+
class Interpolator
|
5
|
+
def initialize(record, attachment_name, config)
|
6
|
+
@record = record
|
7
|
+
@attachment_name = attachment_name
|
8
|
+
@config = config
|
9
|
+
end
|
10
|
+
|
11
|
+
def interpolate(template, style)
|
12
|
+
template.dup.tap do |t|
|
13
|
+
@config.interpolates&.each do |matcher, block|
|
14
|
+
t.gsub!(matcher) { instance_exec(attachment, style, &block) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def class_result
|
22
|
+
@record.class.name.underscore.pluralize
|
23
|
+
end
|
24
|
+
|
25
|
+
def attachment_result
|
26
|
+
@attachment_name.to_s.downcase.pluralize
|
27
|
+
end
|
28
|
+
|
29
|
+
def attachment
|
30
|
+
@record.public_send(@attachment_name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def hash_key(style)
|
34
|
+
OpenSSL::HMAC.hexdigest(
|
35
|
+
OpenSSL::Digest::SHA1.new,
|
36
|
+
@config.hash_secret,
|
37
|
+
interpolate(@config.hash_data, style),
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|