shrine 2.12.0 → 2.13.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.

@@ -14,8 +14,8 @@ uploaded_file.metadata #=>
14
14
  # }
15
15
  ```
16
16
 
17
- You can also use `Shrine#extract_metadata` directly to extract metadata from
18
- any IO object.
17
+ Under the hood `Shrine#extract_metadata` is called, which you can also use
18
+ directly to extract metadata from any IO object.
19
19
 
20
20
  ```rb
21
21
  uploader.extract_metadata(io) #=>
@@ -26,6 +26,15 @@ uploader.extract_metadata(io) #=>
26
26
  # }
27
27
  ```
28
28
 
29
+ Note that you can also manually add or override metadata on upload by passing
30
+ the `:metadata` option to `Shrine#upload`:
31
+
32
+ ```rb
33
+ uploaded_file = uploader.upload(file, metadata: { "filename" => "Matrix[1999].mp4", "foo" => "bar" })
34
+ uploaded_file.original_filename #=> "Matrix[1999].mp4"
35
+ uploaded_file.metadata["foo"] #=> "bar"
36
+ ```
37
+
29
38
  ## MIME type
30
39
 
31
40
  By default, the `mime_type` metadata will be copied over from the
@@ -108,7 +117,7 @@ uploaded_file.metadata["exif"] #=> {...}
108
117
  uploaded_file.exif #=> {...}
109
118
  ```
110
119
 
111
- Of, if you're uploading videos, you might want to extract some video-specific
120
+ Or, if you're uploading videos, you might want to extract some video-specific
112
121
  meatadata:
113
122
 
114
123
  ```rb
@@ -162,17 +171,31 @@ photo = Photo.new(image: file)
162
171
  photo.image_type #=> "image/jpeg"
163
172
  ```
164
173
 
165
- ## Refreshing metadata
166
-
167
- When uploading directly to the cloud, the metadata of the original file by
168
- default won't get extracted on the server side, because your application never
169
- received the file content.
170
-
171
- To have Shrine extra metadata when a cached file is assigned to the attachment
172
- attribute, it's recommended to load the `restore_cached_data` plugin.
174
+ ## Direct uploads
175
+
176
+ When attaching files that were uploaded directly to the cloud or a [tus
177
+ server], Shrine won't automatically extract metadata from them, instead it will
178
+ copy any existing metadata that was set on the client side. The reason why this
179
+ is the default behaviour is because extracting the metadata would require (at
180
+ least partially) retrieving file content from the storage, which could
181
+ potentially be expensive depending on the storage and the type of metadata
182
+ being extracted.
183
+
184
+ If you're just attaching files uploaded directly to local disk, there wouldn't
185
+ be any additional performance penalty for extracting metadata. However, if
186
+ you're attaching files uploaded directly to a cloud service like S3, retrieving
187
+ file content for metadata extraction would require an HTTP download. If you're
188
+ using just the `determine_mime_type` plugin, only a small portion of the file
189
+ will be downloaded, so the performance impact might not be so big. But in other
190
+ cases you might have to download the whole file.
191
+
192
+ There are two ways of extracting metadata from directly uploaded files. If you
193
+ want metadata to be automatically extracted on assignment (which is useful if
194
+ you want to validate the extracted metadata or have it immediately available),
195
+ you can load the `restore_cached_data` plugin:
173
196
 
174
197
  ```rb
175
- Shrine.plugin :restore_cached_data # extract metadata from cached files on assingment
198
+ Shrine.plugin :restore_cached_data # automatically extract metadata from cached files on assingment
176
199
  ```
177
200
  ```rb
178
201
  photo.image = '{"id":"ks9elsd.jpg","storage":"cache","metadata":{}}' # metadata is extracted
@@ -184,25 +207,56 @@ photo.image.metadata #=>
184
207
  # }
185
208
  ```
186
209
 
187
- Extracting metadata from a cached file requires retrieving file content from
188
- the storage, which might not be desirable depending on your case, that's why
189
- `restore_cached_data` plugin is not loaded by default. However, Shrine will not
190
- download the whole file from the storage, instead, it will open a connection to
191
- the storage, and the metadata analyzers will download how much of the file they
192
- need. Most MIME type analyzers and the FastImage dimensions analyzer need only
193
- the first few kilobytes.
194
-
195
- You can also extract metadata from an uploaded file explicitly using the
196
- `refresh_metadata` plugin (which the `restore_cached_data` plugin uses
197
- internally).
210
+ On the other hand, if you're using backgrounding, you can extract metadata
211
+ during background promotion using the `refresh_metadata` plugin (which the
212
+ `restore_cached_data` plugin uses internally):
198
213
 
199
214
  ```rb
