shrine 3.1.0 → 3.4.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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +11 -4
  4. data/doc/advantages.md +4 -4
  5. data/doc/attacher.md +2 -2
  6. data/doc/carrierwave.md +24 -12
  7. data/doc/changing_derivatives.md +1 -1
  8. data/doc/changing_location.md +6 -5
  9. data/doc/design.md +134 -85
  10. data/doc/direct_s3.md +26 -0
  11. data/doc/external/articles.md +57 -45
  12. data/doc/external/extensions.md +41 -35
  13. data/doc/external/misc.md +23 -8
  14. data/doc/getting_started.md +156 -85
  15. data/doc/metadata.md +80 -44
  16. data/doc/multiple_files.md +1 -1
  17. data/doc/paperclip.md +28 -9
  18. data/doc/plugins/add_metadata.md +112 -35
  19. data/doc/plugins/atomic_helpers.md +41 -3
  20. data/doc/plugins/backgrounding.md +12 -2
  21. data/doc/plugins/column.md +36 -7
  22. data/doc/plugins/default_url.md +6 -3
  23. data/doc/plugins/derivatives.md +83 -44
  24. data/doc/plugins/download_endpoint.md +5 -5
  25. data/doc/plugins/dynamic_storage.md +1 -1
  26. data/doc/plugins/entity.md +12 -4
  27. data/doc/plugins/form_assign.md +5 -5
  28. data/doc/plugins/included.md +25 -5
  29. data/doc/plugins/infer_extension.md +9 -0
  30. data/doc/plugins/instrumentation.md +1 -1
  31. data/doc/plugins/metadata_attributes.md +1 -0
  32. data/doc/plugins/mirroring.md +1 -1
  33. data/doc/plugins/model.md +8 -3
  34. data/doc/plugins/persistence.md +10 -1
  35. data/doc/plugins/remote_url.md +6 -1
  36. data/doc/plugins/remove_invalid.md +9 -1
  37. data/doc/plugins/sequel.md +1 -1
  38. data/doc/plugins/store_dimensions.md +10 -0
  39. data/doc/plugins/type_predicates.md +96 -0
  40. data/doc/plugins/upload_endpoint.md +1 -1
  41. data/doc/plugins/upload_options.md +1 -1
  42. data/doc/plugins/url_options.md +4 -4
  43. data/doc/plugins/validation.md +14 -4
  44. data/doc/plugins/versions.md +7 -7
  45. data/doc/processing.md +287 -123
  46. data/doc/refile.md +9 -9
  47. data/doc/release_notes/2.8.0.md +1 -1
  48. data/doc/release_notes/3.0.0.md +1 -1
  49. data/doc/release_notes/3.2.0.md +96 -0
  50. data/doc/release_notes/3.2.1.md +31 -0
  51. data/doc/release_notes/3.2.2.md +14 -0
  52. data/doc/release_notes/3.3.0.md +105 -0
  53. data/doc/release_notes/3.4.0.md +35 -0
  54. data/doc/securing_uploads.md +2 -2
  55. data/doc/storage/memory.md +19 -0
  56. data/doc/storage/s3.md +104 -77
  57. data/doc/testing.md +12 -2
  58. data/doc/upgrading_to_3.md +99 -53
  59. data/lib/shrine.rb +9 -8
  60. data/lib/shrine/attacher.rb +20 -10
  61. data/lib/shrine/attachment.rb +2 -2
  62. data/lib/shrine/plugins.rb +22 -0
  63. data/lib/shrine/plugins/activerecord.rb +3 -3
  64. data/lib/shrine/plugins/add_metadata.rb +20 -5
  65. data/lib/shrine/plugins/backgrounding.rb +2 -2
  66. data/lib/shrine/plugins/default_url.rb +1 -1
  67. data/lib/shrine/plugins/derivation_endpoint.rb +13 -8
  68. data/lib/shrine/plugins/derivatives.rb +59 -30
  69. data/lib/shrine/plugins/determine_mime_type.rb +5 -3
  70. data/lib/shrine/plugins/entity.rb +12 -11
  71. data/lib/shrine/plugins/instrumentation.rb +12 -18
  72. data/lib/shrine/plugins/mirroring.rb +8 -8
  73. data/lib/shrine/plugins/model.rb +3 -3
  74. data/lib/shrine/plugins/presign_endpoint.rb +16 -4
  75. data/lib/shrine/plugins/pretty_location.rb +1 -1
  76. data/lib/shrine/plugins/processing.rb +1 -1
  77. data/lib/shrine/plugins/refresh_metadata.rb +2 -2
  78. data/lib/shrine/plugins/remote_url.rb +3 -3
  79. data/lib/shrine/plugins/remove_attachment.rb +5 -0
  80. data/lib/shrine/plugins/remove_invalid.rb +10 -5
  81. data/lib/shrine/plugins/sequel.rb +1 -1
  82. data/lib/shrine/plugins/store_dimensions.rb +4 -2
  83. data/lib/shrine/plugins/type_predicates.rb +113 -0
  84. data/lib/shrine/plugins/upload_endpoint.rb +10 -5
  85. data/lib/shrine/plugins/upload_options.rb +2 -2
  86. data/lib/shrine/plugins/url_options.rb +2 -2
  87. data/lib/shrine/plugins/validation.rb +9 -7
  88. data/lib/shrine/storage/linter.rb +4 -4
  89. data/lib/shrine/storage/memory.rb +5 -3
  90. data/lib/shrine/storage/s3.rb +117 -38
  91. data/lib/shrine/version.rb +1 -1
  92. data/shrine.gemspec +8 -8
  93. metadata +42 -34
