shrine 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

@@ -1,16 +1,21 @@
1
1
  class Shrine
2
2
  module Plugins
3
3
  # The versions plugin enables your uploader to deal with versions. To
4
- # generate versions, you simply return a hash of versions in `Shrine#process`:
4
+ # generate versions, you simply return a hash of versions in `Shrine#process`.
5
+ # Here is an example of processing image thumbnails using the
6
+ # [image_processing] gem:
7
+ #
8
+ # require "image_processing/mini_magick"
5
9
  #
6
10
  # class ImageUploader < Shrine
11
+ # include ImageProcessing::MiniMagick
7
12
  # plugin :versions, names: [:large, :medium, :small]
8
13
  #
9
14
  # def process(io, context)
10
15
  # if context[:phase] == :store
11
- # size_700 = process_to_limit!(io.download, 700, 700)
12
- # size_500 = process_to_limit!(size_700, 500, 500)
13
- # size_300 = process_to_limit!(size_500, 300, 300)
16
+ # size_700 = resize_to_limit(io.download, 700, 700)
17
+ # size_500 = resize_to_limit(size_700, 500, 500)
18
+ # size_300 = resize_to_limit(size_500, 300, 300)
14
19
  #
15
20
  # {large: size_700, medium: size_500, small: size_300}
16
21
  # end
@@ -20,13 +25,19 @@ class Shrine
20
25
  # Now when you access the attachment through the model, a hash of uploaded
21
26
  # files will be returned:
22
27
  #
28
+ # JSON.parse(user.avatar_data) #=>
29
+ # # {
30
+ # # "large" => {"id" => "lg043.jpg", "storage" => "store", "metadata" => {...}},
31
+ # # "medium" => {"id" => "kd9fk.jpg", "storage" => "store", "metadata" => {...}},
32
+ # # "small" => {"id" => "932fl.jpg", "storage" => "store", "metadata" => {...}},
33
+ # # }
34
+ #
23
35
  # user.avatar #=>
24
36
  # # {
25
- # # large: #<Shrine::UploadedFile>,
26
- # # medium: #<Shrine::UploadedFile>,
27
- # # small: #<Shrine::UploadedFile>,
37
+ # # :large => #<Shrine::UploadedFile @data={"id"=>"lg043.jpg", ...}>,
38
+ # # :medium => #<Shrine::UploadedFile @data={"id"=>"kd9fk.jpg", ...}>,
39
+ # # :small => #<Shrine::UploadedFile @data={"id"=>"932fl.jpg", ...}>,
28
40
  # # }
29
- # user.avatar.class #=> Hash
30
41
  #
31
42
  # # With the store_dimensions plugin
32
43
  # user.avatar[:large].width #=> 700
@@ -64,7 +75,11 @@ class Shrine
64
75
  #
65
76
  # When deleting versions, any multi delete capabilities will be leveraged,
66
77
  # so when usingStorage::S3, deleting versions will issue only a single HTTP
67
- # request.
78
+ # request. If you want to delete versions manually, you can use
79
+ # `Shrine#delete`:
80
+ #
81
+ # versions.keys #=> [:small, :medium, :large]
82
+ # ImageUploader.new(:storage).delete(versions) # deletes a hash of versions
68
83
  module Versions
69
84
  def self.load_dependencies(uploader, *)
70
85
  uploader.plugin :multi_delete
@@ -86,21 +101,19 @@ class Shrine
86
101
 
87
102
  # Asserts that the hash doesn't contain any unknown versions.
88
103
  def versions!(hash)
89
- hash.select do |name, version|
90
- version?(name) or raise Error, "unknown version: #{name.inspect}"
91
- end
104
+ hash.select { |name, _| version?(name) or raise Error, "unknown version: #{name.inspect}" }
92
105
  end
93
106
 
94
107
  # Filters the hash to contain only the registered versions.
95
108
  def versions(hash)
96
- hash.select { |name, version| version?(name) }
109
+ hash.select { |name, _| version?(name) }
97
110
  end