200
215
  Shrine.plugin :refresh_metadata
201
216
  ```
202
217
  ```rb
203
- uploaded_file.metadata #=> {}
204
- uploaded_file.refresh_metadata!
205
- uploaded_file.metadata #=> {"filename"=>"nature.jpg","size"=>532894,"mime_type"=>"image/jpeg"}
218
+ class MyUploader < Shrine
219
+ plugin :processing
220
+
221
+ # this will be called in the background if using backgrounding plugin
222
+ process(:store) do |io, context|
223
+ io.refresh_metadata! # extracts metadata and updates `io.metadata`
224
+ io
225
+ end
226
+ end
227
+ ```
228
+
229
+ If you have metadata that is cheap to extract in the foreground, but also have
230
+ additional metadata that can be extracted asynchronously, you can combine the
231
+ two approaches. For example, if you're attaching video files, you might want to
232
+ extract MIME type upfront and video-specific metadata in a background job, which
233
+ can be done as follows (provided that `backgrounding` plugin is used):
234
+
235
+ ```rb
236
+ Shrine.plugin :restore_cached_data
237
+ ```
238
+ ```rb
239
+ class MyUploader < Shrine
240
+ plugin :determine_mime_type # this will be called in the foreground
241
+ plugin :processing
242
+
243
+ # this will be called in the background if using backgrounding plugin
244
+ process(:store) do |io, context|
245
+ additional_metadata = io.download do |file|
246
+ # example of metadata extraction
247
+ movie = FFMPEG::Movie.new(file.path) # uses the streamio-ffmpeg gem
248
+
249
+ { "duration" => movie.duration,
250
+ "bitrate" => movie.bitrate,
251
+ "resolution" => movie.resolution,
252
+ "frame_rate" => movie.frame_rate }
253
+ end
254
+
255
+ io.metadata.merge!(additional_metadata)
256
+
257
+ io
258
+ end
259
+ end
206
260
  ```
207
261
 
208
262
  [`file`]: http://linux.die.net/man/1/file
@@ -210,4 +264,5 @@ uploaded_file.metadata #=> {"filename"=>"nature.jpg","size"=>532894,"mime_type"=
210
264
  [Marcel]: https://github.com/basecamp/marcel
211
265
  [FastImage]: https://github.com/sdsykes/fastimage
212
266
  [MiniMagick]: https://github.com/minimagick/minimagick
213
- [ruby-vips]: https://github.com/jcupitt/ruby-vips
267
+ [ruby-vips]: https://github.com/libvips/ruby-vips
268
+ [tus server]: https://github.com/janko-m/tus-ruby-server
@@ -65,7 +65,7 @@ Let's create a table for the main resource and attachments, and add a foreign
65
65
  key in the attachment table for the main table:
66
66
 
67
67
  ```rb
68
- # Sequel
68
+ # with Sequel:
69
69
  Sequel.migration do
70
70
  change do
71
71
  create_table :albums do
@@ -81,7 +81,7 @@ Sequel.migration do
81
81
  end
82
82
  end
83
83
 
84
- # Active Record
84
+ # with Active Record:
85
85
  class CreateAlbumsAndPhotos < ActiveRecord::Migration
86
86
  def change
87
87
  create_table :albums do |t|
@@ -102,12 +102,12 @@ In the Photo model, create a Shrine attachment attribute named `image`
102
102
  (`:image` matches the `_data` column prefix above):
103
103
 
104
104
  ```rb
105
- # Sequel
105
+ # with Sequel:
106
106
  class Photo < Sequel::Model
107
107
  include ImageUploader::Attachment.new(:image)
108
108
  end
109
109
 
110
- # Active Record
110
+ # with Active Record:
111
111
  class Photo < ActiveRecord::Base
112
112
  include ImageUploader::Attachment.new(:image)
113
113
  end
@@ -121,7 +121,7 @@ relationship to the photos table, and allow it to directly accept attributes
121
121
  for the associated photo records by enabling nested attributes:
122
122
 
123
123
  ```rb
124
- # Sequel
124
+ # with Sequel:
125
125
  class Album < Sequel::Model
126
126
  one_to_many :photos
127
127
  plugin :association_dependencies, photos: :destroy # destroy photos when album is destroyed
@@ -130,7 +130,7 @@ class Album < Sequel::Model
130
130
  nested_attributes :photos, destroy: true
131
131
  end
132
132
 
133
- # Active Record
133
+ # with Active Record:
134
134
  class Album < ActiveRecord::Base
135
135
  has_many :photos, dependent: :destroy