data/doc/metadata.md CHANGED
@@ -15,6 +15,18 @@ uploaded_file.metadata #=>
15
15
  # }
16
16
  ```
17
17
 
18
+ Under the hood, `Shrine#upload` calls `Shrine#extract_metadata`, which you can
19
+ also use directly to extract metadata from any IO object:
20
+
21
+ ```rb
22
+ uploader.extract_metadata(io) #=>
23
+ # {
24
+ # "size" => 345993,
25
+ # "filename" => "matrix.mp4",
26
+ # "mime_type" => "video/mp4",
27
+ # }
28
+ ```
29
+
18
30
  The following metadata is extracted by default:
19
31
 
20
32
  | Key | Default source |
@@ -23,7 +35,9 @@ The following metadata is extracted by default:
23
35
  | `mime_type` | extracted from `io.content_type` |
24
36
  | `size` | extracted from `io.size` |
25
37
 
26
- You can access extracted metadata in three ways:
38
+ ## Accessing metadata
39
+
40
+ You can access the stored metadata in three ways:
27
41
 
28
42
  ```rb
29
43
  # via methods (if they're defined)
@@ -42,17 +56,7 @@ uploaded_file["filename"]
42
56
  uploaded_file["mime_type"]
43
57
  ```
44
58
 
45
- Under the hood, `Shrine#upload` calls `Shrine#extract_metadata`, which you can
46
- also use directly to extract metadata from any IO object:
47
-
48
- ```rb
49
- uploader.extract_metadata(io) #=>
50
- # {
51
- # "size" => 345993,
52
- # "filename" => "matrix.mp4",
53
- # "mime_type" => "video/mp4",
54
- # }
55
- ```
59
+ ## Controlling extraction
56
60
 
57
61
  `Shrine#upload` accepts a `:metadata` option which accepts the following values:
58
62
 
@@ -85,11 +89,11 @@ By default, the `mime_type` metadata will be copied over from the
85
89
  `#content_type` attribute of the input file (if present). However, since
86
90
  `#content_type` value comes from the `Content-Type` header of the upload
87
91
  request, it's *not guaranteed* to hold the actual MIME type of the file (browser