98
111
 
99
112
  # Converts a hash of data into a hash of versions.
100
113
  def uploaded_file(object, &block)
101
114
  if object.is_a?(Hash) && !object.key?("storage")
102
115
  versions(object).inject({}) do |result, (name, data)|
103
- result.update(name.to_sym => super(data, &block))
116
+ result.update(name.to_sym => uploaded_file(data, &block))
104
117
  end
105
118
  else
106
119
  super
@@ -112,7 +125,7 @@ class Shrine
112
125
  # Checks whether all versions are uploaded by this uploader.
113
126
  def uploaded?(uploaded_file)
114
127
  if (hash = uploaded_file).is_a?(Hash)
115
- hash.all? { |name, version| super(version) }
128
+ hash.all? { |name, version| uploaded?(version) }
116
129
  else
117
130
  super
118
131
  end
@@ -127,7 +140,7 @@ class Shrine
127
140
  def _store(io, context)
128
141
  if (hash = io).is_a?(Hash)
129
142
  self.class.versions!(hash).inject({}) do |result, (name, version)|
130
- result.update(name => super(version, version: name, **context))
143
+ result.update(name => _store(version, version: name, **context))
131
144
  end
132
145
  else
133
146
  super
@@ -138,7 +151,7 @@ class Shrine
138
151
  # capabilities.
139
152
  def _delete(uploaded_file, context)
140
153
  if (versions = uploaded_file).is_a?(Hash)
141
- super(versions.values, context)
154
+ _delete(versions.values, context)
142
155
  versions
143
156
  else
144
157
  super
@@ -5,29 +5,74 @@ require "pathname"
5
5
 
6
6
  class Shrine
7
7
  module Storage
8
+ # The FileSystem storage handles uploads to the filesystem, and it is
9
+ # most commonly initialized with a "base" folder and a "subdirectory":
10
+ #
11
+ # storage = Shrine::Storage::FileSystem.new("public", subdirectory: "uploads")
12
+ # storage.url("image.jpg") #=> "/uploads/image.jpg"
13
+ #
14
+ # This storage will upload all files to "public/uploads", and the URLs
15
+ # of the uploaded files will start with "/uploads/*". This way you can
16
+ # use FileSystem for both cache and store, one having subdirectory
17
+ # "uploads/cache" and other "uploads/store".
18
+ #
19
+ # You can also initialize the storage just with the "base" directory, and
20
+ # then the FileSystem storage will generate absolute URLs to files:
21
+ #
22
+ # storage = Shrine::Storage::FileSystem.new(Dir.tmpdir)
23
+ # storage.url("image.jpg") #=> "/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/image.jpg"
24
+ #
25
+ # ## CDN
26
+ #
27
+ # It's generally a good idea to serve your files via a CDN, so an
28
+ # additional `:host` option can be provided:
29
+ #
30
+ # storage = Shrine::Storage::FileSystem.new("public",
31
+ # subdirectory: "uploads", host: "//abc123.cloudfront.net")
32
+ # storage.url("image.jpg") #=> "//abc123.cloudfront.net/uploads/image.jpg"
33
+ #
34
+ # The `:host` option can also be used wihout `:subdirectory`, and is
35
+ # useful if you for example have files located on another server:
36
+ #
37
+ # storage = Shrine::Storage::FileSystem.new("files", host: "943.23.43.1")
38
+ # storage.url("image.jpg") #=> "943.23.43.1/files/image.jpg"
39
+ #
40
+ # ## Clearing cache
41
+ #
42
+ # If you're using FileSystem as cache, you will probably want to
43
+ # periodically delete old files which aren't used anymore. You can put
44
+ # the following in a periodic Rake task:
45
+ #
46
+ # file_system = Shrine.storages[:cache]
47
+ # file_system.clear!(older_than: 1.week.ago) # adjust the time
48
+ #
49
+ # ## Permissions
50
+ #
51
+ # If you want your files and folders to have certain permissions, you can
52
+ # pass the `:permissions` option:
53
+ #
54
+ # Shrine::Storage::FileSystem.new("directory", permissions: 0755)
8
55
  class FileSystem
