shrine 2.10.1 → 2.11.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.

@@ -206,4 +206,4 @@ automatically:
206
206
  * deletes the uploaded file if attachment was replaced/removed or the record
207
207
  destroyed
208
208
 
209
- [Using Attacher]: http://shrinerb.com/rdoc/files/doc/attacher_md.html
209
+ [Using Attacher]: https://shrinerb.com/rdoc/files/doc/attacher_md.html
@@ -23,11 +23,12 @@ storage service is beneficial for several reasons:
23
23
  request-response lifecycle might not be able to finish before the request
24
24
  times out.
25
25
 
26
- You can start by setting both temporary and permanent storage to S3 with
27
- different prefixes (or even different buckets):
26
+ To start, let's set both temporary and permanent storage to S3, with the
27
+ temporary storage uploading to the `cache/` directory:
28
28
 
29
29
  ```rb
30
30
  # Gemfile
31
+ gem "shrine", "~> 2.11"
31
32
  gem "aws-sdk-s3", "~> 1.2"
32
33
  ```
33
34
  ```rb
@@ -42,7 +43,7 @@ s3_options = {
42
43
 
43
44
  Shrine.storages = {
44
45
  cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
45
- store: Shrine::Storage::S3.new(prefix: "store", **s3_options),
46
+ store: Shrine::Storage::S3.new(**s3_options),
46
47
  }
47
48
  ```
48
49
 