88
- determines this header based on file extension). Moreover, only
89
- `ActionDispatch::Http::UploadedFile`, `Shrine::RackFile`, and
90
- `Shrine::DataFile` objects have `#content_type` defined, so, when uploading
91
- simple file objects, `mime_type` will be nil. That makes relying on
92
- `#content_type` both a security risk and limiting.
92
+ determines this header based on file extension).
93
+
94
+ Moreover, only `ActionDispatch::Http::UploadedFile`, `Shrine::RackFile`, and
95
+ `Shrine::DataFile` objects have `#content_type` defined, so when uploading
96
+ objects such as `File`, the `mime_type` value will be nil by default.
93
97
 
94
98
  To remedy that, Shrine comes with a
95
99
  [`determine_mime_type`][determine_mime_type] plugin which is able to extract
@@ -112,15 +116,14 @@ You can choose different analyzers, and even mix-and-match them. See the
112
116
 
113
117
  ## Image Dimensions
114
118
 
115
- Shrine comes with a `store_dimensions` plugin for extracting image dimensions.
116
- It adds `width` and `height` metadata values, and also adds `#width`,
117
- `#height`, and `#dimensions` methods to the `Shrine::UploadedFile` object. By
118
- default, the plugin uses [FastImage] to analyze dimensions, but you can also
119
- have it use [MiniMagick] or [ruby-vips]:
119
+ Shrine comes with a [`store_dimensions`][store_dimensions] plugin for
120
+ extracting image dimensions. It adds `width` and `height` metadata values, and
121
+ also adds `#width`, `#height`, and `#dimensions` methods to the
122
+ `Shrine::UploadedFile` object.
120
123
 
121
124
  ```rb
122
125
  # Gemfile
123
- gem "fastimage"
126
+ gem "fastimage" # default analyzer
124
127
  ```
125
128
  ```rb
126
129
  Shrine.plugin :store_dimensions
@@ -136,12 +139,17 @@ uploaded_file.height #=> 900
136
139
  uploaded_file.dimensions #=> [1600, 900]
137
140
  ```
138
141
 
142
+ By default, the plugin uses [FastImage] to analyze dimensions, but you can also
143
+ have it use [MiniMagick] or [ruby-vips]. See the
144
+ [`store_dimensions`][store_dimensions] plugin docs for more details.
145
+
139
146
  ## Custom metadata
140
147
 
141
148
  In addition to the built-in metadata, Shrine allows you to extract and store
142
- any custom metadata, using the `add_metadata` plugin (which extends
143
- `Shrine#extract_metadata`). For example, you might want to extract EXIF data
144
- from images:
149
+ any custom metadata, using the [`add_metadata`][add_metadata] plugin (which
150
+ internally extends `Shrine#extract_metadata`).
151
+
152
+ For example, you might want to extract EXIF data from images:
145
153
 
146
154
  ```rb
147
155
  # Gemfile
@@ -167,7 +175,7 @@ uploaded_file.exif #=> {...}
167
175
  ```
168
176
 
169
177
  Or, if you're uploading videos, you might want to extract some video-specific
170
- meatadata:
178
+ metadata:
171
179
 
172
180
  ```rb
173
181
  # Gemfile
@@ -203,9 +211,20 @@ uploaded_file.metadata #=>
203
211
 
204
212
  The yielded `io` object will not always be an object that responds to `#path`.
205
213
  For example, with the `data_uri` plugin the `io` can be a `StringIO` wrapper,
206
- with `restore_cached_data` or `refresh_metadata` plugins the `io` might be a
207
- `Shrine::UploadedFile` object. So we're using `Shrine.with_file` to ensure we
208
- have a file object.
214
+ while with `restore_cached_data` or `refresh_metadata` plugins the `io` might
215
+ be a `Shrine::UploadedFile` object. So, we're using `Shrine.with_file` to
216
+ ensure we have a file object.
217
+
218
+ ### Adding metadata
219
+
220
+ If you wish to add metadata to an already attached file, you can do it as
221
+ follows:
222
+
223
+ ```rb
224
+ photo.image_attacher.add_metadata("foo" => "bar")
225
+ photo.image.metadata #=> { ..., "foo" => "bar" }
226
+ photo.save # persist changes
227
+ ```
209
228
 
210
229
  ## Metadata columns