9
56
  attr_reader :directory, :subdirectory, :host, :permissions
10
57
 
11
- # The `directory` is the root directory where uploaded files will be
12
- # stored. In web applications this is typically the "public/" directory,
13
- # to make the files available via URL.
58
+ # Initializes a storage for uploading to the filesystem.
14
59
  #
15
- # If `:subdirectory` is given, #url will return a URL relative to
16
- # `directory` (and include `:subdirectory`). So, `FileSystem.new('public',
17
- # subdirectory: 'uploads')` will upload files to "public/uploads", and
18
- # URLs will be "/uploads/*".
60
+ # :subdirectory
61
+ # : The directory relative to `directory` to which files will be stored,
62
+ # and it is included in the URL.
19
63
  #
20
- # In applications it's common to serve files over CDN, so an additional
21
- # `:host` option can be provided. This option can also be used without
22
- # `:subdirectory`, if for example files are located on another server
23
- # which requires an IP address.
64
+ # :host
65
+ # : URLs will by default be relative if `:subdirectory` is set, and you
66
+ # can use this option to set a CDN host (e.g. `//abc123.cloudfront.net`).
24
67
  #
25
- # By default FileSystem will clean empty directories when files get
26
- # deleted. However, if this puts too much load on the filesystem, it can
27
- # be disabled with `clean: false`.
68
+ # :permissions
69
+ # : The generated files and folders will have default UNIX permissions,
70
+ # but if you want specific ones you can use this option (e.g. `0755`).
28
71
  #
29
- # Optional folder and file permissions can be set through the
30
- # `:permissions` option.
72
+ # :clean
73
+ # : By default empty folders inside the directory are automatically
74
+ # deleted, but if it happens that it causes too much load on the
75
+ # filesystem, you can set this option to `false`.
31
76
  def initialize(directory, subdirectory: nil, host: nil, clean: true, permissions: nil)
32
77
  if subdirectory
33
78
  @subdirectory = Pathname(relative(subdirectory))
@@ -131,7 +176,7 @@ class Shrine
131
176
 
132
177
  # Returns the full path to the file.
133
178
  def path(id)
134
- directory.join(relative(id))
179
+ directory.join(id.gsub("/", File::SEPARATOR))
135
180
  end
136
181
 
137
182
  # Cleans all empty subdirectories up the hierarchy.
@@ -47,9 +47,7 @@ class Shrine
47
47
  end
48
48
  end
49
49
 
50
- begin
51
- Shrine.io!(storage.open("foo.jpg"))
52
- rescue Shrine::InvalidFile => error
50
+ if !io?(storage.open("foo.jpg"))
53
51
  error! "#open doesn't return a valid IO object"
54
52
  end
55
53
 
@@ -75,6 +73,13 @@ class Shrine
75
73
 
76
74
  private
77
75
 
76
+ def io?(object)
77
+ missing_methods = IO_METHODS.reject do |m, a|
78
+ object.respond_to?(m) && [a.count, -1].include?(object.method(m).arity)
79
+ end
80
+ missing_methods.empty?
81
+ end
82
+
78
83
  def error!(message)
79
84
  @errors << message
80
85
  end
@@ -1,27 +1,64 @@
1
1
  require "aws-sdk"
2
2
  require "down"
3
+ require "uri"
3
4
 
4
5
  class Shrine
5
6
  module Storage