136
136
  accepts_nested_attributes_for :photos, allow_destroy: true
@@ -141,6 +141,7 @@ Documentation on nested attributes:
141
141
 
142
142
  * [`Sequel::Model.nested_attributes`]
143
143
  * [`ActiveRecord::Base.accepts_nested_attributes_for`]
144
+ * [`Mongoid::Document.accepts_nested_attributes_for`]
144
145
 
145
146
  ### 3. Create the View
146
147
 
@@ -165,7 +166,7 @@ form @album, action: "/photos", enctype: "multipart/form-data" do |f|
165
166
  end
166
167
 
167
168
  # with Rails form builder:
168
- form_for @album do |f|
169
+ form_for @album, html: { enctype: "multipart/form-data" } do |f|
169
170
  f.text_field :title
170
171
  f.fields_for :photos do |p| # adds new `album[photos_attributes]` parameter
171
172
  p.hidden_field :image, value: p.object.cached_image_data
@@ -179,37 +180,69 @@ end
179
180
 
180
181
  In your controller you should still be able to assign all the attributes to the
181
182
  album, just remember to whitelist the new parameter for the nested attributes,
182
- in this case `photos_attributes`.
183
+ in this case `album[photos_attributes]`.
183
184
 
184
- ### 4. Direct Upload
185
+ ### 4a. Form upload
185
186
 
186
- On the client side, you can asynchronously upload each of the files to a direct
187
- upload endpoint as soon as they're selected. There are two methods of
188
- implementing direct uploads: upload to your app using the [`upload_endpoint`]
189
- plugin or upload to directly to storage like S3 using the [`presign_endpoint`]
190
- plugin. It's recommended to use [Uppy] to handle the uploads on the client side.
187
+ If you would like to avoid writing JavaScript, you can have selected files
188
+ uploaded via the form, and then in your controller you can assign them in the
189
+ format of nested attributes. You'll want to merge these newly uploaded photos
190
+ with the existing photos params that will come from the nested form fields.
191
+ This could look something like this:
192
+
193
+ ```rb
194
+ # In your controller:
195
+
196
+ # transform the list of uploaded files into a photos attributes hash
197
+ new_photos_attributes = params[:files].inject({}) do |hash, file|
198
+ hash.merge!(SecureRandom.hex => { image: file })
199
+ end
200
+
201
+ # merge new photos attributes with existing (`album_params` is whitelisted `params[:album]`)
202
+ photos_attributes = album_params[:photos_attributes].to_h.merge(new_photos_attributes)
203
+ album_attributes = album_params.merge(photos_attributes: photos_attributes)
204
+
205
+ # create the album with photos
206
+ Album.create(album_params)
207
+ ```
208
+
209
+ In this case you need to make sure your form tag has
210
+ `enctype="multipart/form-data"` attributes specified, otherwise form upload
211
+ won't succeed.
212
+
213
+ ### 4b. Direct upload
214
+
215
+ Alternatively, you can have selected files immediately uploaded to a direct
216
+ upload endpoint, as soon as they're selected. It's recommended to use [Uppy]
217
+ for handling client side file uploads. There are two methods of implementing
218
+ direct uploads: to your app using the [`upload_endpoint`] plugin, or directly
219
+ to a storage like S3 using the [`presign_endpoint`] plugin. See the
220
+ walkthroughs for setting up simple [direct app uploads] and [direct S3
221
+ uploads], as well as the [Roda][roda demo] or [Rails][rails demo] demo app
222
+ which implement multiple file uploads.
191
223
 
192
224
  Once files are uploaded asynchronously, you can dynamically insert photo
193
- attachment fields for the `image` attribute to the form filled with uploaded
194
- file data, so that the corresponding photo records are automatically created
195
- after form submission. The hidden attachment fields should contain uploaded
196
- file data in JSON format, just like when doing single direct uploads. The
197
- attachment field names should be namespaced according to the convention that
198
- the nested attributes feature expects. In this case it should be
199
- `album[photos_attributes][<idx>]`, where `<idx>` is any incrementing integer
200
- (e.g. you can use the current UNIX timestamp).
225
+ attachment fields for the `image` attachment attribute into the form, where the
226
+ hidden field is filled with uploaded file data in JSON format, just like when
227
+ doing single direct uploads. The attachment field names should be namespaced
228
+ according to the convention that the nested attributes feature expects. In this
229
+ case it should be `album[photos_attributes][<uid>]`, where `<uid>` is any
230
+ unique string.
201
231
 