211
230
 
@@ -225,15 +244,20 @@ photo.image_type #=> "image/jpeg"
225
244
  When attaching files that were uploaded directly to the cloud or a [tus
226
245
  server], Shrine won't automatically extract metadata from them, instead it will
227
246
  copy any existing metadata that was set on the client side. The reason why this
228
- is the default behaviour is because extracting the metadata would require (at
229
- least partially) retrieving file content from the storage, which could
230
- potentially be expensive depending on the storage and the type of metadata
231
- being extracted.
247
+ is the default behaviour is because metadata extraction requires (at least
248
+ partially) retrieving file content from the storage, which could potentially be
249
+ expensive depending on the storage and the type of metadata being extracted.
250
+
251
+ ```rb
252
+ # no additional metadata will be extracted in this assignment by default
253
+ photo.image = '{"id":"9e6581a4ea1.jpg","storage":"cache","metadata":{...}}'
254
+ ```
232
255
 
233
- There are two ways of extracting metadata from directly uploaded files. If you
234
- want metadata to be automatically extracted on assignment (which is useful if
235
- you want to validate the extracted metadata or have it immediately available
236
- for any other reason), you can load the `restore_cached_data` plugin:
256
+ ### Extracting on attachment
257
+
258
+ If you want metadata to be automatically extracted on assignment (which is
259
+ useful if you want to validate the extracted metadata or have it immediately
260
+ available for any other reason), you can load the `restore_cached_data` plugin:
237
261
 
238
262
  ```rb
239
263
  Shrine.plugin :restore_cached_data # automatically extract metadata from cached files on assignment
@@ -248,7 +272,9 @@ photo.image.metadata #=>
248
272
  # }
249
273
  ```
250
274
 
251
- ### Backgrounding
275
+ ### Extracting in the background
276
+
277
+ #### A) Extracting with promotion
252
278
 
253
279
  If you're using [backgrounding], you can extract metadata during background
254
280
  promotion using the `refresh_metadata` plugin (which the `restore_cached_data`
@@ -271,12 +297,14 @@ class PromoteJob
271
297
  record = Object.const_get(record_class).find(record_id) # if using Active Record
272
298
 
273
299
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
274
- attacher.refresh_metadata!
300
+ attacher.refresh_metadata! # extract metadata
275
301
  attacher.atomic_promote
276
302
  end
277
303
  end
278
304
  ```
279
305
 
306
+ #### B) Extracting separately from promotion
307
+
280
308
  You can also extract metadata in the background separately from promotion:
281
309
 
282
310
  ```rb
@@ -303,11 +331,13 @@ class MetadataJob
303
331
  end
304
332
  ```
305
333
 
334
+ ### Combining foreground and background
335
+
306
336
  If you have some metadata that you want to extract in the foreground and some
307
337
  that you want to extract in the background, you can use the uploader context:
308
338
 
309
339
  ```rb
310
- class MyUploader < Shrine
340
+ class VideoUploader < Shrine
311
341
  plugin :add_metadata
312
342
 
313
343
  add_metadata do |io, **options|
@@ -324,7 +354,7 @@ class MyUploader < Shrine
324
354
  end
325
355
  ```
326
356
  ```rb
327
- class MetadataJob
357
+ class PromoteJob
328
358
  include Sidekiq::Worker
329
359
 
330
360
  def perform(attacher_class, record_class, record_id, name, file_data)
@@ -332,12 +362,16 @@ class MetadataJob
332
362
  record = Object.const_get(record_class).find(record_id) # if using Active Record
333
363
 
334
364
  attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
335
- attacher.refresh_metadata!(background: true)
336
- attacher.atomic_persist
365
+ attacher.refresh_metadata!(background: true) # specify the flag
366
+ attacher.atomic_promote
337
367
  end
338
368
  end
339
369
  ```
340
370
 
371
+ Now triggering metadata extraction in the controller on attachment (using
372
+ `restore_cached_data` or `refresh_metadata` plugin) will skip the video
373
+ metadata block, which will be triggered later in the background job.
374
+
341
375
  ### Optimizations
342
376
 
343
377
  If you want to do both metadata extraction and file processing during
@@ -389,4 +423,6 @@ end
389
423
  [ruby-vips]: https://github.com/libvips/ruby-vips
390
424
  [tus server]: https://github.com/janko/tus-ruby-server
391
425
  [determine_mime_type]: https://shrinerb.com/docs/plugins/determine_mime_type
426
+ [store_dimensions]: https://shrinerb.com/docs/plugins/store_dimensions
427
+ [add_metadata]: https://shrinerb.com/docs/plugins/add_metadata
392
428
  [backgrounding]: https://shrinerb.com/docs/plugins/backgrounding
@@ -228,7 +228,7 @@ photos_attributes = album_params[:photos_attributes].to_h.merge(new_photos_attri
228
228
  album_attributes = album_params.merge(photos_attributes: photos_attributes)
229
229
 
230
230
  # create the album with photos
231
- Album.create(album_params)
231
+ Album.create(album_attributes)
232
232
  ```
233
233
 
234
234
  In this case you need to make sure your form tag has
data/doc/paperclip.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Shrine for Paperclip Users
2
+ title: Upgrading from Paperclip
3
3
  ---
4
4
 
5
5
  This guide is aimed at helping Paperclip users transition to Shrine, and it
@@ -136,7 +136,7 @@ into separate columns.
136
136
  While Paperclip works only with Active Record, Shrine is designed to integrate
137
137
  with any persistence library (there are integrations for [Active
138
138
  Record][activerecord], [Sequel][sequel], [ROM][rom], [Hanami][hanami] and
139
- [Hanami][mongoid]), and can also be used standalone:
139
+ [Mongoid][mongoid]), and can also be used standalone:
140
140
 
