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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3eaf93cef812b79947acfc7c7430ef325d9a491d
4
- data.tar.gz: 0f303e9d11e6068d7fea7d9ee82de5f3b0366ae4
3
+ metadata.gz: 5820e9c06eec70c66a52ff3cc099847651076e2e
4
+ data.tar.gz: 6328bfb5edf7cb31a0e6b8ebb8a0815483451314
5
5
  SHA512:
6
- metadata.gz: 4fe0e87ded5e804acfa29876e7b1f3fc777cfb8ba1fcb3f579d82f108db6df526e9fe1c606a9058ed6071985e94408cace9d0584656421d7daf83b16302617a6
7
- data.tar.gz: 98553462acfd41d1f24de296c82cbe12f4be7333693e2e4acd436416a524e7d348a0169248b7ea60d168462814e9e52d48a35afa6f642eb7dfc1592d72abfca7
6
+ metadata.gz: 41411c21937516fb928e686378c8b2d16a3a30ae9f1bf15c47010863a7d39c676c6a9ded87cad0a48f95108e1cc6dfc7e3cfab5363d79e73f3f14505fa05a9b9
7
+ data.tar.gz: 832a9a41216b0c2406b327bbb442aa0f82af69973afef87f7ee99abcc72ddde3d5634330f8b8cb9fc123589a171fd61cd2754079999d2e0d5d643f9ae493fe44
@@ -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/janko-m/shrine](https://github.com/janko-m/shrine)
12
- - Bugs: [github.com/janko-m/shrine/issues](https://github.com/janko-m/shrine/issues)
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.download #=> #<Tempfile>
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.open { |io| io.read }
244
- uploaded_file.delete
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", ">= 4.3.5"
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
- resize_to_limit!(io.download, 800, 800) { |cmd| cmd.auto_orient } # orient rotated images
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
- size_800 = resize_to_limit!(original, 800, 800) { |cmd| cmd.auto_orient } # orient rotated images
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 DocumentUploader < Shrine
695
+ class ImageUploader < Shrine
683
696
  Attacher.validate do
684
- errors << "has more than 3 pages" if get.metadata["pages"] > 3
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/janko-m/shrine#context
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/janko-m/shrine-cloudinary
1023
- [Imgix]: https://github.com/janko-m/shrine-imgix
1024
- [Uploadcare]: https://github.com/janko-m/shrine-uploadcare
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/janko-m/shrine-tus-demo
1030
- [shrine-url]: https://github.com/janko-m/shrine-url
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/janko-m/shrine/blob/master/CODE_OF_CONDUCT.md
1035
- [roda_demo]: https://github.com/janko-m/shrine/tree/master/demo
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/janko-m/shrine/wiki/Backgrounding-libraries
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/
@@ -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
- size_800 = resize_to_limit(io.download, 800, 800)
100
- size_500 = resize_to_limit(size_800, 500, 500)
101
- size_300 = resize_to_limit(size_500, 300, 300)
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(endnpoint: "https://s3-accelerate.amazonaws.com", **options)
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/janko-m/shrine/tree/master/demo
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/janko-m/shrine-fog
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/janko-m/shrine-fog
719
+ [`Shrine::Storage::Fog`]: https://github.com/shrinerb/shrine-fog
@@ -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/janko-m/shrine-cloudinary/blob/ca587c580ea0762992a2df33fd590c9a1e534905/test/test_helper.rb#L20-L27
229
+ [fake IO]: https://github.com/shrinerb/shrine-cloudinary/blob/ca587c580ea0762992a2df33fd590c9a1e534905/test/test_helper.rb#L20-L27
@@ -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 console] or via an [API
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/janko-m/shrine/tree/master/demo
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
@@ -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
- size_800 = resize_to_limit(io.download, 800, 800)
105
- size_500 = resize_to_limit(size_800, 500, 500)
106
- size_300 = resize_to_limit(size_500, 300, 300)
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
  ```
@@ -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
- size_800 = resize_to_limit(io.download, 800, 800)
75
- size_500 = resize_to_limit(size_800, 500, 500)
76
- size_300 = resize_to_limit(size_500, 300, 300)
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/janko-m/shrine-cloudinary
477
- [shrine-imgix]: https://github.com/janko-m/shrine-imgix
478
- [shrine-uploadcare]: https://github.com/janko-m/shrine-uploadcare
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/janko-m/shrine/tree/master/demo
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
@@ -169,7 +169,7 @@ class FakeIO
169
169
  end
170
170
 
171
171
  extend Forwardable
172
- delegate Shrine::IO_METHODS.keys => :@io
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
- Then you can open the Minio UI in the browser and create a new bucket. Once
259
- you've done that, all that's left to do is point aws-sdk-s3 to your Minio
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: "MINIO_ACCESS_KEY_ID",
265
- secret_access_key: "MINIO_SECRET_ACCESS_KEY",
266
- bucket: "MINIO_BUCKET",
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/janko-m/shrine-memory
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
@@ -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
- @io, @missing_methods = io, missing_methods
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 = IO_METHODS.select { |m, a| !io.respond_to?(m) }
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
- # Opens an IO object of the uploaded file for reading, yields it to
782
- # the block, and closes it after the block finishes. If opening without
783
- # a block, it returns an opened IO object for the uploaded file.
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
- # uploaded_file.open do |io|
786
- # puts io.read # prints the content of the file
787
- # end
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
- begin
807
- tempfile = Tempfile.new(["shrine", ".#{extension}"], binmode: true)
808
- open(*args) { |io| IO.copy_stream(io, tempfile.path) }
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 = mime_type_analyzers[analyzer] if analyzer.is_a?(Symbol)
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 => MimeTypeAnalyzer.new(tool).method(:call))
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 ArgumentError, "unsupported mime type analysis tool: #{tool}" unless SUPPORTED_TOOLS.include?(tool)
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 `Rack::Mime` will be used for
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: :mime_types
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, :rack_mime))
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 = extension_inferrers[inferrer] if inferrer.is_a?(Symbol)
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 => ExtensionInferrer.new(tool).method(:call))
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 = [:rack_mime, :mime_types, :mini_mime]
88
+ SUPPORTED_TOOLS = [:mime_types, :mini_mime]
89
89
 
90
90
  def initialize(tool)
91
- raise ArgumentError, "unsupported extension inferrer tool: #{tool}" unless SUPPORTED_TOOLS.include?(tool)
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 `:heroku`.
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: :heroku
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 || Logger.new(nil)
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
- logger = Logger.new(opts[:logging_stream])
68
- logger.level = Logger::INFO
69
- logger.level = Logger::WARN if ENV["RACK_ENV"] == "test"
70
- logger.formatter = pretty_formatter
71
- logger
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 _log_message_heroku(data)
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
- # include ImageProcessing::MiniMagick
34
+ # require "image_processing/mini_magick"
35
35
  #
36
36
  # process(:store) do |io, context|
37
- # resize_to_limit!(io.download, 800, 800)
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
- # You can manually trigger the defined processing via the uploader, you
45
- # just need to specify `:action` to the name of your processing block:
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/janko-m/shrine-url
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 ArgumentError, "hash algorithm not supported: #{algorithm}" unless SUPPORTED_ALGORITHMS.include?(algorithm)
68
- raise ArgumentError, "hash format not supported: #{format}" unless SUPPORTED_FORMATS.include?(format)
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 and stores dimensions of the
6
- # uploaded image using the [fastimage] gem, which has built-in protection
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
- # It adds "width" and "height" metadata values to Shrine::UploadedFile,
12
- # and creates `#width`, `#height` and `#dimensions` reader methods.
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
- # You can provide your own custom dimensions analyzer, and reuse any of the
25
- # built-in analyzers; you just need to return a two-element array of width
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
- # require "mini_magick"
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 = analyzers[:fastimage].call(io) # try extracting dimensions with FastImage
32
- # dimensions || MiniMagick::Image.new(io).dimensions # otherwise fall back to MiniMagick
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 also use methods for extracting the dimensions directly:
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
- # [fastimage]: https://github.com/sdsykes/fastimage
46
- # [image bombs]: https://www.bamsoftware.com/hacks/deflate.html
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 = dimensions_analyzers[analyzer] if analyzer.is_a?(Symbol)
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 => DimensionsAnalyzer.new(tool).method(:call))
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.update(
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 ArgumentError, "unsupported dimensions analysis tool: #{tool}" unless SUPPORTED_TOOLS.include?(tool)
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
- # include ImageProcessing::MiniMagick
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
- # size_800 = resize_to_limit!(original, 800, 800) { |cmd| cmd.auto_orient }
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
- # # :large => #<Shrine::UploadedFile @data={"id"=>"lg043.jpg", ...}>,
42
- # # :medium => #<Shrine::UploadedFile @data={"id"=>"kd9fk.jpg", ...}>,
43
- # # :small => #<Shrine::UploadedFile @data={"id"=>"932fl.jpg", ...}>,
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
- # If you want to keep the original file, you can include the original
125
- # `Shrine::UploadedFile` object as one of the versions:
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`
@@ -140,7 +140,7 @@ class Shrine
140
140
  end
141
141
 
142
142
  extend Forwardable
143
- delegate Shrine::IO_METHODS.keys => :@io
143
+ delegate %i[read rewind eof? close size] => :@io
144
144
  end
145
145
  end
146
146
  end
@@ -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`. And additional
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
- object(id).put(body: io, **options)
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
@@ -7,7 +7,7 @@ class Shrine
7
7
 
8
8
  module VERSION
9
9
  MAJOR = 2
10
- MINOR = 9
10
+ MINOR = 10
11
11
  TINY = 0
12
12
  PRE = nil
13
13
 
@@ -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/janko-m/shrine"
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.9.0
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-01-27 00:00:00.000000000 Z
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/janko-m/shrine
392
+ homepage: https://github.com/shrinerb/shrine
365
393
  licenses:
366
394
  - MIT
367
395
  metadata: {}