202
232
  ```rb
203
233
  # naming format in which photos fields should be generated and submitted
204
234
  album[photos_attributes][11111][image] = '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
205
235
  album[photos_attributes][29323][image] = '{"id":"sg0fg.jpg","storage":"cache","metadata":{...}}'
206
236
  album[photos_attributes][34820][image] = '{"id":"041jd.jpg","storage":"cache","metadata":{...}}'
207
- # ...
208
237
  ```
209
238
 
210
- See the walkthroughs for setting up simple [direct app uploads] and
211
- [direct S3 uploads], and the [Roda][roda demo] or [Rails][rails demo] demo app
212
- which implements multiple file uploads.
239
+ In your controller you don't need to make any changes to handle the nested
240
+ photos attributes (other than allowing them), your ORM will take care of
241
+ creating, updating, or removing the associated photos.
242
+
243
+ ```rb
244
+ Album.create(album_params) # or `params[:album]`
245
+ ```
213
246
 
214
247
  ### 5. Adding Validations
215
248
 
@@ -229,28 +262,38 @@ class ImageUploader < Shrine
229
262
  end
230
263
  ```
231
264
  ```rb
232
- # Sequel
265
+ # with Sequel:
233
266
  class Album < Sequel::Model
234
267
  # ... (nested_attributes already enables validating associated photos) ...
235
268
  end
236
269
 
237
- # ActiveRecord
270
+ # with ActiveRecord:
238
271
  class Album < ActiveRecord::Base
239
272
  # ...
240
273
  validates_associated :photos
241
274
  end
242
275
  ```
243
276
 
244
- ### 6. Conclusion
277
+ Note that by default only metadata set on the client side will be available for
278
+ validations. Shrine will not automatically run metadata extraction for directly
279
+ uploaded files, as this would require (at least partially) downloading each
280
+ file from storage, which could be an unwanted performance penalty. If you want
281
+ metadata to be extracted on the server side, you can load the
282
+ `restore_cached_data` plugin which will make metadata extraction run on
283
+ assignment, or you can extract the metadata manually (e.g. in a background job)
284
+ using the `refresh_metadata` plugin.
285
+
286
+ ### Conclusion
245
287
 
246
288
  Now we have a simple interface for accepting multiple attachments, which
247
- internally uses nested attributes to create multiple associated records, each
248
- with a single attachment. After creation you can also add new attachments, or
249
- update and delete existing ones, which the nested attributes feature gives you
250
- for free.
289
+ internally uses the nested attributes feature of your ORM to create multiple
290
+ associated records, each with a single attachment. After creation you can also
291
+ add new attachments, or update and delete existing ones, which the nested
292
+ attributes feature gives you for free.
251
293
 
252
294
  [`Sequel::Model.nested_attributes`]: http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/NestedAttributes.html
253
295
  [`ActiveRecord::Base.accepts_nested_attributes_for`]: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
296
+ [`Mongoid::Document.accepts_nested_attributes_for`]: https://docs.mongodb.com/mongoid/master/tutorials/mongoid-nested-attributes/
254
297
  [`upload_endpoint`]: https://shrinerb.com/rdoc/classes/Shrine/Plugins/UploadEndpoint.html
255
298
  [`presign_endpoint`]: https://shrinerb.com/rdoc/classes/Shrine/Plugins/PresignEndpoint.html
256
299
  [Uppy]: https://uppy.io
@@ -253,15 +253,6 @@ generic gem.
253
253
  To demonstrate, here is an example of transcoding videos using
254
254
  [streamio-ffmpeg]:
255
255
 
256
- ```sh
257
- $ brew install ffmpeg
258
- ```
259
-
260
- ```rb
261
- # Gemfile
262
- gem "streamio-ffmpeg"
263
- ```
264
-
265
256
  ```rb
266
257
  require "streamio-ffmpeg"
267
258
 
@@ -404,8 +395,8 @@ photo.image.url(width: 100, height: 100, crop: :fit)
404
395
  [`ImageProcessing::MiniMagick`]: https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md
405
396
  [ImageMagick]: https://www.imagemagick.org
406
397
  [GraphicsMagick]: http://www.graphicsmagick.org
407
- [libvips]: http://jcupitt.github.io/libvips/
408
- [Why is libvips quick]: https://github.com/jcupitt/libvips/wiki/Why-is-libvips-quick
398
+ [libvips]: http://libvips.github.io/libvips/
399
+ [Why is libvips quick]: https://github.com/libvips/libvips/wiki/Why-is-libvips-quick
409
400
  [ImageOptim.com]: https://imageoptim.com/api
410
401
  [Down]: https://github.com/janko-m/down
411
402
  [HTTP.rb]: https://github.com/httprb/http
@@ -54,8 +54,8 @@ Shrine.storages = {
54
54
  }