141
141
  ```rb
142
142
  attacher = ImageUploader::Attacher.new
@@ -358,13 +358,14 @@ module PaperclipShrineSynchronization
358
358
  end
359
359
  end
360
360
 
361
- # If you'll be using a `:prefix` on your Shrine storage, or you're storing
362
- # files on the filesystem, make sure to subtract the appropriate part
363
- # from the path assigned to `:id`.
364
361
  def shrine_attachment_file(attachment)
362
+ location = attachment.path
363
+ # if you're storing files on disk, make sure to subtract the absolute path
364
+ location = location.sub(%r{^#{storage.prefix}/}, "") if storage.prefix
365
+
365
366
  Shrine.uploaded_file(
366
367
  storage: :store,
367
- id: attachment.path,
368
+ id: location,
368
369
  metadata: {
369
370
  "size" => attachment.size,
370
371
  "filename" => attachment.original_filename,
@@ -377,12 +378,20 @@ module PaperclipShrineSynchronization
377
378
  # files on the filesystem, make sure to subtract the appropriate part
378
379
  # from the path assigned to `:id`.
379
380
  def shrine_style_file(style)
381
+ location = style.attachment.path(style.name)
382
+ # if you're storing files on disk, make sure to subtract the absolute path
383
+ location = location.sub(%r{^#{storage.prefix}/}, "") if storage.prefix
384
+
380
385
  Shrine.uploaded_file(
381
386
  storage: :store,
382
- id: style.attachment.path(style.name),
387
+ id: location,
383
388
  metadata: {},
384
389
  )
385
390
  end
391
+
392
+ def storage
393
+ Shrine.storages[:store]
394
+ end
386
395
  end
387
396
  ```
388
397
  ```rb
@@ -511,11 +520,20 @@ Alternatively, if you want to generate locations yourself you can override the
511
520
 
512
521
  ```rb
513
522
  class ImageUploader < Shrine
514
- def generate_location(io, **options)
515
- # ...
523
+ def generate_location(io, record: nil, name: nil, **)
524
+ [ storage_key,
525
+ record && record.class.name.underscore,
526
+ record && record.id,
527
+ super,
528
+ io.original_filename ].compact.join("/")
516
529
  end
517
530
  end