7
+ # The S3 storage handles uploads to Amazon S3 service, and it is
8
+ # initialized with the following 4 required options:
9
+ #
10
+ # Shrine::Storage::S3.new(
11
+ # access_key_id: "xyz",
12
+ # secret_access_key: "abc",
13
+ # region: "eu-west-1",
14
+ # bucket: "my-app",
15
+ # )
16
+ #
17
+ # ## Prefix
18
+ #
19
+ # The `:prefix` option can be specified for uploading all files inside
20
+ # a specific S3 prefix (folder), which is useful when using S3 for both
21
+ # cache and store:
22
+ #
23
+ # Shrine::Storage::S3.new(prefix: "cache", **s3_options)
24
+ # Shrine::Storage::S3.new(prefix: "store", **s3_options)
25
+ #
26
+ # ## CDN
27
+ #
28
+ # If you're using a CDN with S3 like Amazon CloudFront, you can specify
29
+ # the `:host` option to have all your URLs use the CDN host:
30
+ #
31
+ # Shrine::Storage::S3.new(host: "//abc123.cloudfront.net", **s3_options)
32
+ #
33
+ # ## Clearing cache
34
+ #
35
+ # If you're using S3 as a cache, you will probably want to periodically
36
+ # delete old files which aren't used anymore. S3 has a built-in way to do
37
+ # this, read [this article](http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html)
38
+ # for instructions.
6
39
  class S3
7
- attr_reader :prefix, :bucket, :s3
40
+ attr_reader :prefix, :bucket, :s3, :host
8
41
 
9
- # Example:
42
+ # Initializes a storage for uploading to S3.
10
43
  #
11
- # Shrine::Storage::S3.new(
12
- # access_key_id: "xyz",
13
- # secret_access_key: "abc",
14
- # region: "eu-west-1"
15
- # bucket: "my-app",
16
- # prefix: "cache",
17
- # )
44
+ # :access_key_id
45
+ # :secret_access_key
46
+ # :region
47
+ # :bucket
48
+ # : Credentials required by the `aws-sdk` gem.
18
49
  #
19
- # The above storage will store file into the "my-app" bucket in the
20
- # "cache" directory.
21
- def initialize(bucket:, prefix: nil, **s3_options)
50
+ # :prefix
51
+ # : "Folder" name inside the bucket to store files into.
52
+ #
53
+ # :host
54
+ # : This option is used for setting CDNs, e.g. it can be set to `//abc123.cloudfront.net`.
55
+ #
56
+ # All other options are forwarded to [`Aws::S3::Client#initialize`](http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html#initialize-instance_method).
57
+ def initialize(bucket:, prefix: nil, host: nil, **s3_options)
22
58
  @prefix = prefix
23
- @s3 = Aws::S3::Resource.new(s3_options)
59
+ @s3 = Aws::S3::Resource.new(**s3_options)
24
60
  @bucket = @s3.bucket(bucket)
61
+ @host = host
25
62
  end
26
63
 
27
64
  # If the file is an UploadedFile from S3, issues a COPY command, otherwise
@@ -68,20 +105,47 @@ class Shrine
68
105
  # This is called when multiple files are being deleted at once. Issues
69
106
  # a single MULTI DELETE command.
70
107
  def multi_delete(ids)
71
- bucket.delete_objects(delete: {objects: ids.map { |id| {key: id} }})
108
+ objects = ids.map { |id| {key: object(id).key} }
109
+ bucket.delete_objects(delete: {objects: objects})
72
110
  end
73
111
 
74
- # Returns the presigned URL to the file. If `download: true` is passed,
75
- # returns a forced download link.
76
- def url(id, download: nil, **options)
77
- options[:response_content_disposition] = "attachment" if download
78
- object(id).presigned_url(:get, **options)
112
+ # Returns the presigned URL to the file.
113
+ #
114
+ # :download
115
+ # : If set to `true`, creates a "forced download" link, which means that
116
+ # the browser will never display the file and always ask the user to
117
+ # download it.
118
+ #
119
+ # :public
120
+ # : Creates an unsigned version of the URL (requires setting appropriate
121
+ # permissions on the S3 bucket).
122
+ #
123
+ # options
124
+ # : All other optione are forwarded to
125
+ # If `download: true` is passed,
126
+ # returns a forced download link. If `public: true` is passed, it returns
127
+ # an unsigned S3 URL. All other options are forwarded to
128
+ # [`Aws::S3::Object#presigned_url`], so take a look there for the
129
+ # complete list of additional options.
130
+ #
131
+ # [`Aws::S3::Object#presigned_url`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_url-instance_method
132
+ def url(id, download: nil, public: nil, **options)
133
+ if host.nil?
134
+ options[:response_content_disposition] = "attachment" if download
135
+ if public.nil?
136
+ object(id).presigned_url(:get, **options)
137
+ else
138
+ object(id).public_url(**options)
139
+ end
140
+ else
141
+ URI.join(host, object(id).key).to_s
142
+ end
79
143
  end
