shrine 2.9.0 → 2.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +40 -25
- data/doc/carrierwave.md +15 -9
- data/doc/creating_storages.md +1 -1
- data/doc/direct_s3.md +2 -2
- data/doc/paperclip.md +11 -5
- data/doc/refile.md +15 -9
- data/doc/testing.md +23 -10
- data/lib/shrine.rb +44 -28
- data/lib/shrine/plugins/determine_mime_type.rb +21 -4
- data/lib/shrine/plugins/download_endpoint.rb +1 -1
- data/lib/shrine/plugins/infer_extension.rb +12 -19
- data/lib/shrine/plugins/logging.rb +17 -13
- data/lib/shrine/plugins/processing.rb +14 -4
- data/lib/shrine/plugins/remote_url.rb +1 -1
- data/lib/shrine/plugins/signature.rb +2 -2
- data/lib/shrine/plugins/store_dimensions.rb +65 -22
- data/lib/shrine/plugins/versions.rb +23 -11
- data/lib/shrine/storage/file_system.rb +3 -1
- data/lib/shrine/storage/linter.rb +1 -1
- data/lib/shrine/storage/s3.rb +69 -3
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +3 -1
- metadata +31 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5820e9c06eec70c66a52ff3cc099847651076e2e
|
4
|
+
data.tar.gz: 6328bfb5edf7cb31a0e6b8ebb8a0815483451314
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41411c21937516fb928e686378c8b2d16a3a30ae9f1bf15c47010863a7d39c676c6a9ded87cad0a48f95108e1cc6dfc7e3cfab5363d79e73f3f14505fa05a9b9
|
7
|
+
data.tar.gz: 832a9a41216b0c2406b327bbb442aa0f82af69973afef87f7ee99abcc72ddde3d5634330f8b8cb9fc123589a171fd61cd2754079999d2e0d5d643f9ae493fe44
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
## 2.10.0 (2018-03-28)
|
2
|
+
|
3
|
+
* Add `:fastimage` analyzer to `determine_mime_type` plugin (@mokolabs)
|
4
|
+
|
5
|
+
* Keep download endpoint URL the same regardless of metadata ordering (@MSchmidt)
|
6
|
+
|
7
|
+
* Remove `:rack_mime` extension inferrer from the `infer_extension` plugin (@janko-m)
|
8
|
+
|
9
|
+
* Allow `UploadedFile#download` to accept a block for temporary file download (@janko-m)
|
10
|
+
|
11
|
+
* Add `:ruby_vips` analyzer to `store_dimensions` plugin (@janko-m)
|
12
|
+
|
13
|
+
* Add `:mini_magick` analyzer to `store_dimensions` plugin (@janko-m)
|
14
|
+
|
15
|
+
* Soft-rename `:heroku` logging format to `:logfmt` (@janko-m)
|
16
|
+
|
17
|
+
* Deprecate `Shrine::IO_METHODS` constant (@janko-m)
|
18
|
+
|
19
|
+
* Don't require IO size to be known on upload (@janko-m)
|
20
|
+
|
21
|
+
* Inherit the logger on subclassing `Shrine` and make it shared across subclasses (@hmistry)
|
22
|
+
|
1
23
|
## 2.9.0 (2018-01-27)
|
2
24
|
|
3
25
|
* Support arrays of files in `versions` plugin (@janko-m)
|
data/README.md
CHANGED
@@ -8,8 +8,8 @@ If you're not sure why you should care, you're encouraged to read the
|
|
8
8
|
## Resources
|
9
9
|
|
10
10
|
- Documentation: [shrinerb.com](http://shrinerb.com)
|
11
|
-
- Source: [github.com/
|
12
|
-
- Bugs: [github.com/
|
11
|
+
- Source: [github.com/shrinerb/shrine](https://github.com/shrinerb/shrine)
|
12
|
+
- Bugs: [github.com/shrinerb/shrine/issues](https://github.com/shrinerb/shrine/issues)
|
13
13
|
- Help & Discussion: [groups.google.com/group/ruby-shrine](https://groups.google.com/forum/#!forum/ruby-shrine)
|
14
14
|
|
15
15
|
## Quick start
|
@@ -238,10 +238,14 @@ It comes with many convenient methods that delegate to the storage:
|
|
238
238
|
|
239
239
|
```rb
|
240
240
|
uploaded_file.url #=> "https://my-bucket.s3.amazonaws.com/949sdjg834.jpg"
|
241
|
-
uploaded_file.
|
241
|
+
uploaded_file.open #=> IO object
|
242
|
+
uploaded_file.download #=> #<File:/var/folders/.../20180302-33119-1h1vjbq.jpg>
|
242
243
|
uploaded_file.exists? #=> true
|
243
|
-
uploaded_file.
|
244
|
-
|
244
|
+
uploaded_file.delete # deletes the file from the storage
|
245
|
+
|
246
|
+
# open/download the uploaded file for the duration of the block
|
247
|
+
uploaded_file.open { |io| io.read }
|
248
|
+
uploaded_file.download { |tempfile| tempfile.read }
|
245
249
|
```
|
246
250
|
|
247
251
|
It also implements the IO-like interface that conforms to Shrine's IO
|
@@ -500,18 +504,25 @@ images, I created the [image_processing] gem which you can use with Shrine:
|
|
500
504
|
|
501
505
|
```rb
|
502
506
|
# Gemfile
|
503
|
-
gem "image_processing"
|
504
|
-
gem "mini_magick", "
|
507
|
+
gem "image_processing", "~> 0.10"
|
508
|
+
gem "mini_magick", "~> 4.0"
|
505
509
|
```
|
506
510
|
```rb
|
507
511
|
require "image_processing/mini_magick"
|
508
512
|
|
509
513
|
class ImageUploader < Shrine
|
510
|
-
include ImageProcessing::MiniMagick
|
511
514
|
plugin :processing
|
512
515
|
|
513
516
|
process(:store) do |io, context|
|
514
|
-
|
517
|
+
original = io.download
|
518
|
+
|
519
|
+
resized = ImageProcessing::MiniMagick
|
520
|
+
.source(original)
|
521
|
+
.resize_to_limit!(800, 800)
|
522
|
+
|
523
|
+
original.close!
|
524
|
+
|
525
|
+
resized
|
515
526
|
end
|
516
527
|
end
|
517
528
|
```
|
@@ -541,19 +552,21 @@ deleted after uploading.
|
|
541
552
|
require "image_processing/mini_magick"
|
542
553
|
|
543
554
|
class ImageUploader < Shrine
|
544
|
-
include ImageProcessing::MiniMagick
|
545
555
|
plugin :processing
|
546
556
|
plugin :versions # enable Shrine to handle a hash of files
|
547
557
|
plugin :delete_raw # delete processed files after uploading
|
548
558
|
|
549
559
|
process(:store) do |io, context|
|
550
560
|
original = io.download
|
561
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
562
|
+
|
563
|
+
size_800 = pipeline.resize_to_limit!(800, 800)
|
564
|
+
size_500 = pipeline.resize_to_limit!(500, 500)
|
565
|
+
size_300 = pipeline.resize_to_limit!(300, 300)
|
551
566
|
|
552
|
-
|
553
|
-
size_500 = resize_to_limit(size_800, 500, 500)
|
554
|
-
size_300 = resize_to_limit(size_500, 300, 300)
|
567
|
+
original.close!
|
555
568
|
|
556
|
-
{original: io, large: size_800, medium: size_500, small: size_300}
|
569
|
+
{ original: io, large: size_800, medium: size_500, small: size_300 }
|
557
570
|
end
|
558
571
|
end
|
559
572
|
```
|
@@ -679,9 +692,11 @@ user.errors.to_hash #=> {cv: ["is too large (max is 5 MB)"]}
|
|
679
692
|
You can also do custom validations:
|
680
693
|
|
681
694
|
```rb
|
682
|
-
class
|
695
|
+
class ImageUploader < Shrine
|
683
696
|
Attacher.validate do
|
684
|
-
|
697
|
+
get.download do |tempfile|
|
698
|
+
errors << "image is corrupted" unless ImageProcessing::MiniMagick.valid_image?(tempfile)
|
699
|
+
end
|
685
700
|
end
|
686
701
|
end
|
687
702
|
```
|
@@ -1010,7 +1025,7 @@ The gem is available as open source under the terms of the [MIT License].
|
|
1010
1025
|
[plugins]: http://shrinerb.com/#plugins
|
1011
1026
|
[`file`]: http://linux.die.net/man/1/file
|
1012
1027
|
[backgrounding]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/Backgrounding.html
|
1013
|
-
[Context]: https://github.com/
|
1028
|
+
[Context]: https://github.com/shrinerb/shrine#context
|
1014
1029
|
[image_processing]: https://github.com/janko-m/image_processing
|
1015
1030
|
[ffmpeg]: https://github.com/streamio/streamio-ffmpeg
|
1016
1031
|
[Uppy]: https://uppy.io
|
@@ -1019,22 +1034,22 @@ The gem is available as open source under the terms of the [MIT License].
|
|
1019
1034
|
[Microsoft Azure Storage]: https://azure.microsoft.com/en-us/services/storage/
|
1020
1035
|
[upload_endpoint]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/UploadEndpoint.html
|
1021
1036
|
[presign_endpoint]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/PresignEndpoint.html
|
1022
|
-
[Cloudinary]: https://github.com/
|
1023
|
-
[Imgix]: https://github.com/
|
1024
|
-
[Uploadcare]: https://github.com/
|
1037
|
+
[Cloudinary]: https://github.com/shrinerb/shrine-cloudinary
|
1038
|
+
[Imgix]: https://github.com/shrinerb/shrine-imgix
|
1039
|
+
[Uploadcare]: https://github.com/shrinerb/shrine-uploadcare
|
1025
1040
|
[Dragonfly]: http://markevans.github.io/dragonfly/
|
1026
1041
|
[tus]: http://tus.io
|
1027
1042
|
[tus-ruby-server]: https://github.com/janko-m/tus-ruby-server
|
1028
1043
|
[tus-js-client]: https://github.com/tus/tus-js-client
|
1029
|
-
[shrine-tus-demo]: https://github.com/
|
1030
|
-
[shrine-url]: https://github.com/
|
1044
|
+
[shrine-tus-demo]: https://github.com/shrinerb/shrine-tus-demo
|
1045
|
+
[shrine-url]: https://github.com/shrinerb/shrine-url
|
1031
1046
|
[Roda]: https://github.com/jeremyevans/roda
|
1032
1047
|
[Refile]: https://github.com/refile/refile
|
1033
1048
|
[MIT License]: http://opensource.org/licenses/MIT
|
1034
|
-
[CoC]: https://github.com/
|
1035
|
-
[roda_demo]: https://github.com/
|
1049
|
+
[CoC]: https://github.com/shrinerb/shrine/blob/master/CODE_OF_CONDUCT.md
|
1050
|
+
[roda_demo]: https://github.com/shrinerb/shrine/tree/master/demo
|
1036
1051
|
[rails_demo]: https://github.com/erikdahlstrand/shrine-rails-example
|
1037
|
-
[backgrounding libraries]: https://github.com/
|
1052
|
+
[backgrounding libraries]: https://github.com/shrinerb/shrine/wiki/Backgrounding-libraries
|
1038
1053
|
[S3 lifecycle Console]: http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html
|
1039
1054
|
[S3 lifecycle API]: https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Client.html#put_bucket_lifecycle_configuration-instance_method
|
1040
1055
|
[processing post]: https://twin.github.io/better-file-uploads-with-shrine-processing/
|
data/doc/carrierwave.md
CHANGED
@@ -90,17 +90,23 @@ end
|
|
90
90
|
```
|
91
91
|
|
92
92
|
```rb
|
93
|
+
require "image_processing/mini_magick"
|
94
|
+
|
93
95
|
class ImageUploader < Shrine
|
94
|
-
include ImageProcessing::MiniMagick
|
95
96
|
plugin :processing
|
96
97
|
plugin :versions
|
97
98
|
|
98
99
|
process(:store) do |io, context|
|
99
|
-
|
100
|
-
|
101
|
-
|
100
|
+
original = io.download
|
101
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
102
|
+
|
103
|
+
size_800 = pipeline.resize_to_limit!(800, 800)
|
104
|
+
size_500 = pipeline.resize_to_limit!(500, 500)
|
105
|
+
size_300 = pipeline.resize_to_limit!(300, 300)
|
106
|
+
|
107
|
+
original.close!
|
102
108
|
|
103
|
-
{original: size_800, medium: size_500, small: size_300}
|
109
|
+
{ original: size_800, medium: size_500, small: size_300 }
|
104
110
|
end
|
105
111
|
end
|
106
112
|
```
|
@@ -700,14 +706,14 @@ plugin :default_url_options, store: {expires_in: 600}
|
|
700
706
|
Shrine allows you to override the S3 endpoint:
|
701
707
|
|
702
708
|
```rb
|
703
|
-
Shrine::Storage::S3.new(
|
709
|
+
Shrine::Storage::S3.new(endpoint: "https://s3-accelerate.amazonaws.com", **options)
|
704
710
|
```
|
705
711
|
|
706
712
|
[image_processing]: https://github.com/janko-m/image_processing
|
707
|
-
[demo app]: https://github.com/
|
713
|
+
[demo app]: https://github.com/shrinerb/shrine/tree/master/demo
|
708
714
|
[Reprocessing versions]: http://shrinerb.com/rdoc/files/doc/regenerating_versions_md.html
|
709
|
-
[shrine-fog]: https://github.com/
|
715
|
+
[shrine-fog]: https://github.com/shrinerb/shrine-fog
|
710
716
|
[direct uploads]: http://shrinerb.com/rdoc/files/doc/direct_s3_md.html
|
711
717
|
[`Shrine::Storage::S3`]: http://shrinerb.com/rdoc/classes/Shrine/Storage/S3.html
|
712
718
|
[`Shrine::Storage::GoogleCloudStorage`]: https://github.com/renchap/shrine-google_cloud_storage
|
713
|
-
[`Shrine::Storage::Fog`]: https://github.com/
|
719
|
+
[`Shrine::Storage::Fog`]: https://github.com/shrinerb/shrine-fog
|
data/doc/creating_storages.md
CHANGED
@@ -226,4 +226,4 @@ tests for your storage. There will likely be some edge cases that won't be
|
|
226
226
|
tested by the linter.
|
227
227
|
|
228
228
|
[HTTP.rb]: https://github.com/httprb/http
|
229
|
-
[fake IO]: https://github.com/
|
229
|
+
[fake IO]: https://github.com/shrinerb/shrine-cloudinary/blob/ca587c580ea0762992a2df33fd590c9a1e534905/test/test_helper.rb#L20-L27
|
data/doc/direct_s3.md
CHANGED
@@ -299,7 +299,7 @@ s3.clear! { |object| object.last_modified < Time.now - 7*24*60*60 } # delete fil
|
|
299
299
|
```
|
300
300
|
|
301
301
|
Alternatively you can add a bucket lifeycle rule to do this for you. This can
|
302
|
-
be done either from the [AWS Console][lifecycle
|
302
|
+
be done either from the [AWS Console][lifecycle Console] or via an [API
|
303
303
|
call][lifecycle API]:
|
304
304
|
|
305
305
|
```rb
|
@@ -354,7 +354,7 @@ end
|
|
354
354
|
```
|
355
355
|
|
356
356
|
[`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/
|
357
|
+
[demo app]: https://github.com/shrinerb/shrine/tree/master/demo
|
358
358
|
[Uppy]: https://uppy.io
|
359
359
|
[Amazon S3 Data Consistency Model]: http://docs.aws.amazon.com/AmazonS3/latest/dev/Introduction.html#ConsistencyMode
|
360
360
|
[CORS guide]: http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html
|
data/doc/paperclip.md
CHANGED
@@ -95,17 +95,23 @@ end
|
|
95
95
|
```
|
96
96
|
|
97
97
|
```rb
|
98
|
+
require "image_processing/mini_magick"
|
99
|
+
|
98
100
|
class ImageUploader < Shrine
|
99
|
-
include ImageProcessing::MiniMagick
|
100
101
|
plugin :processing
|
101
102
|
plugin :versions
|
102
103
|
|
103
104
|
process(:store) do |io, context|
|
104
|
-
|
105
|
-
|
106
|
-
|
105
|
+
original = io.download
|
106
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
107
|
+
|
108
|
+
size_800 = pipeline.resize_to_limit!(800, 800)
|
109
|
+
size_500 = pipeline.resize_to_limit!(500, 500)
|
110
|
+
size_300 = pipeline.resize_to_limit!(300, 300)
|
111
|
+
|
112
|
+
original.close!
|
107
113
|
|
108
|
-
{large: size_800, medium: size_500, small: size_300}
|
114
|
+
{ original: io, large: size_800, medium: size_500, small: size_300 }
|
109
115
|
end
|
110
116
|
end
|
111
117
|
```
|
data/doc/refile.md
CHANGED
@@ -65,17 +65,23 @@ an open-source solution, [Attache], which you can also use with Shrine.
|
|
65
65
|
This is how you would process multiple versions in Shrine:
|
66
66
|
|
67
67
|
```rb
|
68
|
+
require "image_processing/mini_magick"
|
69
|
+
|
68
70
|
class ImageUploader < Shrine
|
69
|
-
include ImageProcessing::MiniMagick
|
70
71
|
plugin :processing
|
71
72
|
plugin :versions
|
72
73
|
|
73
74
|
process(:store) do |io, context|
|
74
|
-
|
75
|
-
|
76
|
-
|
75
|
+
original = io.download
|
76
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
77
|
+
|
78
|
+
size_800 = pipeline.resize_to_limit!(800, 800)
|
79
|
+
size_500 = pipeline.resize_to_limit!(500, 500)
|
80
|
+
size_300 = pipeline.resize_to_limit!(300, 300)
|
81
|
+
|
82
|
+
original.close!
|
77
83
|
|
78
|
-
{original: size_800, medium: size_500, small: size_300}
|
84
|
+
{ original: io, large: size_800, medium: size_500, small: size_300 }
|
79
85
|
end
|
80
86
|
end
|
81
87
|
```
|
@@ -473,12 +479,12 @@ Shrine.plugin :remote_url
|
|
473
479
|
<% end %>
|
474
480
|
```
|
475
481
|
|
476
|
-
[shrine-cloudinary]: https://github.com/
|
477
|
-
[shrine-imgix]: https://github.com/
|
478
|
-
[shrine-uploadcare]: https://github.com/
|
482
|
+
[shrine-cloudinary]: https://github.com/shrinerb/shrine-cloudinary
|
483
|
+
[shrine-imgix]: https://github.com/shrinerb/shrine-imgix
|
484
|
+
[shrine-uploadcare]: https://github.com/shrinerb/shrine-uploadcare
|
479
485
|
[Attache]: https://github.com/choonkeat/attache
|
480
486
|
[image_processing]: https://github.com/janko-m/image_processing
|
481
487
|
[Uppy]: https://uppy.io
|
482
488
|
[Direct Uploads to S3]: http://shrinerb.com/rdoc/files/doc/direct_s3_md.html
|
483
|
-
[demo app]: https://github.com/
|
489
|
+
[demo app]: https://github.com/shrinerb/shrine/tree/master/demo
|
484
490
|
[Multiple Files]: http://shrinerb.com/rdoc/files/doc/multiple_files_md.html
|
data/doc/testing.md
CHANGED
@@ -169,7 +169,7 @@ class FakeIO
|
|
169
169
|
end
|
170
170
|
|
171
171
|
extend Forwardable
|
172
|
-
delegate
|
172
|
+
delegate %i[read rewind eof? close size] => :@io
|
173
173
|
end
|
174
174
|
```
|
175
175
|
|
@@ -251,25 +251,38 @@ storage server with Amazon S3 compatible API. If you're on a Mac you can
|
|
251
251
|
install it with Homebrew:
|
252
252
|
|
253
253
|
```
|
254
|
-
$ brew install minio
|
255
|
-
$ minio server data/
|
254
|
+
$ brew install minio/stable/minio
|
256
255
|
```
|
257
256
|
|
258
|
-
|
259
|
-
|
260
|
-
server:
|
257
|
+
Now you can start the Minio server and give it a directory where it will store
|
258
|
+
the data:
|
261
259
|
|
262
260
|
```
|
261
|
+
$ minio server data/
|
262
|
+
```
|
263
|
+
|
264
|
+
This command will print out the credentials for the running Minio server, as
|
265
|
+
well as a link to the Minio web interface. Follow that link and create a new
|
266
|
+
bucket. Once you've done that, all that's lef to do is configure
|
267
|
+
`Shrine::Storage::S3` with the credentials of your Minio server:
|
268
|
+
|
269
|
+
```rb
|
263
270
|
Shrine::Storage::S3.new(
|
264
|
-
access_key_id: "
|
265
|
-
secret_access_key: "
|
266
|
-
|
271
|
+
access_key_id: "MINIO_ACCESS_KEY", # "AccessKey" value
|
272
|
+
secret_access_key: "MINIO_SECRET_KEY", # "SecretKey" value
|
273
|
+
endpoint: "MINIO_ENDPOINT", # "Endpoint" value
|
274
|
+
bucket: "MINIO_BUCKET", # name of the bucket you created
|
267
275
|
region: "us-east-1",
|
276
|
+
force_path_style: true,
|
268
277
|
)
|
269
278
|
```
|
270
279
|
|
280
|
+
The `:endpoint` option will make `aws-sdk-s3` point all URLs to your Minio
|
281
|
+
server (instead of `s3.amazonaws.com`), and `:force_path_style` tells it not
|
282
|
+
to use subdomains when generating URLs.
|
283
|
+
|
271
284
|
[DatabaseCleaner]: https://github.com/DatabaseCleaner/database_cleaner
|
272
|
-
[shrine-memory]: https://github.com/
|
285
|
+
[shrine-memory]: https://github.com/shrinerb/shrine-memory
|
273
286
|
[factory_bot]: https://github.com/thoughtbot/factory_bot
|
274
287
|
[Capybara]: https://github.com/jnicklas/capybara
|
275
288
|
[`#attach_file`]: http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions#attach_file-instance_method
|
data/lib/shrine.rb
CHANGED
@@ -13,17 +13,7 @@ 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
|
-
|
17
|
-
end
|
18
|
-
|
19
|
-
def message
|
20
|
-
"#{@io.inspect} is not a valid IO object (it doesn't respond to #{missing_methods_string})"
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def missing_methods_string
|
26
|
-
@missing_methods.map { |m, args| "##{m}" }.join(", ")
|
16
|
+
super "#{io.inspect} is not a valid IO object (it doesn't respond to #{missing_methods.map{|m, args|"##{m}"}.join(", ")})"
|
27
17
|
end
|
28
18
|
end
|
29
19
|
|
@@ -36,6 +26,7 @@ class Shrine
|
|
36
26
|
size: [],
|
37
27
|
close: [],
|
38
28
|
}
|
29
|
+
deprecate_constant(:IO_METHODS) if RUBY_VERSION > "2.3"
|
39
30
|
|
40
31
|
# Core class that represents a file uploaded to a storage. The instance
|
41
32
|
# methods for this class are added by Shrine::Plugins::Base::FileMethods, the
|
@@ -289,7 +280,7 @@ class Shrine
|
|
289
280
|
|
290
281
|
# Extracts the filesize from the IO object.
|
291
282
|
def extract_size(io)
|
292
|
-
io.size
|
283
|
+
io.size if io.respond_to?(:size)
|
293
284
|
end
|
294
285
|
|
295
286
|
# It first asserts that `io` is a valid IO object. It then extracts
|
@@ -366,7 +357,7 @@ class Shrine
|
|
366
357
|
# object doesn't respond to one of these methods, a Shrine::InvalidFile
|
367
358
|
# error is raised.
|
368
359
|
def _enforce_io(io)
|
369
|
-
missing_methods =
|
360
|
+
missing_methods = %i[read eof? rewind close].select { |m| !io.respond_to?(m) }
|
370
361
|
raise InvalidFile.new(io, missing_methods) if missing_methods.any?
|
371
362
|
end
|
372
363
|
|
@@ -778,13 +769,24 @@ class Shrine
|
|
778
769
|
end
|
779
770
|
alias content_type mime_type
|
780
771
|
|
781
|
-
#
|
782
|
-
#
|
783
|
-
#
|
772
|
+
# Calls `#open` on the storage to open the uploaded file for reading.
|
773
|
+
# Most storages will return a lazy IO object which dynamically
|
774
|
+
# retrieves file content from the storage as the object is being read.
|
784
775
|
#
|
785
|
-
#
|
786
|
-
#
|
787
|
-
#
|
776
|
+
# If a block is given, the opened IO object is yielded to the block,
|
777
|
+
# and at the end of the block it's automatically closed. In this case
|
778
|
+
# the return value of the method is the block return value.
|
779
|
+
#
|
780
|
+
# If no block is given, the opened IO object is returned.
|
781
|
+
#
|
782
|
+
# uploaded_file.open #=> IO object returned by the storage
|
783
|
+
# uploaded_file.read #=> "..."
|
784
|
+
# uploaded_file.close
|
785
|
+
#
|
786
|
+
# # or
|
787
|
+
#
|
788
|
+
# uploaded_file.open { |io| io.read }
|
789
|
+
# #=> "..."
|
788
790
|
def open(*args)
|
789
791
|
return to_io unless block_given?
|
790
792
|
|
@@ -799,19 +801,33 @@ class Shrine
|
|
799
801
|
|
800
802
|
# Calls `#download` on the storage if the storage implements it,
|
801
803
|
# otherwise uses #open to stream the underlying IO to a Tempfile.
|
804
|
+
#
|
805
|
+
# If a block is given, the opened Tempfile object is yielded to the
|
806
|
+
# block, and at the end of the block it's automatically closed and
|
807
|
+
# deleted. In this case the return value of the method is the block
|
808
|
+
# return value.
|
809
|
+
#
|
810
|
+
# If no block is given, the opened Tempfile is returned.
|
811
|
+
#
|
812
|
+
# uploaded_file.download
|
813
|
+
# #=> #<File:/var/folders/.../20180302-33119-1h1vjbq.jpg>
|
814
|
+
#
|
815
|
+
# # or
|
816
|
+
#
|
817
|
+
# uploaded_file.download { |tempfile| tempfile.read } # tempfile is deleted
|
818
|
+
# #=> "..."
|
802
819
|
def download(*args)
|
803
820
|
if storage.respond_to?(:download)
|
804
|
-
storage.download(id, *args)
|
821
|
+
tempfile = storage.download(id, *args)
|
805
822
|
else
|
806
|
-
|
807
|
-
|
808
|
-
|
809
|
-
tempfile.tap(&:open)
|
810
|
-
rescue
|
811
|
-
tempfile.close! if tempfile
|
812
|
-
raise
|
813
|
-
end
|
823
|
+
tempfile = Tempfile.new(["shrine", ".#{extension}"], binmode: true)
|
824
|
+
open(*args) { |io| IO.copy_stream(io, tempfile) }
|
825
|
+
tempfile.open
|
814
826
|
end
|
827
|
+
|
828
|
+
block_given? ? yield(tempfile) : tempfile
|
829
|
+
ensure
|
830
|
+
tempfile.close! if ($! || block_given?) && tempfile
|
815
831
|
end
|
816
832
|
|
817
833
|
# Part of complying to the IO interface. It delegates to the internally
|
@@ -20,6 +20,10 @@ class Shrine
|
|
20
20
|
# contents. It is installed by default on most operating systems, but the
|
21
21
|
# [Windows equivalent] needs to be installed separately.
|
22
22
|
#
|
23
|
+
# :fastimage
|
24
|
+
# : Uses the [fastimage] gem to determine the MIME type from file contents.
|
25
|
+
# Fastimage is optimized for speed over accuracy. Best used for image content.
|
26
|
+
#
|
23
27
|
# :filemagic
|
24
28
|
# : Uses the [ruby-filemagic] gem to determine the MIME type from file
|
25
29
|
# contents, using a similar MIME database as the `file` utility. Unlike
|
@@ -81,6 +85,7 @@ class Shrine
|
|
81
85
|
# [marcel]: https://github.com/basecamp/marcel
|
82
86
|
# [mime-types]: https://github.com/mime-types/ruby-mime-types
|
83
87
|
# [mini_mime]: https://github.com/discourse/mini_mime
|
88
|
+
# [fastimage]: https://github.com/sdsykes/fastimage
|
84
89
|
module DetermineMimeType
|
85
90
|
def self.configure(uploader, opts = {})
|
86
91
|
uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:mime_type_analyzer, :file))
|
@@ -94,7 +99,7 @@ class Shrine
|
|
94
99
|
mime_type = io.content_type if io.respond_to?(:content_type)
|
95
100
|
else
|
96
101
|
analyzer = opts[:mime_type_analyzer]
|
97
|
-
analyzer =
|
102
|
+
analyzer = mime_type_analyzer(analyzer) if analyzer.is_a?(Symbol)
|
98
103
|
args = [io, mime_type_analyzers].take(analyzer.arity.abs)
|
99
104
|
|
100
105
|
mime_type = analyzer.call(*args)
|
@@ -109,9 +114,14 @@ class Shrine
|
|
109
114
|
# IO object.
|
110
115
|
def mime_type_analyzers
|
111
116
|
@mime_type_analyzers ||= MimeTypeAnalyzer::SUPPORTED_TOOLS.inject({}) do |hash, tool|
|
112
|
-
hash.merge!(tool =>
|
117
|
+
hash.merge!(tool => mime_type_analyzer(tool))
|
113
118
|
end
|
114
119
|
end
|
120
|
+
|
121
|
+
# Returns callable mime type analyzer object.
|
122
|
+
def mime_type_analyzer(name)
|
123
|
+
MimeTypeAnalyzer.new(name).method(:call)
|
124
|
+
end
|
115
125
|
end
|
116
126
|
|
117
127
|
module InstanceMethods
|
@@ -135,11 +145,11 @@ class Shrine
|
|
135
145
|
end
|
136
146
|
|
137
147
|
class MimeTypeAnalyzer
|
138
|
-
SUPPORTED_TOOLS = [:file, :filemagic, :mimemagic, :marcel, :mime_types, :mini_mime]
|
148
|
+
SUPPORTED_TOOLS = [:fastimage, :file, :filemagic, :mimemagic, :marcel, :mime_types, :mini_mime]
|
139
149
|
MAGIC_NUMBER = 256 * 1024
|
140
150
|
|
141
151
|
def initialize(tool)
|
142
|
-
raise
|
152
|
+
raise Error, "unknown mime type analyzer #{tool.inspect}, supported analyzers are: #{SUPPORTED_TOOLS.join(",")}" unless SUPPORTED_TOOLS.include?(tool)
|
143
153
|
|
144
154
|
@tool = tool
|
145
155
|
end
|
@@ -176,6 +186,13 @@ class Shrine
|
|
176
186
|
raise Error, "The `file` command-line tool is not installed"
|
177
187
|
end
|
178
188
|
|
189
|
+
def extract_with_fastimage(io)
|
190
|
+
require "fastimage"
|
191
|
+
|
192
|
+
type = FastImage.type(io)
|
193
|
+
"image/#{type}" if type
|
194
|
+
end
|
195
|
+
|
179
196
|
def extract_with_filemagic(io)
|
180
197
|
require "filemagic"
|
181
198
|
|
@@ -131,7 +131,7 @@ class Shrine
|
|
131
131
|
# long.
|
132
132
|
def download_identifier
|
133
133
|
semantical_metadata = metadata.select { |name, _| %w[filename size mime_type].include?(name) }
|
134
|
-
download_serializer.dump(data.merge("metadata" => semantical_metadata))
|
134
|
+
download_serializer.dump(data.merge("metadata" => semantical_metadata.sort.to_h))
|
135
135
|
end
|
136
136
|
|
137
137
|
def download_serializer
|
@@ -10,19 +10,15 @@ class Shrine
|
|
10
10
|
# plugin :infer_extension
|
11
11
|
#
|
12
12
|
# The upload location will gain the inferred extension only if couldn't be
|
13
|
-
# determined from the filename. By default `
|
13
|
+
# determined from the filename. By default `MIME::Types` will be used for
|
14
14
|
# inferring the extension, but you can also choose a different inferrer:
|
15
15
|
#
|
16
|
-
# plugin :infer_extension, inferrer: :
|
16
|
+
# plugin :infer_extension, inferrer: :mini_mime
|
17
17
|
#
|
18
18
|
# The following inferrers are accepted:
|
19
19
|
#
|
20
|
-
# :rack_mime
|
21
|
-
# : (Default). Uses the `Rack::Mime` module to infer the appropriate
|
22
|
-
# extension from MIME type.
|
23
|
-
#
|
24
20
|
# :mime_types
|
25
|
-
# : Uses the [mime-types] gem to infer the appropriate extension from MIME
|
21
|
+
# : (Default). Uses the [mime-types] gem to infer the appropriate extension from MIME
|
26
22
|
# type.
|
27
23
|
#
|
28
24
|
# :mini_mime
|
@@ -49,13 +45,13 @@ class Shrine
|
|
49
45
|
# [mini_mime]: https://github.com/discourse/mini_mime
|
50
46
|
module InferExtension
|
51
47
|
def self.configure(uploader, opts = {})
|
52
|
-
uploader.opts[:extension_inferrer] = opts.fetch(:inferrer, uploader.opts.fetch(:infer_extension_inferrer, :
|
48
|
+
uploader.opts[:extension_inferrer] = opts.fetch(:inferrer, uploader.opts.fetch(:infer_extension_inferrer, :mime_types))
|
53
49
|
end
|
54
50
|
|
55
51
|
module ClassMethods
|
56
52
|
def infer_extension(mime_type)
|
57
53
|
inferrer = opts[:extension_inferrer]
|
58
|
-
inferrer =
|
54
|
+
inferrer = extension_inferrer(inferrer) if inferrer.is_a?(Symbol)
|
59
55
|
args = [mime_type, extension_inferrers].take(inferrer.arity.abs)
|
60
56
|
|
61
57
|
inferrer.call(*args)
|
@@ -63,9 +59,13 @@ class Shrine
|
|
63
59
|
|
64
60
|
def extension_inferrers
|
65
61
|
@extension_inferrers ||= ExtensionInferrer::SUPPORTED_TOOLS.inject({}) do |hash, tool|
|
66
|
-
hash.merge!(tool =>
|
62
|
+
hash.merge!(tool => extension_inferrer(tool))
|
67
63
|
end
|
68
64
|
end
|
65
|
+
|
66
|
+
def extension_inferrer(name)
|
67
|
+
ExtensionInferrer.new(name).method(:call)
|
68
|
+
end
|
69
69
|
end
|
70
70
|
|
71
71
|
module InstanceMethods
|
@@ -85,10 +85,10 @@ class Shrine
|
|
85
85
|
end
|
86
86
|
|
87
87
|
class ExtensionInferrer
|
88
|
-
SUPPORTED_TOOLS = [:
|
88
|
+
SUPPORTED_TOOLS = [:mime_types, :mini_mime]
|
89
89
|
|
90
90
|
def initialize(tool)
|
91
|
-
raise
|
91
|
+
raise Error, "unknown extension inferrer #{tool.inspect}, supported inferrers are: #{SUPPORTED_TOOLS.join(",")}" unless SUPPORTED_TOOLS.include?(tool)
|
92
92
|
|
93
93
|
@tool = tool
|
94
94
|
end
|
@@ -103,13 +103,6 @@ class Shrine
|
|
103
103
|
|
104
104
|
private
|
105
105
|
|
106
|
-
def infer_with_rack_mime(mime_type)
|
107
|
-
require "rack/mime"
|
108
|
-
|
109
|
-
mime_types = Rack::Mime::MIME_TYPES
|
110
|
-
mime_types.key(mime_type)
|
111
|
-
end
|
112
|
-
|
113
106
|
def infer_with_mime_types(mime_type)
|
114
107
|
require "mime/types"
|
115
108
|
|
@@ -22,7 +22,7 @@ class Shrine
|
|
22
22
|
#
|
23
23
|
# :format
|
24
24
|
# : This allows you to change the logging output into something that may be
|
25
|
-
# easier to grep. Accepts `:human` (default), `:json` and `:
|
25
|
+
# easier to grep. Accepts `:human` (default), `:json` and `:logfmt`.
|
26
26
|
#
|
27
27
|
# :stream
|
28
28
|
# : The default logging stream is `$stdout`, but you may want to change it,
|
@@ -40,7 +40,7 @@ class Shrine
|
|
40
40
|
# plugin :logging, format: :json
|
41
41
|
# # {"action":"upload","phase":"cache","uploader":"ImageUploader","attachment":"avatar",...}
|
42
42
|
#
|
43
|
-
# plugin :logging, format: :
|
43
|
+
# plugin :logging, format: :logfmt
|
44
44
|
# # action=upload phase=cache uploader=ImageUploader attachment=avatar record_class=User ...
|
45
45
|
#
|
46
46
|
# Logging is by default disabled in tests, but you can enable it by setting
|
@@ -51,25 +51,28 @@ class Shrine
|
|
51
51
|
end
|
52
52
|
|
53
53
|
def self.configure(uploader, opts = {})
|
54
|
-
uploader.opts[:logging_logger] = opts.fetch(:logger, uploader.opts[:logging_logger])
|
55
54
|
uploader.opts[:logging_stream] = opts.fetch(:stream, uploader.opts.fetch(:logging_stream, $stdout))
|
55
|
+
uploader.opts[:logging_logger] = opts.fetch(:logger, uploader.opts.fetch(:logging_logger, uploader.create_logger))
|
56
56
|
uploader.opts[:logging_format] = opts.fetch(:format, uploader.opts.fetch(:logging_format, :human))
|
57
|
+
|
58
|
+
Shrine.deprecation("The :heroku logging format has been renamed to :logfmt. Using :heroku name will stop being supported in Shrine 3.") if uploader.opts[:logging_format] == :heroku
|
57
59
|
end
|
58
60
|
|
59
61
|
module ClassMethods
|
60
62
|
def logger=(logger)
|
61
|
-
@logger = logger
|
63
|
+
@logger = logger
|
62
64
|
end
|
63
65
|
|
64
|
-
# Initializes a new logger if it hasn't been initialized.
|
65
66
|
def logger
|
66
|
-
@logger ||= opts[:logging_logger]
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
67
|
+
@logger ||= opts[:logging_logger]
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_logger
|
71
|
+
logger = Logger.new(opts[:logging_stream])
|
72
|
+
logger.level = Logger::INFO
|
73
|
+
logger.level = Logger::WARN if ENV["RACK_ENV"] == "test"
|
74
|
+
logger.formatter = pretty_formatter
|
75
|
+
logger
|
73
76
|
end
|
74
77
|
|
75
78
|
# It makes logging preamble simpler than the default logger. Also, it
|
@@ -140,10 +143,11 @@ class Shrine
|
|
140
143
|
JSON.generate(data)
|
141
144
|
end
|
142
145
|
|
143
|
-
def
|
146
|
+
def _log_message_logfmt(data)
|
144
147
|
data[:files] = Array(data[:files]).join("-")
|
145
148
|
data.map { |key, value| "#{key}=#{value}" }.join(" ")
|
146
149
|
end
|
150
|
+
alias _log_message_heroku _log_message_logfmt # deprecated alias
|
147
151
|
|
148
152
|
# We may have one file, a hash of versions, or an array of files or
|
149
153
|
# hashes.
|
@@ -31,18 +31,28 @@ class Shrine
|
|
31
31
|
#
|
32
32
|
# An example of resizing an image using the [image_processing] library:
|
33
33
|
#
|
34
|
-
#
|
34
|
+
# require "image_processing/mini_magick"
|
35
35
|
#
|
36
36
|
# process(:store) do |io, context|
|
37
|
-
#
|
37
|
+
# original = io.download
|
38
|
+
#
|
39
|
+
# resized = ImageProcessing::MiniMagick
|
40
|
+
# .source(original)
|
41
|
+
# .resize_to_limit!(800, 800)
|
42
|
+
#
|
43
|
+
# original.close!
|
44
|
+
#
|
45
|
+
# resized
|
38
46
|
# end
|
39
47
|
#
|
40
48
|
# The declarations are additive and inheritable, so for the same action you
|
41
49
|
# can declare multiple blocks, and they will be performed in the same order,
|
42
50
|
# with output from previous block being the input to next.
|
43
51
|
#
|
44
|
-
#
|
45
|
-
#
|
52
|
+
# ## Manually Run Processing
|
53
|
+
#
|
54
|
+
# You can manually trigger the defined processing via the uploader by calling
|
55
|
+
# `#upload` or `#process` and setting `:action` to the name of your processing block:
|
46
56
|
#
|
47
57
|
# uploader.upload(file, action: :store) # process and upload
|
48
58
|
# uploader.process(file, action: :store) # only process
|
@@ -79,7 +79,7 @@ class Shrine
|
|
79
79
|
# plugin :infer_extension
|
80
80
|
#
|
81
81
|
# [Down]: https://github.com/janko-m/down
|
82
|
-
# [shrine-url]: https://github.com/
|
82
|
+
# [shrine-url]: https://github.com/shrinerb/shrine-url
|
83
83
|
module RemoteUrl
|
84
84
|
def self.configure(uploader, opts = {})
|
85
85
|
raise Error, "The :max_size option is required for remote_url plugin" if !opts.key?(:max_size) && !uploader.opts.key?(:remote_url_max_size)
|
@@ -64,8 +64,8 @@ class Shrine
|
|
64
64
|
attr_reader :algorithm, :format
|
65
65
|
|
66
66
|
def initialize(algorithm, format:)
|
67
|
-
raise
|
68
|
-
raise
|
67
|
+
raise Error, "unknown hash algorithm #{algorithm.inspect}, supported algorithms are: #{SUPPORTED_ALGORITHMS.join(",")}" unless SUPPORTED_ALGORITHMS.include?(algorithm)
|
68
|
+
raise Error, "unknown hash format #{format.inspect}, supported formats are: #{SUPPORTED_FORMATS.join(",")}" unless SUPPORTED_FORMATS.include?(format)
|
69
69
|
|
70
70
|
@algorithm = algorithm
|
71
71
|
@format = format
|
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
class Shrine
|
4
4
|
module Plugins
|
5
|
-
# The `store_dimensions` plugin extracts
|
6
|
-
#
|
7
|
-
# agains [image bombs].
|
5
|
+
# The `store_dimensions` plugin extracts dimensions of uploaded images and
|
6
|
+
# stores them into the metadata hash.
|
8
7
|
#
|
9
8
|
# plugin :store_dimensions
|
10
9
|
#
|
11
|
-
#
|
12
|
-
#
|
10
|
+
# The dimensions are stored as "width" and "height" metadata values on the
|
11
|
+
# Shrine::UploadedFile object. For convenience the plugin also adds
|
12
|
+
# `#width`, `#height` and `#dimensions` reader methods.
|
13
13
|
#
|
14
14
|
# image = uploader.upload(file)
|
15
15
|
#
|
@@ -21,18 +21,37 @@ class Shrine
|
|
21
21
|
# # or
|
22
22
|
# image.dimensions #=> [300, 500]
|
23
23
|
#
|
24
|
-
#
|
25
|
-
# built-in
|
26
|
-
# and height, or nil to signal that dimensions weren't extracted.
|
24
|
+
# By default the [fastimage] gem is used to extract dimensions. You can
|
25
|
+
# choose a different built-in analyzer via the `:analyzer` option:
|
27
26
|
#
|
28
|
-
#
|
27
|
+
# plugin :store_dimensions, analyzer: :mini_magick
|
28
|
+
#
|
29
|
+
# The following analyzers are supported:
|
30
|
+
#
|
31
|
+
# :fastimage
|
32
|
+
# : (Default). Uses the [FastImage] gem to extract dimensions from any IO
|
33
|
+
# object.
|
34
|
+
#
|
35
|
+
# :mini_magick
|
36
|
+
# : Uses the [MiniMagick] gem to extract dimensions from File objects. If
|
37
|
+
# non-file IO object is given it will be temporarily downloaded to disk.
|
38
|
+
#
|
39
|
+
# :ruby_vips
|
40
|
+
# : Uses the [ruby-vips] gem to extract dimensions from File objects. If
|
41
|
+
# non-file IO object is given it will be temporarily downloaded to disk.
|
42
|
+
#
|
43
|
+
# You can also create your own custom dimensions analyzer, where you can
|
44
|
+
# reuse any of the built-in analyzers. The analyzer is a lambda that
|
45
|
+
# accepts an IO object and returns width and height as a two-element array,
|
46
|
+
# or `nil` if dimensions could not be extracted.
|
29
47
|
#
|
30
48
|
# plugin :store_dimensions, analyzer: -> (io, analyzers) do
|
31
|
-
# dimensions
|
32
|
-
# dimensions
|
49
|
+
# dimensions = analyzers[:fastimage].call(io) # try extracting dimensions with FastImage
|
50
|
+
# dimensions ||= analyzers[:mini_magick].call(io) # otherwise fall back to MiniMagick
|
51
|
+
# dimensions
|
33
52
|
# end
|
34
53
|
#
|
35
|
-
# You can
|
54
|
+
# You can use methods for extracting the dimensions directly:
|
36
55
|
#
|
37
56
|
# # or YourUploader.extract_dimensions(io)
|
38
57
|
# Shrine.extract_dimensions(io) # calls the defined analyzer
|
@@ -42,8 +61,9 @@ class Shrine
|
|
42
61
|
# Shrine.dimensions_analyzers[:fastimage].call(io) # calls a built-in analyzer
|
43
62
|
# #=> [300, 400]
|
44
63
|
#
|
45
|
-
# [
|
46
|
-
# [
|
64
|
+
# [FastImage]: https://github.com/sdsykes/fastimage
|
65
|
+
# [MiniMagick]: https://github.com/minimagick/minimagick
|
66
|
+
# [ruby-vips]: https://github.com/jcupitt/ruby-vips
|
47
67
|
module StoreDimensions
|
48
68
|
def self.configure(uploader, opts = {})
|
49
69
|
uploader.opts[:dimensions_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:dimensions_analyzer, :fastimage))
|
@@ -54,7 +74,7 @@ class Shrine
|
|
54
74
|
# analyzer.
|
55
75
|
def extract_dimensions(io)
|
56
76
|
analyzer = opts[:dimensions_analyzer]
|
57
|
-
analyzer =
|
77
|
+
analyzer = dimensions_analyzer(analyzer) if analyzer.is_a?(Symbol)
|
58
78
|
args = [io, dimensions_analyzers].take(analyzer.arity.abs)
|
59
79
|
|
60
80
|
dimensions = analyzer.call(*args)
|
@@ -68,9 +88,14 @@ class Shrine
|
|
68
88
|
# IO object.
|
69
89
|
def dimensions_analyzers
|
70
90
|
@dimensions_analyzers ||= DimensionsAnalyzer::SUPPORTED_TOOLS.inject({}) do |hash, tool|
|
71
|
-
hash.merge!(tool =>
|
91
|
+
hash.merge!(tool => dimensions_analyzer(tool))
|
72
92
|
end
|
73
93
|
end
|
94
|
+
|
95
|
+
# Returns callable dimensions analyzer object.
|
96
|
+
def dimensions_analyzer(name)
|
97
|
+
DimensionsAnalyzer.new(name).method(:call)
|
98
|
+
end
|
74
99
|
end
|
75
100
|
|
76
101
|
module InstanceMethods
|
@@ -78,10 +103,7 @@ class Shrine
|
|
78
103
|
def extract_metadata(io, context)
|
79
104
|
width, height = extract_dimensions(io)
|
80
105
|
|
81
|
-
super.
|
82
|
-
"width" => width,
|
83
|
-
"height" => height,
|
84
|
-
)
|
106
|
+
super.merge!("width" => width, "height" => height)
|
85
107
|
end
|
86
108
|
|
87
109
|
private
|
@@ -112,10 +134,10 @@ class Shrine
|
|
112
134
|
end
|
113
135
|
|
114
136
|
class DimensionsAnalyzer
|
115
|
-
SUPPORTED_TOOLS = [:fastimage]
|
137
|
+
SUPPORTED_TOOLS = [:fastimage, :mini_magick, :ruby_vips]
|
116
138
|
|
117
139
|
def initialize(tool)
|
118
|
-
raise
|
140
|
+
raise Error, "unknown dimensions analyzer #{tool.inspect}, supported analyzers are: #{SUPPORTED_TOOLS.join(",")}" unless SUPPORTED_TOOLS.include?(tool)
|
119
141
|
|
120
142
|
@tool = tool
|
121
143
|
end
|
@@ -132,6 +154,27 @@ class Shrine
|
|
132
154
|
require "fastimage"
|
133
155
|
FastImage.size(io)
|
134
156
|
end
|
157
|
+
|
158
|
+
def extract_with_mini_magick(io)
|
159
|
+
require "mini_magick"
|
160
|
+
ensure_file(io) { |file| MiniMagick::Image.new(file.path).dimensions }
|
161
|
+
end
|
162
|
+
|
163
|
+
def extract_with_ruby_vips(io)
|
164
|
+
require "vips"
|
165
|
+
ensure_file(io) { |file| Vips::Image.new_from_file(file.path).size }
|
166
|
+
end
|
167
|
+
|
168
|
+
def ensure_file(io)
|
169
|
+
if io.respond_to?(:path)
|
170
|
+
yield io
|
171
|
+
else
|
172
|
+
Tempfile.create("shrine-store_dimensions") do |tempfile|
|
173
|
+
IO.copy_stream(io, tempfile.path)
|
174
|
+
yield tempfile
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
135
178
|
end
|
136
179
|
end
|
137
180
|
|
@@ -10,17 +10,21 @@ class Shrine
|
|
10
10
|
# Here is an example of processing image thumbnails using the
|
11
11
|
# [image_processing] gem:
|
12
12
|
#
|
13
|
-
#
|
13
|
+
# require "image_processing/mini_magick"
|
14
|
+
#
|
14
15
|
# plugin :processing
|
15
16
|
#
|
16
17
|
# process(:store) do |io, context|
|
17
18
|
# original = io.download
|
19
|
+
# pipeline = ImageProcessing::MiniMagick.source(original)
|
20
|
+
#
|
21
|
+
# size_800 = pipeline.resize_to_limit!(800, 800)
|
22
|
+
# size_500 = pipeline.resize_to_limit!(500, 500)
|
23
|
+
# size_300 = pipeline.resize_to_limit!(300, 300)
|
18
24
|
#
|
19
|
-
#
|
20
|
-
# size_500 = resize_to_limit(size_800, 500, 500)
|
21
|
-
# size_300 = resize_to_limit(size_500, 300, 300)
|
25
|
+
# original.close!
|
22
26
|
#
|
23
|
-
# {large: size_800, medium: size_500, small: size_300}
|
27
|
+
# { original: io, large: size_800, medium: size_500, small: size_300}
|
24
28
|
# end
|
25
29
|
#
|
26
30
|
# You probably want to load the `delete_raw` plugin to automatically
|
@@ -31,6 +35,7 @@ class Shrine
|
|
31
35
|
#
|
32
36
|
# user.avatar_data #=>
|
33
37
|
# # '{
|
38
|
+
# # "original": {"id":"0gsdf.jpg", "storage":"store", "metadata":{...}},
|
34
39
|
# # "large": {"id":"lg043.jpg", "storage":"store", "metadata":{...}},
|
35
40
|
# # "medium": {"id":"kd9fk.jpg", "storage":"store", "metadata":{...}},
|
36
41
|
# # "small": {"id":"932fl.jpg", "storage":"store", "metadata":{...}}
|
@@ -38,9 +43,10 @@ class Shrine
|
|
38
43
|
#
|
39
44
|
# user.avatar #=>
|
40
45
|
# # {
|
41
|
-
# # :
|
42
|
-
# # :
|
43
|
-
# # :
|
46
|
+
# # :original => #<Shrine::UploadedFile @data={"id"=>"0gsdf.jpg", ...}>,
|
47
|
+
# # :large => #<Shrine::UploadedFile @data={"id"=>"lg043.jpg", ...}>,
|
48
|
+
# # :medium => #<Shrine::UploadedFile @data={"id"=>"kd9fk.jpg", ...}>,
|
49
|
+
# # :small => #<Shrine::UploadedFile @data={"id"=>"932fl.jpg", ...}>,
|
44
50
|
# # }
|
45
51
|
#
|
46
52
|
# user.avatar[:medium] #=> #<Shrine::UploadedFile>
|
@@ -121,12 +127,13 @@ class Shrine
|
|
121
127
|
#
|
122
128
|
# ## Original file
|
123
129
|
#
|
124
|
-
#
|
125
|
-
#
|
130
|
+
# It's recommended to always keep the original file after processing
|
131
|
+
# versions, which you can do by adding the yielded `Shrine::UploadedFile`
|
132
|
+
# object as one of the versions, by convention named `:original`:
|
126
133
|
#
|
127
134
|
# process(:store) do |io, context|
|
128
135
|
# # processing thumbnail
|
129
|
-
# {original: io, thumbnail: thumbnail}
|
136
|
+
# { original: io, thumbnail: thumbnail }
|
130
137
|
# end
|
131
138
|
#
|
132
139
|
# If both temporary and permanent storage are Amazon S3, the cached original
|
@@ -147,6 +154,11 @@ class Shrine
|
|
147
154
|
# "/images/defaults/#{options[:version]}.jpg"
|
148
155
|
# end
|
149
156
|
#
|
157
|
+
# ## Re-create Versions
|
158
|
+
#
|
159
|
+
# If you want to re-create a single or all versions, refer to the [reprocessing versions] guide for details.
|
160
|
+
#
|
161
|
+
# [reprocessing versions]: http://shrinerb.com/rdoc/files/doc/regenerating_versions_md.html
|
150
162
|
# [image_processing]: https://github.com/janko-m/image_processing
|
151
163
|
module Versions
|
152
164
|
def self.load_dependencies(uploader, *)
|
@@ -126,8 +126,10 @@ class Shrine
|
|
126
126
|
|
127
127
|
# Copies the file into the given location.
|
128
128
|
def upload(io, id, shrine_metadata: {}, **upload_options)
|
129
|
-
IO.copy_stream(io, path!(id))
|
129
|
+
bytes_copied = IO.copy_stream(io, path!(id))
|
130
130
|
path(id).chmod(permissions) if permissions
|
131
|
+
|
132
|
+
shrine_metadata["size"] ||= bytes_copied
|
131
133
|
end
|
132
134
|
|
133
135
|
# Moves the file to the given location. This gets called by the `moving`
|
data/lib/shrine/storage/s3.rb
CHANGED
@@ -15,9 +15,11 @@ rescue LoadError => exception
|
|
15
15
|
raise exception
|
16
16
|
end
|
17
17
|
end
|
18
|
+
|
18
19
|
require "down/chunked_io"
|
19
20
|
require "uri"
|
20
21
|
require "cgi"
|
22
|
+
require "tempfile"
|
21
23
|
|
22
24
|
class Shrine
|
23
25
|
module Storage
|
@@ -172,6 +174,8 @@ class Shrine
|
|
172
174
|
# [Transfer Acceleration]: http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html
|
173
175
|
# [object lifecycle]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#put-instance_method
|
174
176
|
class S3
|
177
|
+
MIN_PART_SIZE = 5 * 1024 * 1024 # 5MB
|
178
|
+
|
175
179
|
attr_reader :client, :bucket, :prefix, :host, :upload_options
|
176
180
|
|
177
181
|
# Initializes a storage for uploading to S3.
|
@@ -248,11 +252,12 @@ class Shrine
|
|
248
252
|
if copyable?(io)
|
249
253
|
copy(io, id, **options)
|
250
254
|
else
|
251
|
-
put(io, id, **options)
|
255
|
+
bytes_uploaded = put(io, id, **options)
|
256
|
+
shrine_metadata["size"] ||= bytes_uploaded
|
252
257
|
end
|
253
258
|
end
|
254
259
|
|
255
|
-
# Downloads the file from S3, and returns a `Tempfile`.
|
260
|
+
# Downloads the file from S3, and returns a `Tempfile`. Any additional
|
256
261
|
# options are forwarded to [`Aws::S3::Object#get`].
|
257
262
|
#
|
258
263
|
# [`Aws::S3::Object#get`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Object.html#get-instance_method
|
@@ -396,11 +401,25 @@ class Shrine
|
|
396
401
|
# use `upload_file` for files because it can do multipart upload
|
397
402
|
options = { multipart_threshold: @multipart_threshold[:upload] }.merge!(options)
|
398
403
|
object(id).upload_file(path, **options)
|
404
|
+
File.size(path)
|
399
405
|
else
|
400
|
-
|
406
|
+
io.open if io.is_a?(UploadedFile)
|
407
|
+
|
408
|
+
if io.respond_to?(:size) && io.size
|
409
|
+
object(id).put(body: io, **options)
|
410
|
+
io.size
|
411
|
+
else
|
412
|
+
# IO has unknown size, so we have to use multipart upload
|
413
|
+
multipart_put(io, id, **options)
|
414
|
+
end
|
401
415
|
end
|
402
416
|
end
|
403
417
|
|
418
|
+
# Uploads the file to S3 using multipart upload.
|
419
|
+
def multipart_put(io, id, **options)
|
420
|
+
MultipartUploader.new(object(id)).upload(io, **options)
|
421
|
+
end
|
422
|
+
|
404
423
|
# The file is copyable if it's on S3 and on the same Amazon account.
|
405
424
|
def copyable?(io)
|
406
425
|
io.is_a?(UploadedFile) &&
|
@@ -425,6 +444,53 @@ class Shrine
|
|
425
444
|
CGI.escape(filename).gsub("+", " ")
|
426
445
|
end
|
427
446
|
end
|
447
|
+
|
448
|
+
# Uploads IO objects of unknown size using the multipart API.
|
449
|
+
class MultipartUploader
|
450
|
+
def initialize(object)
|
451
|
+
@object = object
|
452
|
+
end
|
453
|
+
|
454
|
+
# Initiates multipart upload, uploads IO content into multiple parts,
|
455
|
+
# and completes the multipart upload. If an exception is raised, the
|
456
|
+
# multipart upload is automatically aborted.
|
457
|
+
def upload(io, **options)
|
458
|
+
multipart_upload = @object.initiate_multipart_upload(**options)
|
459
|
+
|
460
|
+
parts = upload_parts(multipart_upload, io)
|
461
|
+
bytes_uploaded = parts.inject(0) { |size, part| size + part.delete(:size) }
|
462
|
+
|
463
|
+
multipart_upload.complete(multipart_upload: { parts: parts })
|
464
|
+
|
465
|
+
bytes_uploaded
|
466
|
+
rescue
|
467
|
+
multipart_upload.abort if multipart_upload
|
468
|
+
raise
|
469
|
+
end
|
470
|
+
|
471
|
+
# Uploads parts until the IO object has reached EOF.
|
472
|
+
def upload_parts(multipart_upload, io)
|
473
|
+
1.step.inject([]) do |parts, part_number|
|
474
|
+
parts << upload_part(multipart_upload, io, part_number)
|
475
|
+
break parts if io.eof?
|
476
|
+
parts
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Uploads at most 5MB of IO content into a single multipart part.
|
481
|
+
def upload_part(multipart_upload, io, part_number)
|
482
|
+
Tempfile.create("shrine-s3-part-#{part_number}") do |body|
|
483
|
+
multipart_part = multipart_upload.part(part_number)
|
484
|
+
|
485
|
+
IO.copy_stream(io, body, MIN_PART_SIZE)
|
486
|
+
body.rewind
|
487
|
+
|
488
|
+
response = multipart_part.upload(body: body)
|
489
|
+
|
490
|
+
{ part_number: part_number, size: body.size, etag: response.etag }
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
428
494
|
end
|
429
495
|
end
|
430
496
|
end
|
data/lib/shrine/version.rb
CHANGED
data/shrine.gemspec
CHANGED
@@ -18,7 +18,7 @@ tying them to record's lifecycle. It natively supports background jobs and
|
|
18
18
|
direct uploads for fully asynchronous user experience.
|
19
19
|
END
|
20
20
|
|
21
|
-
gem.homepage = "https://github.com/
|
21
|
+
gem.homepage = "https://github.com/shrinerb/shrine"
|
22
22
|
gem.authors = ["Janko Marohnić"]
|
23
23
|
gem.email = ["janko.marohnic@gmail.com"]
|
24
24
|
gem.license = "MIT"
|
@@ -42,6 +42,8 @@ direct uploads for fully asynchronous user experience.
|
|
42
42
|
gem.add_development_dependency "mime-types"
|
43
43
|
gem.add_development_dependency "mini_mime", "~> 1.0"
|
44
44
|
gem.add_development_dependency "fastimage"
|
45
|
+
gem.add_development_dependency "mini_magick", "~> 4.0"
|
46
|
+
gem.add_development_dependency "ruby-vips", "~> 2.0"
|
45
47
|
gem.add_development_dependency "aws-sdk-s3", "~> 1.2"
|
46
48
|
|
47
49
|
unless RUBY_ENGINE == "jruby" || ENV["CI"]
|
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: 2.
|
4
|
+
version: 2.10.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: 2018-
|
11
|
+
date: 2018-03-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: down
|
@@ -206,6 +206,34 @@ dependencies:
|
|
206
206
|
- - ">="
|
207
207
|
- !ruby/object:Gem::Version
|
208
208
|
version: '0'
|
209
|
+
- !ruby/object:Gem::Dependency
|
210
|
+
name: mini_magick
|
211
|
+
requirement: !ruby/object:Gem::Requirement
|
212
|
+
requirements:
|
213
|
+
- - "~>"
|
214
|
+
- !ruby/object:Gem::Version
|
215
|
+
version: '4.0'
|
216
|
+
type: :development
|
217
|
+
prerelease: false
|
218
|
+
version_requirements: !ruby/object:Gem::Requirement
|
219
|
+
requirements:
|
220
|
+
- - "~>"
|
221
|
+
- !ruby/object:Gem::Version
|
222
|
+
version: '4.0'
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: ruby-vips
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - "~>"
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '2.0'
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - "~>"
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '2.0'
|
209
237
|
- !ruby/object:Gem::Dependency
|
210
238
|
name: aws-sdk-s3
|
211
239
|
requirement: !ruby/object:Gem::Requirement
|
@@ -361,7 +389,7 @@ files:
|
|
361
389
|
- lib/shrine/storage/s3.rb
|
362
390
|
- lib/shrine/version.rb
|
363
391
|
- shrine.gemspec
|
364
|
-
homepage: https://github.com/
|
392
|
+
homepage: https://github.com/shrinerb/shrine
|
365
393
|
licenses:
|
366
394
|
- MIT
|
367
395
|
metadata: {}
|