518
531
  ```
532
+ ```
533
+ cache/user/123/2feff8c724e7ce17/nature.jpg
534
+ store/user/456/7f99669fde1e01fc/kitten.jpg
535
+ ...
536
+ ```
519
537
 
520
538
  #### `:validate_media_type`
521
539
 
@@ -677,6 +695,7 @@ s3.upload(io, "object/destination/path")
677
695
  The Shrine storage has no replacement for the `:url` Paperclip option, and it
678
696
  isn't needed.
679
697
 
698
+ [metadata_attributes]: https://shrinerb.com/docs/plugins/metadata_attributes
680
699
  [Managing Derivatives]: https://shrinerb.com/docs/changing-derivatives
681
700
  [direct uploads]: https://shrinerb.com/docs/getting-started#direct-uploads
682
701
  [S3]: https://shrinerb.com/docs/storage/s3
@@ -2,76 +2,130 @@
2
2
  title: Add Metadata
3
3
  ---
4
4
 
5
- The [`add_metadata`][add_metadata] plugin provides a convenient method for
6
- extracting and adding custom metadata values.
5
+ The [`add_metadata`][add_metadata] plugin allows adding custom metadata to
6
+ uploaded files.
7
7
 
8
8
  ```rb
9
- plugin :add_metadata
9
+ Shrine.plugin :add_metadata
10
+ ```
11
+
12
+ ## Metadata block
13
+
14
+ The `Shrine.add_metadata` method allows you to register a block that will get
15
+ executed on upload, where you can return custom metadata:
16
+
17
+ ```rb
18
+ require "pdf-reader" # https://github.com/yob/pdf-reader
10
19
 
11
- add_metadata :exif do |io|
12
- begin
13
- Exif::Data.new(io).to_h
14
- rescue Exif::NotReadable # not a valid image
15
- {}
20
+ class PdfUploader < Shrine
21
+ add_metadata :page_count do |io|
22
+ reader = PDF::Reader.new(io)
23
+ reader.page_count
16
24
  end
17
25
  end
18
26
  ```
19
27
 
20
- The above will add "exif" to the metadata hash, and also create the `#exif`
21
- reader method on `Shrine::UploadedFile`.
28
+ The above will add `page_count` key to the metadata hash, and also create the
29
+ `#page_count` reader method on the `Shrine::UploadedFile`.
22
30
 
23
31
  ```rb
24
- image.metadata["exif"]
32
+ uploaded_file.metadata["page_count"] #=> 30
25
33
  # or
26
- image.exif
34
+ uploaded_file.page_count #=> 30
35
+ ```
36
+
37
+ ### Skipping nil values
38
+
39
+ By default, if your block returns `nil` then the `nil` value will be stored into
40
+ metadata. If you do not want to store anything when your block returns nil, you
41
+ can use the `skip_nil: true` option:
42
+
43
+ ```rb
44
+ class PdfUploader < Shrine
45
+ add_metadata :pages, skip_nil: true do |io|
46
+ if is_pdf?(io)
47
+ reader = PDF::Reader.new(io)
48
+ reader.page_count
49
+ else
50
+ # If this is not a PDF, then the pages metadata will not be stored
51
+ nil
52
+ end
53
+ end
54
+ end
27
55
  ```
28
56
 
29
- ## Multiple values
57
+ ### Multiple values
30
58
 
31
59
  You can also extract multiple metadata values at once, by using `add_metadata`
32
60
  without an argument and returning a hash of metadata.
33
61
 
34
62
  ```rb
35
- add_metadata do |io|
36
- begin
37
- data = Exif::Data.new(io)
38
- rescue Exif::NotReadable # not a valid image
39
- next {}
63
+ require "exif" # https://github.com/tonytonyjan/exif
64
+
65
+ class ImageUploader < Shrine
66
+ add_metadata do |io|
67
+ begin
68
+ data = Exif::Data.new(io)
69
+ rescue Exif::NotReadable # not a valid image
70
+ next {}
71
+ end
72
+
73
+ { "date_time" => data.date_time,
74
+ "flash" => data.flash,
75
+ "focal_length" => data.focal_length,
76
+ "exposure_time" => data.exposure_time }
40
77
  end
41
-
42
- { date_time: data.date_time,
43
- flash: data.flash,
44
- focal_length: data.focal_length,
45
- exposure_time: data.exposure_time }
46
78
  end
47
79
  ```