@@ -69,7 +70,7 @@ client.put_bucket_cors(
69
70
  cors_configuration: {
70
71
  cors_rules: [{
71
72
  allowed_headers: ["Authorization", "Content-Type", "Origin"],
72
- allowed_methods: ["GET", "POST"],
73
+ allowed_methods: ["GET", "POST", "PUT"],
73
74
  allowed_origins: ["*"],
74
75
  max_age_seconds: 3000,
75
76
  }]
@@ -80,27 +81,6 @@ client.put_bucket_cors(
80
81
  Note that due to DNS propagation it may take some time for the CORS update to
81
82
  be applied.
82
83
 
83
- ## File hash
84
-
85
- After direct S3 uploads we'll need to manually construct Shrine's JSON
86
- representation of an uploaded file:
87
-
88
- ```rb
89
- {
90
- "id": "349234854924394", # requied
91
- "storage": "cache", # required
92
- "metadata": {
93
- "size": 45461, # optional, but recommended
94
- "filename": "foo.jpg", # optional
95
- "mime_type": "image/jpeg" # optional
96
- }
97
- }
98
- ```
99
-
100
- * `id` – location of the file on S3 (minus the `:prefix`)
101
- * `storage` – direct uploads typically use the `:cache` storage
102
- * `metadata` – hash of metadata extracted from the file
103
-
104
84
  ## Strategy A (dynamic)
105
85
 
106
86
  * Best user experience
@@ -113,7 +93,7 @@ upload the file to S3. The `presign_endpoint` plugin gives us this presign
113
93
  route, so we just need to mount it in our application:
114
94
 
115
95
  ```rb
116
- Shrine.plugin :presign_endpoint
96
+ Shrine.plugin :presign_endpoint, presign_options: { method: :put }
117
97
  ```
118
98
  ```rb
119
99
  # config.ru (Rack)
@@ -129,37 +109,31 @@ Rails.application.routes.draw do
129
109
  end
130
110
  ```
131
111
 
132
- The above will create a `GET /presign` route, which returns the S3 URL which
133
- the file should be uploaded to, along with the required POST parameters and
134
- request headers.
112
+ The above will create a `GET /presign` route, which internally calls
113
+ [`Shrine::Storage::S3#presign`], returning the HTTP verb (PUT) and the S3 URL
114
+ to which the file should be uploaded, along with the required parameters (will
115
+ only be present for POST presigns) and request headers.
135
116
 
136
117
  ```rb
137
118
  # GET /presign
138
119
  {
139
- "url": "https://my-bucket.s3-eu-west-1.amazonaws.com",
140
- "fields": {
141
- "key": "cache/b7d575850ba61b44c8a9ff889dfdb14d88cdc25f8dd121004c8",
142
- "policy": "eyJleHBpcmF0aW9uIjoiMjAxNS0QwMToxMToyOVoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJzaHJpbmUtdGVzdGluZyJ9LHsia2V5IjoiYjdkNTc1ODUwYmE2MWI0NGU3Y2M4YTliZmY4OGU5ZGZkYjE2NTQ0ZDk4OGNkYzI1ZjhkZDEyMTAwNGM4In0seyJ4LWFtei1jcmVkZW50aWFsIjoiQUtJQUlKRjU1VE1aWlk0NVVUNlEvMjAxNTEwMjQvZXUtd2VzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LHsieC1hbXotYWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsieC1hbXotZGF0ZSI6IjIwMTUxMDI0VDAwMTEyOVoifV19",
143
- "x-amz-credential": "AKIAIJF55TMZYT6Q/20151024/eu-west-1/s3/aws4_request",
144
- "x-amz-algorithm": "AWS4-HMAC-SHA256",
145
- "x-amz-date": "20151024T001129Z",
146
- "x-amz-signature": "c1eb634f83f96b69bd675f535b3ff15ae184b102fcba51e4db5f4959b4ae26f4"
147
- },
120
+ "method": "put",
121
+ "url": "https://my-bucket.s3.eu-central-1.amazonaws.com/cache/my-key?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIMDH2HTSB3RKB4WQ%2F20180424%2Feu-central-1%2Fs3%2Faws4_request&X-Amz-Date=20180424T212022Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=1036b9cefe52f0b46c1f257f6817fc3c55cd8d9004f87a38cf86177762359375",
122
+ "fields": {},
148
123
  "headers": {}
149
124
  }
150
125
  ```
151
126
 
152
- On the client side you can then make a request to the presign endpoint as soon
153
- as the user selects a file, and use the returned request information to upload
127
+ On the client side you can make it so that, when the user selects a file,
128
+ upload parameters are fetched from presign endpoint, and are used to upload
154
129
  the selected file directly to S3. It's recommended to use [Uppy] for this.
155
130
 
156
131
  Once the file has been uploaded, you can generate a JSON representation of the
157
- uploaded file on the client-side, and write it to the hidden attachment field.
158
- The `id` field needs to be equal to the `key` presign field minus the storage
159
- `:prefix`.
132
+ uploaded file on the client-side, and write it to the hidden attachment field
133
+ (or send it directly in an AJAX request).
160
134
 
161
- ```html
162
- <input type='hidden' name='photo[image]' value='{
135
+ ```rb
136
+ {
163
137
  "id": "302858ldg9agjad7f3ls.jpg",
164
138
  "storage": "cache",
165
139
  "metadata": {
@@ -167,12 +141,18 @@ The `id` field needs to be equal to the `key` presign field minus the storage
167
141
  "filename": "nature.jpg",
168
142
  "mime_type": "image/jpeg",
169
143
  }
170
- }'>
144
+ }
171
145
  ```
172
146
 
173
- This JSON string will now be submitted and assigned to the attachment attribute
174
- instead of the raw file. See the [demo app] for an example JavaScript
175
- implementation of multiple direct S3 uploads.
147
+ * `id` location of the file on S3 (minus the `:prefix`)
148
+ * `storage` direct uploads typically use the `:cache` storage
149
+ * `metadata` – hash of metadata extracted from the file
150
+
151
+ Once submitted this JSON will then be assigned to the attachment attribute
152
+ instead of the raw file. See [this walkthrough][direct S3 upload walkthrough]
153
+ for adding dynamic direct S3 uploads from scratch using [Uppy], as well as the
154
+ [Roda][roda demo] or [Rails][rails demo] demo app for a complete example of
155
+ multiple direct S3 uploads.
176
156
 
177
157
  ## Strategy B (static)
178
158
 
@@ -182,22 +162,22 @@ implementation of multiple direct S3 uploads.
182
162
 
183
163
  An alternative to the previous strategy is to generate an S3 upload form on
184
164
  page render. The user can then select a file and submit it directly to S3. For
185
- generating the form we can use `Shrine::Storage::S3#presign`, which returns a
186
- [`Aws::S3::PresignedPost`] object with `#url` and `#fields` attributes:
187
-
188
- ```erb
189
- <%
190
- presign = Shrine.storages[:cache].presign SecureRandom.hex,
191
- success_action_redirect: new_album_url
192
- %>
193
-
194
- <form action="<%= presign.url %>" method="post" enctype="multipart/form-data">
195
- <% presign.fields.each do |name, value| %>
196
- <input type="hidden" name="<%= name %>" value="<%= value %>">
197
- <% end %>
198
- <input type="file" name="file">
199
- <input type="submit" value="Upload">
200
- </form>
165
+ generating the form can use [`Shrine::Storage::S3#presign`], which returns URL
166
+ and form fields that should be used for the upload.
167
+
168
+ ```rb
169
+ presigned_data = Shrine.storages[:cache].presign(
170
+ SecureRandom.hex,
171
+ success_action_redirect: new_album_url
172
+ )
173
+
174
+ Forme.form(action: presigned_data[:url], method: "post", enctype: "multipart/form-data") do |f|
175
+ presigned_data[:fields].each do |name, value|
176
+ f.input :hidden, name: name, value: value
177
+ end
178
+ f.input :file, name: "file"
179
+ f.input :submit, value: "Upload"
180
+ end
201
181
  ```
202
182
 
203
183
  Note the additional `:success_action_redirect` option which tells S3 where to
@@ -206,30 +186,30 @@ builder to generate this form, you might need to also tell S3 to ignore the
206
186
  additional `utf8` and `authenticity_token` fields that Rails generates:
207
187
 
208
188
  ```rb
209
- <%
210
- presign = Shrine.storages[:cache].presign SecureRandom.hex,
211
- allow_any: ["utf8", "authenticity_token"],
212
- success_action_redirect: new_album_url
213
- %>
189
+ presigned_data = Shrine.storages[:cache].presign(
190
+ SecureRandom.hex,
191
+ allow_any: ["utf8", "authenticity_token"],
192
+ success_action_redirect: new_album_url
193
+ )
194
+
195
+ # ...
214
196
  ```
215
197
 
216
198
  Let's assume we specified the redirect URL to be a page which renders the form
217
199
  for a new record. S3 will include some information about the upload in form of
218
200
  GET parameters in the URL, out of which we only need the `key` parameter:
219
201
 
220
- ```erb
221
- <%
222
- cached_file = {
223
- storage: "cache",
224
- id: params[:key][/cache\/(.+)/, 1], # we subtract the storage prefix
225
- metadata: {},
226
- }
227
- %>
202
+ ```rb
203
+ cached_file = {
204
+ storage: "cache",
205
+ id: request.params[:key][/cache\/(.+)/, 1], # we subtract the storage prefix
206
+ metadata: {},
207
+ }
228
208
 
229
- <form action="/albums" method="post">
230
- <input type="hidden" name="album[image]" value="<%= cached_file.to_json %>">
231
- <input type="submit" value="Save">
232
- </form>
209
+ Forme.form(@album, action: "/albums", method: "post") do |f|
210
+ f.input :image, type: :hidden, value: cached_file.to_json
211
+ f.button "Save"
212
+ end
233
213
  ```
234
214
 
235
215
  ## Object data
@@ -278,15 +258,35 @@ following trick:
278
258
  ```rb
279
259
  class MyUploader < Shrine
280
260
  plugin :processing
261
+ plugin :refresh_metadata
281
262
 
282
263
  process(:store) do |io, context|
283
- real_metadata = io.open { |opened_io| extract_metadata(opened_io, context) }
284
- io.metadata.update(real_metadata)
264
+ io.refresh_metadata!
285
265
  io # return the same cached IO
286
266
  end
287
267
  end
288
268
  ```
289
269
 
270
+ ## Checksum
271
+
272
+ To have AWS S3 verify the integrity of the uploaded data, you can use a
273
+ checksum. For that you first need to tell AWS S3 that you're going to be
274
+ including the `Content-MD5` request header in the upload request, by adding
275
+ the `:content_md5` presign option.
276
+
277
+ ```rb
278
+ Shrine.plugin :presign_endpoint, presign_options: -> (request) do
279
+ {
280
+ content_md5: request.params["checksum"],
281
+ method: :put,
282
+ }
283
+ end
284
+ ```
285
+
286
+ With the above setup, you can pass the MD5 hash of the file via the `checksum`
287
+ query parameter in the request to the presign endpoint. See [this
288
+ walkthrough][checksum walkthrough] for a complete JavaScript solution.
289
+
290
290
  ## Clearing cache
291
291
 
292
292
  Directly uploaded files won't automatically be deleted from your temporary
@@ -353,11 +353,24 @@ Shrine::Attacher.promote do |data|
353
353
  end
354
354
  ```
355
355
 
356
+ ## Testing
357
+
358
+ To avoid network requests in your test and development environment, you can use
359
+ [Minio]. Minio is an open source object storage server with AWS S3 compatible
360
+ API which you can run locally. See how to set it up in the [Testing][minio
361
+ setup] guide.
362
+
363
+ [`Shrine::Storage::S3#presign`]: https://shrinerb.com/rdoc/classes/Shrine/Storage/S3.html#method-i-presign
356
364
  [`Aws::S3::PresignedPost`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Bucket.html#presigned_post-instance_method
357
- [demo app]: https://github.com/shrinerb/shrine/tree/master/demo
365
+ [direct S3 upload walkthrough]: https://gist.github.com/janko-m/9aea154d72eb85b1fbfa16e1d77946e5#adding-direct-s3-uploads-to-a-roda--sequel-app-with-shrine
366
+ [checksum walkthrough]: https://gist.github.com/janko-m/4470b5fb0737c5c1f8bcfe8cdc3fd296#using-checksums-to-verify-integrity-of-direct-uploads-with-shrine--uppy
367
+ [roda demo]: https://github.com/shrinerb/shrine/tree/master/demo
368
+ [rails demo]: https://github.com/erikdahlstrand/shrine-rails-example
358
369
  [Uppy]: https://uppy.io
359
370
  [Amazon S3 Data Consistency Model]: http://docs.aws.amazon.com/AmazonS3/latest/dev/Introduction.html#ConsistencyMode
360
371
  [CORS guide]: http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html
361
372
  [CORS API]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_bucket_cors-instance_method
362
373
  [lifecycle Console]: http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html
363
374
  [lifecycle API]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_bucket_lifecycle_configuration-instance_method
375
+ [Minio]: https://minio.io
376
+ [minio setup]: https://shrinerb.com/rdoc/files/doc/testing_md.html#label-Minio
@@ -0,0 +1,213 @@
1
+ # Extracting Metadata
2
+
3
+ Before a file is uploaded, Shrine automatically extracts metadata from it, and
4
+ stores them in the `Shrine::UploadedFile` object. By default it extracts
5
+ `size`, `filename` and `mime_type`.
6
+
7
+ ```rb
8
+ uploaded_file = uploader.upload(file)
9
+ uploaded_file.metadata #=>
10
+ # {
11
+ # "size" => 345993,
12
+ # "filename" => "matrix.mp4",
13
+ # "mime_type" => "video/mp4",
14
+ # }
15
+ ```
16
+
17
+ You can also use `Shrine#extract_metadata` directly to extract metadata from
18
+ any IO object.
19
+
20
+ ```rb
21
+ uploader.extract_metadata(io) #=>
22
+ # {
23
+ # "size" => 345993,
24
+ # "filename" => "matrix.mp4",
25
+ # "mime_type" => "video/mp4",
26
+ # }
27
+ ```
28
+
29
+ ## MIME type
30
+
31
+ By default, the `mime_type` metadata will be copied over from the
32
+ `#content_type` attribute of the input file, if present. However, since
33
+ `#content_type` value comes from the `Content-Type` header of the upload
34
+ request, it's *not guaranteed* to hold the actual MIME type of the file (browser
35
+ determines this header based on file extension). Moreover, only
36
+ `ActionDispatch::Http::UploadedFile` and `Shrine::Plugins::RackFile::UploadedFile`
37
+ objects have `#content_type` defined, so when uploading simple file objects
38
+ `mime_type` will be nil. That makes relying on `#content_type` both a security
39
+ risk and limiting.
40
+
41
+ To remedy that, Shrine comes with a `determine_mime_type` plugin which is able
42
+ to extract the MIME type from IO *content*. When you load it, the `mime_type`
43
+ plugin will now be determined using the UNIX [`file`] command.
44
+
45
+ ```rb
46
+ Shrine.plugin :determine_mime_type
47
+ ```
48
+ ```rb
49
+ uploaded_file = uploader.upload StringIO.new("<?php ... ?>")
50
+ uploaded_file.mime_type #=> "text/x-php"
51
+ ```
52
+
53
+ The `file` command won't correctly determine the MIME type in all cases, that's
54
+ why the `determine_mime_type` plugin comes with different MIME type analyzers.
55
+ So, instead of the `file` command you can use gems like [MimeMagic] or
56
+ [Marcel], as well as mix-and-match the analyzers to suit your needs. See the
57
+ plugin documentation for more details.
58
+
59
+ ## Image Dimensions
60
+
61
+ Shrine comes with a `store_dimensions` plugin for extracting image dimensions.
62
+ It adds `width` and `height` metadata values, and also adds `#width`,
63
+ `#height`, and `#dimensions` methods to the `Shrine::UploadedFile` object. By
64
+ default, the plugin uses [FastImage] to analyze dimensions, but you can also
65
+ have it use [MiniMagick] or [ruby-vips]:
66
+
67
+ ```rb
68
+ Shrine.plugin :store_dimensions, analyzer: :mini_magick
69
+ ```
70
+ ```rb
71
+ uploaded_file = uploader.upload(image)
72
+ uploaded_file.metadata["width"] #=> 1600
73
+ uploaded_file.metadata["height"] #=> 900
74
+
75
+ # convenience methods
76
+ uploaded_file.width #=> 1600
77
+ uploaded_file.height #=> 900
78
+ uploaded_file.dimensions #=> [1600, 900]
79
+ ```
80
+
81
+ ## Custom metadata
82
+
83
+ In addition to the built-in metadata, Shrine allows you to extract and store
84
+ any custom metadata, using the `add_metadata` plugin (which extends
85
+ `Shrine#extract_metadata`). For example, you might want to extract EXIF data
86
+ from images:
87
+
88
+ ```rb
89
+ require "mini_magick"
90
+
91
+ class ImageUploader < Shrine
92
+ plugin :add_metadata
93
+
94
+ add_metadata :exif do |io|
95
+ Shrine.with_file(io) do |file|
96
+ begin
97
+ MiniMagick::Image.new(file.path).exif
98
+ rescue MiniMagick::Error
99
+ # not a valid image
100
+ end
101
+ end
102
+ end
103
+ end
104
+ ```
105
+ ```rb
106
+ uploaded_file = uploader.upload(image)
107
+ uploaded_file.metadata["exif"] #=> {...}
108
+ uploaded_file.exif #=> {...}
109
+ ```
110
+
111
+ Of, if you're uploading videos, you might want to extract some video-specific
112
+ meatadata:
113
+
114
+ ```rb
115
+ require "streamio-ffmpeg"
116
+
117
+ class VideoUploader < Shrine
118
+ plugin :add_metadata
119
+
120
+ add_metadata do |io, context|
121
+ movie = Shrine.with_file(io) { |file| FFMPEG::Movie.new(file.path) }
122
+
123
+ { "duration" => movie.duration,
124
+ "bitrate" => movie.bitrate,
125
+ "resolution" => movie.resolution,
126
+ "frame_rate" => movie.frame_rate }
127
+ end
128
+ end
129
+ ```
130
+ ```rb
131
+ uploaded_file = uploader.upload(video)
132
+ uploaded_file.metadata #=>
133
+ # {
134
+ # ...
135
+ # "duration" => 7.5,
136
+ # "bitrate" => 481,
137
+ # "resolution" => "640x480",
138
+ # "frame_rate" => 16.72
139
+ # }
140
+ ```
141
+
142
+ The yielded `io` object will not always be an object that responds to `#path`.
143
+ If you're using the `data_uri` plugin, the `io` will be a `StringIO` wrapper.
144
+ When the `restore_cached_data` plugin is loaded, any assigned cached file will
145
+ get their metadata extracted, and `io` will be a `Shrine::UploadedFile` object.
146
+ If you're using a metadata analyzer that requires the source file to be on
147
+ disk, you can use `Shrine.with_file` to ensure you have a file object.
148
+
149
+ Also, be aware that metadata is extracted before file validation, so you'll
150
+ need to handle the cases where the file is not of expected type.
151
+
152
+ ## Metadata columns
153
+
154
+ If you want to write any of the metadata values into a separate database column
155
+ on the record, you can use the `metadata_attributes` plugin.
156
+
157
+ ```rb
158
+ Shrine.plugin :metadata_attributes, :mime_type => :type
159
+ ```
160
+ ```rb
161
+ photo = Photo.new(image: file)
162
+ photo.image_type #=> "image/jpeg"
163
+ ```
164
+
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.
173
+
174
+ ```rb
175
+ Shrine.plugin :restore_cached_data # extract metadata from cached files on assingment
176
+ ```
177
+ ```rb
178
+ photo.image = '{"id":"ks9elsd.jpg","storage":"cache","metadata":{}}' # metadata is extracted
179
+ photo.image.metadata #=>
180
+ # {
181
+ # "size" => 4593484,
182
+ # "filename" => "nature.jpg",
183
+ # "mime_type" => "image/jpeg"
184
+ # }
185
+ ```
186
+
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).
198
+
199
+ ```rb
200
+ Shrine.plugin :refresh_metadata
201
+ ```
202
+ ```rb
203
+ uploaded_file.metadata #=> {}
204
+ uploaded_file.refresh_metadata!
205
+ uploaded_file.metadata #=> {"filename"=>"nature.jpg","size"=>532894,"mime_type"=>"image/jpeg"}
206
+ ```
207
+
208
+ [`file`]: http://linux.die.net/man/1/file
209
+ [MimeMagic]: https://github.com/minad/mimemagic
210
+ [Marcel]: https://github.com/basecamp/marcel
211
+ [FastImage]: https://github.com/sdsykes/fastimage
212
+ [MiniMagick]: https://github.com/minimagick/minimagick
213
+ [ruby-vips]: https://github.com/jcupitt/ruby-vips