80
144
 
81
145
  # Deletes all files from the storage (requires confirmation).
82
146
  def clear!(confirm = nil)
83
147
  raise Shrine::Confirm unless confirm == :confirm
84
- @bucket.clear!
148
+ @bucket.object_versions(prefix: prefix).delete
85
149
  end
86
150
 
87
151
  # Returns a signature for direct uploads. Internally it calls
@@ -4,8 +4,8 @@ class Shrine
4
4
  end
5
5
 
6
6
  module VERSION
7
- MAJOR = 0
8
- MINOR = 9
7
+ MAJOR = 1
8
+ MINOR = 0
9
9
  TINY = 0
10
10
  PRE = nil
11
11
 
@@ -16,8 +16,9 @@ Gem::Specification.new do |gem|
16
16
  gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "shrine.gemspec", "doc/*.md"]
17
17
  gem.require_path = "lib"
18
18
 
19
- gem.add_dependency "down", ">= 1.0.2"
19
+ gem.add_dependency "down", ">= 1.0.3"
20
20
 
21
+ gem.add_development_dependency "rake"
21
22
  gem.add_development_dependency "minitest", "~> 5.8"
22
23
  gem.add_development_dependency "minitest-hooks", "~> 1.3.0"
23
24
  gem.add_development_dependency "mocha"
@@ -27,20 +28,22 @@ Gem::Specification.new do |gem|
27
28
  gem.add_development_dependency "dotenv"
28
29
 
29
30
  gem.add_development_dependency "roda"
30
- gem.add_development_dependency "ruby-filemagic", "~> 0.7" unless RUBY_ENGINE == "jruby"
31
31
  gem.add_development_dependency "mimemagic"
32
32
  gem.add_development_dependency "mime-types"
33
33
  gem.add_development_dependency "fastimage"
34
34
  gem.add_development_dependency "thread", "~> 0.2"
35
- gem.add_development_dependency "aws-sdk", "~> 2.1"
36
- gem.add_development_dependency "image_processing", ">= 0.2.4"
37
- gem.add_development_dependency "mini_magick", ">= 4.3.5"
35
+ gem.add_development_dependency "aws-sdk", "~> 2.1.30"
36
+
37
+ unless RUBY_ENGINE == "jruby" || ENV["CI"]
38
+ gem.add_development_dependency "ruby-filemagic", "~> 0.7"
39
+ end
38
40
 
39
41
  gem.add_development_dependency "sequel"
40
42
  gem.add_development_dependency "activerecord"
41
- unless RUBY_ENGINE == "jruby"
42
- gem.add_development_dependency "sqlite3"
43
- else
43
+
44
+ if RUBY_ENGINE == "jruby"
44
45
  gem.add_development_dependency "activerecord-jdbcsqlite3-adapter"
46
+ else
47
+ gem.add_development_dependency "sqlite3"
45
48
  end
46
49
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shrine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-25 00:00:00.000000000 Z
11
+ date: 2015-11-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down
@@ -16,14 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.0.2
19
+ version: 1.0.3
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 1.0.2
26
+ version: 1.0.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: minitest
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -136,20 +150,6 @@ dependencies:
136
150
  - - ">="
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
- - !ruby/object:Gem::Dependency
140
- name: ruby-filemagic
141
- requirement: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - "~>"
144
- - !ruby/object:Gem::Version
145
- version: '0.7'
146
- type: :development
147
- prerelease: false
148
- version_requirements: !ruby/object:Gem::Requirement
149
- requirements:
150
- - - "~>"
151
- - !ruby/object:Gem::Version
152
- version: '0.7'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: mimemagic
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -212,42 +212,28 @@ dependencies:
212
212
  requirements:
213
213
  - - "~>"
214
214
  - !ruby/object:Gem::Version
215
- version: '2.1'
215
+ version: 2.1.30
216
216
  type: :development
217
217
  prerelease: false
218
218
  version_requirements: !ruby/object:Gem::Requirement
219
219
  requirements:
220
220
  - - "~>"
221
221
  - !ruby/object:Gem::Version
222
- version: '2.1'
223
- - !ruby/object:Gem::Dependency
224
- name: image_processing
225
- requirement: !ruby/object:Gem::Requirement
226
- requirements:
227
- - - ">="
228
- - !ruby/object:Gem::Version
229
- version: 0.2.4
230
- type: :development
231
- prerelease: false
232
- version_requirements: !ruby/object:Gem::Requirement
233
- requirements:
234
- - - ">="
235
- - !ruby/object:Gem::Version
236
- version: 0.2.4
222
+ version: 2.1.30
237
223
  - !ruby/object:Gem::Dependency
238
- name: mini_magick
224
+ name: ruby-filemagic
239
225
  requirement: !ruby/object:Gem::Requirement
240
226
  requirements:
241
- - - ">="
227
+ - - "~>"
242
228
  - !ruby/object:Gem::Version
243
- version: 4.3.5
229
+ version: '0.7'
244
230
  type: :development
245
231
  prerelease: false
246
232
  version_requirements: !ruby/object:Gem::Requirement
247
233
  requirements:
248
- - - ">="
234
+ - - "~>"
249
235
  - !ruby/object:Gem::Version
250
- version: 4.3.5
236
+ version: '0.7'
251
237
  - !ruby/object:Gem::Dependency
252
238
  name: sequel
253
239
  requirement: !ruby/object:Gem::Requirement
@@ -299,10 +285,12 @@ extra_rdoc_files: []
299
285
  files:
300
286
  - LICENSE.txt
301
287
  - README.md
288
+ - doc/carrierwave.md
302
289
  - doc/creating_plugins.md
303
290
  - doc/creating_storages.md
304
291
  - doc/direct_s3.md
305
292
  - doc/migrating_storage.md
293
+ - doc/paperclip.md
306
294
  - doc/regenerating_versions.md
307
295
  - lib/shrine.rb
308
296
  - lib/shrine/plugins/activerecord.rb
@@ -310,7 +298,7 @@ files:
310
298
  - lib/shrine/plugins/cached_attachment_data.rb
311
299
  - lib/shrine/plugins/data_uri.rb
312
300
  - lib/shrine/plugins/default_storage.rb
313
- - lib/shrine/plugins/delete_invalid.rb
301
+ - lib/shrine/plugins/default_url_options.rb
314
302
  - lib/shrine/plugins/determine_mime_type.rb
315
303
  - lib/shrine/plugins/direct_upload.rb
316
304
  - lib/shrine/plugins/dynamic_storage.rb
@@ -319,10 +307,13 @@ files:
319
307
  - lib/shrine/plugins/keep_files.rb
320
308
  - lib/shrine/plugins/logging.rb
321
309
  - lib/shrine/plugins/migration_helpers.rb
310
+ - lib/shrine/plugins/module_include.rb
322
311
  - lib/shrine/plugins/moving.rb
323
312
  - lib/shrine/plugins/multi_delete.rb
324
313
  - lib/shrine/plugins/parallelize.rb
314
+ - lib/shrine/plugins/parsed_json.rb
325
315
  - lib/shrine/plugins/pretty_location.rb
316
+ - lib/shrine/plugins/rack_file.rb
326
317
  - lib/shrine/plugins/recache.rb
327
318
  - lib/shrine/plugins/remote_url.rb
328
319
  - lib/shrine/plugins/remove_attachment.rb