80
+ ```rb
81
+ uploaded_file.metadata #=>
82
+ # {
83
+ # ...
84
+ # "date_time" => "2019:07:20 16:16:08",
85
+ # "flash" => 16,
86
+ # "focal_length" => 26/1,
87
+ # "exposure_time" => 1/500,
88
+ # }
89
+ ```
48
90
 
49
91
  In this case Shrine won't automatically create reader methods for the extracted
50
- metadata on Shrine::UploadedFile, but you can create them via
51
- `#metadata_method`.
92
+ metadata, but you can create them via `Shrine.metadata_method`:
52
93
 
53
94
  ```rb
54
- metadata_method :date_time, :flash
95
+ class ImageUploader < Shrine
96
+ # ...
97
+ metadata_method :date_time, :flash
98
+ end
99
+ ```
100
+ ```rb
101
+ uploaded_file.date_time #=> "2019:07:20 16:16:08"
102
+ uploaded_file.flash #=> 16
55
103
  ```
56
104
 
57
- ## Ensuring file
105
+ ### Ensuring file
58
106
 
59
107
  The `io` might not always be a file object, so if you're using an analyzer
60
108
  which requires the source file to be on disk, you can use `Shrine.with_file` to
61
109
  ensure you have a file object.
62
110
 
63
111
  ```rb
64
- add_metadata do |io|
65
- movie = Shrine.with_file(io) { |file| FFMPEG::Movie.new(file.path) }
66
-
67
- { "duration" => movie.duration,
68
- "bitrate" => movie.bitrate,
69
- "resolution" => movie.resolution,
70
- "frame_rate" => movie.frame_rate }
112
+ require "streamio-ffmpeg" # https://github.com/streamio/streamio-ffmpeg
113
+
114
+ class VideoUploader < Shrine
115
+ add_metadata do |io|
116
+ movie = Shrine.with_file(io) do |file|
117
+ FFMPEG::Movie.new(file.path)
118
+ end
119
+
120
+ { "duration" => movie.duration,
121
+ "bitrate" => movie.bitrate,
122
+ "resolution" => movie.resolution,
123
+ "frame_rate" => movie.frame_rate }
124
+ end
71
125
  end
72
126
  ```
73
127
 
74
- ## Uploader options
128
+ ### Uploader options
75
129
 
76
130
  Uploader options are also yielded to the block, you can access them for more
77
131
  context:
@@ -89,6 +143,8 @@ add_metadata do |io, **options|
89
143
  end
90
144
  ```
91
145
 
146
+ #### Metadata
147
+
92
148
  The `:metadata` option holds metadata that was extracted so far:
93
149
 
94
150
  ```rb
@@ -116,4 +172,25 @@ add_metadata :bar do |io, metadata:, **|
116
172
  end
117
173
  ```
118
174
 
175
+ ## Updating metadata
176
+
177
+ If you just wish to add some custom metadata to existing uploads, you can do it
178
+ with `UploadedFile#add_metadata` (and write the changes back to the model):
179
+
180
+ ```rb
181
+ attacher.file.add_metadata("foo" => "bar")
182
+ attacher.write # write changes to the model attribute
183
+
184
+ attacher.file.metadata #=> { ..., "foo" => "bar" }
185
+ ```
186
+
187
+ You can also use the `Attacher#add_metadata` shorthand, which also takes care
188
+ of syncing the model:
189
+
190
+ ```rb
191
+ attacher.add_metadata("foo" => "bar")
192
+
193
+ attacher.file.metadata #=> { ..., "foo" => "bar" }
194
+ ```
195
+
119
196
  [add_metadata]: https://github.com/shrinerb/shrine/blob/master/lib/shrine/plugins/add_metadata.rb