55
55
  ```
56
56
 
57
- If you're using AWS S3 storage, in you can switch to using [Minio] (explained
58
- below), both in test and development environment. Alternatively, you can [stub
57
+ If you're using AWS S3 storage, you can use [Minio] (explained below) instead
58
+ of S3, both in test and development environment. Alternatively, you can [stub
59
59
  aws-sdk-s3 requests][aws-sdk-ruby stubs] in tests.
60
60
 
61
61
  ### Minio
@@ -13,7 +13,8 @@ class Shrine
13
13
  # Raised when a file is not a valid IO.
14
14
  class InvalidFile < Error
15
15
  def initialize(io, missing_methods)
16
- super "#{io.inspect} is not a valid IO object (it doesn't respond to #{missing_methods.map{|m, args|"##{m}"}.join(", ")})"
16
+ super "#{io.inspect} is not a valid IO object (it doesn't respond to \
17
+ #{missing_methods.map{|m, _|"##{m}"}.join(", ")})"
17
18
  end
18
19
  end
19
20
 
@@ -218,7 +219,7 @@ class Shrine
218
219
 
219
220
  # The main method for uploading files. Takes an IO-like object and an
220
221
  # optional context hash (used internally by Shrine::Attacher). It calls
221
- # user-defined #process, and aferwards it calls #store. The `io` is
222
+ # user-defined #process, and afterwards it calls #store. The `io` is
222
223
  # closed after upload.
223
224
  def upload(io, context = {})
224
225
  io = processed(io, context) || io
@@ -311,7 +312,10 @@ class Shrine
311
312
  # file that was uploaded.
312
313
  def _store(io, context)
313
314
  _enforce_io(io)
315
+
314
316
  metadata = get_metadata(io, context)
317
+ metadata = metadata.merge(context[:metadata]) if context[:metadata]
318
+
315
319
  location = get_location(io, context.merge(metadata: metadata))
316
320
 
317
321
  put(io, context.merge(location: location, metadata: metadata))
@@ -527,21 +531,22 @@ class Shrine
527
531
  # already cached file as a JSON string, otherwise it assumes that it's
528
532
  # an IO object and uploads it to the temporary storage. The cached file
529
533
  # is then written to the attachment attribute in the JSON format.
530
- def assign(value)
534
+ def assign(value, **options)
531
535
  if value.is_a?(String)
532
536
  return if value == "" || !cache.uploaded?(uploaded_file(value))
533
537
  assign_cached(uploaded_file(value))
534
538
  else
535
- uploaded_file = cache!(value, action: :cache) if value
539
+ uploaded_file = cache!(value, action: :cache, **options) if value
536
540
  set(uploaded_file)
537
541
  end
538
542
  end
539
543
 
540
- # Accepts a Shrine::UploadedFile object and writes is to the attachment
544
+ # Accepts a Shrine::UploadedFile object and writes it to the attachment
541
545
  # attribute. It then runs file validations, and records that the
542
546
  # attachment has changed.
543
547
  def set(uploaded_file)
544
- @old = get unless uploaded_file == get
548
+ file = get
549
+ @old = file unless uploaded_file == file
545
550
  _set(uploaded_file)
546
551
  validate
547
552
  end
@@ -601,7 +606,8 @@ class Shrine
601
606
  # Deletes the current attachment, typically called after destroying the
602
607
  # record.
603
608
  def destroy
604
- _delete(get, action: :destroy) if get && !cache.uploaded?(get)
609
+ file = get
610
+ _delete(file, action: :destroy) if file && !cache.uploaded?(file)
605
611
  end
606
612
 
607
613
  # Delegates to #delete!, overriden for backgrounding.
@@ -617,12 +623,14 @@ class Shrine
617
623
 
618
624
  # Returns true if attachment is present and cached.
619
625
  def cached?
620
- get && cache.uploaded?(get)
626
+ file = get
627
+ file && cache.uploaded?(file)
621
628
  end
622
629
 
623
630
  # Returns true if attachment is present and stored.
624
631
  def stored?
625
- get && store.uploaded?(get)
632
+ file = get
633
+ file && store.uploaded?(file)
626
634
  end
627
635
 
628
636
  # Returns a Shrine::UploadedFile instantiated from the data written to
@@ -681,7 +689,7 @@ class Shrine
681
689
  set(cached_file)
682
690
  end
683
691
 
684
- # Writes the uploaded file the attachment attribute. Overriden in ORM
692
+ # Writes the uploaded file to the attachment attribute. Overriden in ORM
685
693
  # plugins to additionally save the model instance.
686
694
  def update(uploaded_file)
687
695
  _set(uploaded_file)