shrine 2.5.0 → 2.6.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.

Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -13
  3. data/doc/attacher.md +7 -6
  4. data/doc/carrierwave.md +19 -17
  5. data/doc/design.md +1 -1
  6. data/doc/direct_s3.md +8 -5
  7. data/doc/multiple_files.md +4 -4
  8. data/doc/paperclip.md +7 -6
  9. data/doc/refile.md +67 -4
  10. data/doc/securing_uploads.md +41 -25
  11. data/doc/testing.md +6 -15
  12. data/lib/shrine.rb +19 -10
  13. data/lib/shrine/plugins/activerecord.rb +4 -4
  14. data/lib/shrine/plugins/add_metadata.rb +7 -3
  15. data/lib/shrine/plugins/background_helpers.rb +1 -1
  16. data/lib/shrine/plugins/backgrounding.rb +19 -6
  17. data/lib/shrine/plugins/cached_attachment_data.rb +4 -4
  18. data/lib/shrine/plugins/data_uri.rb +105 -31
  19. data/lib/shrine/plugins/default_url.rb +1 -1
  20. data/lib/shrine/plugins/delete_raw.rb +7 -3
  21. data/lib/shrine/plugins/determine_mime_type.rb +96 -44
  22. data/lib/shrine/plugins/direct_upload.rb +3 -1
  23. data/lib/shrine/plugins/download_endpoint.rb +14 -5
  24. data/lib/shrine/plugins/logging.rb +4 -4
  25. data/lib/shrine/plugins/metadata_attributes.rb +61 -0
  26. data/lib/shrine/plugins/migration_helpers.rb +1 -1
  27. data/lib/shrine/plugins/rack_file.rb +54 -30
  28. data/lib/shrine/plugins/recache.rb +1 -1
  29. data/lib/shrine/plugins/refresh_metadata.rb +29 -0
  30. data/lib/shrine/plugins/remote_url.rb +26 -4
  31. data/lib/shrine/plugins/remove_invalid.rb +5 -4
  32. data/lib/shrine/plugins/restore_cached_data.rb +10 -13
  33. data/lib/shrine/plugins/sequel.rb +4 -4
  34. data/lib/shrine/plugins/signature.rb +146 -0
  35. data/lib/shrine/plugins/store_dimensions.rb +68 -24
  36. data/lib/shrine/plugins/validation_helpers.rb +48 -29
  37. data/lib/shrine/plugins/versions.rb +16 -8
  38. data/lib/shrine/storage/file_system.rb +27 -16
  39. data/lib/shrine/storage/s3.rb +99 -58
  40. data/lib/shrine/version.rb +1 -1
  41. data/shrine.gemspec +1 -1
  42. metadata +9 -6
@@ -20,7 +20,7 @@ class Shrine
20
20
  # Recaching will be automatically triggered in a "before save" callback,
21
21
  # but if you're using the attacher directly, you can call it manually:
22
22
  #
23
- # attacher.recache if attacher.attached?
23
+ # attacher.recache if attacher.changed?
24
24
  module Recache
25
25
  module AttacherMethods
26
26
  def save
@@ -0,0 +1,29 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The `refresh_metadata` plugin allows you to re-extract metadata from an
4
+ # uploaded file.
5
+ #
6
+ # plugin :refresh_metadata
7
+ #
8
+ # It provides `UploadedFile#refresh_metadata!` method, which calls
9
+ # `Shrine#extract_metadata` with the uploaded file opened for reading,
10
+ # and updates the existing metadata hash with the results.
11
+ #
12
+ # uploaded_file.refresh_metadata!
13
+ # uploaded_file.metadata # re-extracted metadata
14
+ #
15
+ # For remote storages this will make an HTTP request to open the file for
16
+ # reading, but only the portion of the file needed for extracting each
17
+ # metadata value will be downloaded.
18
+ module RefreshMetadata
19
+ module FileMethods
20
+ def refresh_metadata!(context = {})
21
+ refreshed_metadata = open { uploader.extract_metadata(self, context) }
22
+ metadata.merge!(refreshed_metadata)
23
+ end
24
+ end
25
+ end
26
+
27
+ register_plugin(:refresh_metadata, RefreshMetadata)
28
+ end
29
+ end
@@ -21,8 +21,12 @@ class Shrine
21
21
  # attacher.remote_url = "http://example.com/cool-image.png"
22
22
  #
23
23
  # The file will by default be downloaded using [Down], which is a wrapper
24
- # around the open-uri standard library. It's a good practice to limit the
25
- # maximum filesize of the remote file:
24
+ # around the `open-uri` standard library. Note that Down expects the given
25
+ # URL to be URI-encoded.
26
+ #
27
+ # ## Maximum size
28
+ #
29
+ # It's a good practice to limit the maximum filesize of the remote file:
26
30
  #
27
31
  # plugin :remote_url, max_size: 20*1024*1024 # 20 MB
28
32
  #
@@ -34,20 +38,38 @@ class Shrine
34
38
  #
35
39
  # plugin :remote_url, max_size: nil
36
40
  #
37
- # If you need to additionally customize how the file is downloaded, you can
38
- # override the `:downloader`:
41
+ # ## Custom downloader
42
+ #
43
+ # If you want to customize how the file is downloaded, you can override the
44
+ # `:downloader` parameter and provide your own implementation. For example,
45
+ # you can ensure the URL is URI-encoded (using the [Addressable] gem) before
46
+ # passing it to Down:
47
+ #
48
+ # require "down"
49
+ # require "addressable/uri"
39
50
  #
40
51
  # plugin :remote_url, max_size: 20*1024*1024, downloader: ->(url, max_size:) do
52
+ # url = Addressable::URI.encode(Addressable::URI.decode(url))
41
53
  # Down.download(url, max_size: max_size, max_redirects: 4, read_timeout: 3)
42
54
  # end
43
55
  #
56
+ # ## Errors
57
+ #
44
58
  # If download errors, the error is rescued and a validation error is added
45
59
  # equal to the error message. You can change the default error message:
46
60
  #
47
61
  # plugin :remote_url, error_message: "download failed"
48
62
  # plugin :remote_url, error_message: ->(url, error) { I18n.t("errors.download_failed") }
49
63
  #
64
+ # ## Background
65
+ #
66
+ # If you want the file to be downloaded from the URL in the background, you
67
+ # can use the [shrine-url] storage which allows you to assign a custom URL
68
+ # as cached file ID, and pair that with the `backgrounding` plugin.
69
+ #
50
70
  # [Down]: https://github.com/janko-m/down
71
+ # [Addressable]: https://github.com/sporkmonger/addressable
72
+ # [shrine-url]: https://github.com/janko-m/shrine-url
51
73
  module RemoteUrl
52
74
  def self.configure(uploader, opts = {})
53
75
  raise Error, "The :max_size option is required for remote_url plugin" if !opts.key?(:max_size) && !uploader.opts.key?(:remote_url_max_size)
@@ -1,8 +1,9 @@
1
1
  class Shrine
2
2
  module Plugins
3
- # The `remove_invalid` plugin automatically deletes a cached file if it was
4
- # invalid and deassigns it from the record. If there was a previous file
5
- # attached, it will be assigned back, otherwise `nil` will be assigned.
3
+ # The `remove_invalid` plugin automatically deletes a new assigned file if
4
+ # it was invalid and deassigns it from the record. If there was a previous
5
+ # file attached, it will be assigned back, otherwise no attachment will be
6
+ # assigned.
6
7
  #
7
8
  # plugin :remove_invalid
8
9
  module RemoveInvalid
@@ -10,7 +11,7 @@ class Shrine
10
11
  def validate
11
12
  super
12
13
  ensure
13
- if errors.any? && cached?
14
+ if errors.any? && changed?
14
15
  _delete(get, action: :validate)
15
16
  _set(@old)
16
17
  remove_instance_variable(:@old)
@@ -1,28 +1,25 @@
1
1
  class Shrine
2
2
  module Plugins
3
- # The `restore_cached_data` plugin re-extracts cached file's metadata on
4
- # assignment. This happens when an uploaded file is retained on validation
5
- # errors, or when assigning direct uploaded files. In both cases you
6
- # usually want to re-extract metadata on the server side, mainly to prevent
3
+ # The `restore_cached_data` plugin re-extracts metadata when assigning
4
+ # already cached files, i.e. when the attachment has been retained on
5
+ # validation errors or assigned from a direct upload. In both cases you may
6
+ # want to re-extract metadata on the server side, mainly to prevent
7
7
  # tempering, but also in case of direct uploads to obtain metadata that
8
8
  # couldn't be extracted on the client side.
9
9
  #
10
10
  # plugin :restore_cached_data
11
11
  #
12
- # This will give an opened `UploadedFile` for metadata extraction. For
13
- # remote storages this will make an HTTP request, and since metadata is
14
- # typically found in the beginning of the file, Shrine will download only
15
- # the amount of bytes necessary for extracting the metadata.
12
+ # It uses the `refresh_metadata` plugin to re-extract metadata.
16
13
  module RestoreCachedData
14
+ def self.load_dependencies(uploader, *)
15
+ uploader.plugin :refresh_metadata
16
+ end
17
+
17
18
  module AttacherMethods
18
19
  private
19
20
 
20
21
  def assign_cached(cached_file)
21
- uploaded_file(cached_file) do |file|
22
- real_metadata = file.open { cache.extract_metadata(file, context) }
23
- file.metadata.update(real_metadata)
24
- end
25
-
22
+ uploaded_file(cached_file) { |file| file.refresh_metadata!(context) }
26
23
  super(cached_file)
27
24
  end
28
25
  end
@@ -22,7 +22,7 @@ class Shrine
22
22
  # saves again with a stored attachment, you can detect this in callbacks:
23
23
  #
24
24
  # class User < Sequel::Model
25
- # include ImageUploader[:avatar]
25
+ # include ImageUploader::Attachment.new(:avatar)
26
26
  #
27
27
  # def before_save
28
28
  # super
@@ -48,7 +48,7 @@ class Shrine
48
48
  # attachment, you can do it directly on the model.
49
49
  #
50
50
  # class User < Sequel::Model
51
- # include ImageUploader[:avatar]
51
+ # include ImageUploader::Attachment.new(:avatar)
52
52
  # validates_presence_of :avatar
53
53
  # end
54
54
  #
@@ -82,12 +82,12 @@ class Shrine
82
82
  module_eval <<-RUBY, __FILE__, __LINE__ + 1 if opts[:sequel_callbacks]
83
83
  def before_save
84
84
  super
85
- #{@name}_attacher.save if #{@name}_attacher.attached?
85
+ #{@name}_attacher.save if #{@name}_attacher.changed?
86
86
  end
87
87
 
88
88
  def after_save
89
89
  super
90
- db.after_commit{#{@name}_attacher.finalize} if #{@name}_attacher.attached?
90
+ db.after_commit{#{@name}_attacher.finalize} if #{@name}_attacher.changed?
91
91
  end
92
92
 
93
93
  def after_destroy
@@ -0,0 +1,146 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The `signature` plugin provides the ability to calculate a hash from file
4
+ # content. The hash can then be used as a checksum or just as a unique
5
+ # signature of the file.
6
+ #
7
+ # plugin :signature
8
+ #
9
+ # The plugin adds a `calculate_signature` instance and class method to
10
+ # `Shrine`, which accepts the IO object and hashing algorithm and returns
11
+ # the calculated hash.
12
+ #
13
+ # Shrine.calculate_signature(io, :md5)
14
+ # #=> "9a0364b9e99bb480dd25e1f0284c8555"
15
+ #
16
+ # You can then use the `add_metadata` plugin to add a new metadata field
17
+ # with the calculated hash.
18
+ #
19
+ # plugin :add_metadata
20
+ #
21
+ # add_metadata :md5 do |io, context|
22
+ # calculate_signature(io, :md5)
23
+ # end
24
+ #
25
+ # This will generate a hash for each uploaded file, but if you want to
26
+ # generate one only for the original file, you can add a conditional:
27
+ #
28
+ # add_metadata :md5 do |io, context|
29
+ # calculate_signature(io, :md5) if context[:action] == :cache
30
+ # end
31
+ #
32
+ # The following hashing algorithms are supported:
33
+ #
34
+ # * `sha1`
35
+ # * `sha256`
36
+ # * `sha384`
37
+ # * `sha512`
38
+ # * `md5`
39
+ # * `crc32`
40
+ #
41
+ # You can also choose which format will the calculated hash be encoded in:
42
+ #
43
+ # Shrine.calculate_signature(io, :md5, format: :hex)
44
+ #
45
+ # The following encoding formats are supported:
46
+ #
47
+ # * `none`
48
+ # * `hex` (default)
49
+ # * `base64`
50
+ module Signature
51
+ module ClassMethods
52
+ # Calculates `algorithm` hash of the contents of the IO object, and
53
+ # encodes it into `format`.
54
+ def calculate_signature(io, algorithm, format: :hex)
55
+ algorithm = algorithm.downcase # support uppercase algorithm names like :MD5
56
+ SignatureCalculator.new(algorithm, format: format).call(io)
57
+ end
58
+ end
59
+
60
+ module InstanceMethods
61
+ # Calculates `algorithm` hash of the contents of the IO object, and
62
+ # encodes it into `format`.
63
+ def calculate_signature(io, algorithm, format: :hex)
64
+ self.class.calculate_signature(io, algorithm, format: format)
65
+ end
66
+ end
67
+
68
+ class SignatureCalculator
69
+ SUPPORTED_ALGORITHMS = [:sha1, :sha256, :sha384, :sha512, :md5, :crc32]
70
+ SUPPORTED_FORMATS = [:none, :hex, :base64]
71
+
72
+ attr_reader :algorithm, :format
73
+
74
+ def initialize(algorithm, format:)
75
+ raise ArgumentError, "hash algorithm not supported: #{algorithm}" unless SUPPORTED_ALGORITHMS.include?(algorithm)
76
+ raise ArgumentError, "hash format not supported: #{format}" unless SUPPORTED_FORMATS.include?(format)
77
+
78
+ @algorithm = algorithm
79
+ @format = format
80
+ end
81
+
82
+ def call(io)
83
+ hash = send(:"calculate_#{algorithm}", io)
84
+ io.rewind
85
+
86
+ encoded_hash = send(:"encode_#{format}", hash)
87
+ encoded_hash
88
+ end
89
+
90
+ private
91
+
92
+ def calculate_sha1(io)
93
+ calculate_digest(:SHA1, io)
94
+ end
95
+
96
+ def calculate_sha256(io)
97
+ calculate_digest(:SHA256, io)
98
+ end
99
+
100
+ def calculate_sha384(io)
101
+ calculate_digest(:SHA384, io)
102
+ end
103
+
104
+ def calculate_sha512(io)
105
+ calculate_digest(:SHA512, io)
106
+ end
107
+
108
+ def calculate_md5(io)
109
+ calculate_digest(:MD5, io)
110
+ end
111
+
112
+ def calculate_crc32(io)
113
+ require "zlib"
114
+ crc = 0
115
+ crc = Zlib.crc32(io.read(16*1024, buffer ||= ""), crc) until io.eof?
116
+ crc.to_s
117
+ end
118
+
119
+ def calculate_digest(name, io)
120
+ require "digest"
121
+ digest = Digest.const_get(name).new
122
+ digest.update(io.read(16*1024, buffer ||= "")) until io.eof?
123
+ digest.digest
124
+ end
125
+
126
+ def encode_none(hash)
127
+ hash
128
+ end
129
+
130
+ def encode_hex(hash)
131
+ hash.unpack("H*").first
132
+ end
133
+
134
+ def encode_base64(hash)
135
+ require "base64"
136
+ Base64.encode64(hash)
137
+ end
138
+ end
139
+
140
+ SUPPORTED_ALGORITHMS = SignatureCalculator::SUPPORTED_ALGORITHMS
141
+ SUPPORTED_FORMATS = SignatureCalculator::SUPPORTED_FORMATS
142
+ end
143
+
144
+ register_plugin(:signature, Signature)
145
+ end
146
+ end
@@ -1,7 +1,8 @@
1
1
  class Shrine
2
2
  module Plugins
3
3
  # The `store_dimensions` plugin extracts and stores dimensions of the
4
- # uploaded image using the [fastimage] gem.
4
+ # uploaded image using the [fastimage] gem, which has built-in protection
5
+ # agains [image bombs].
5
6
  #
6
7
  # plugin :store_dimensions
7
8
  #
@@ -18,15 +19,27 @@ class Shrine
18
19
  # # or
19
20
  # image.dimensions #=> [300, 500]
20
21
  #
21
- # The fastimage gem has built-in protection against [image bombs]. However,
22
- # if for some reason it doesn't suit your needs, you can provide a custom
23
- # `:analyzer`:
22
+ # You can provide your own custom dimensions analyzer, and reuse any of the
23
+ # built-in analyzers; you just need to return a two-element array of width
24
+ # and height, or nil to signal that dimensions weren't extracted.
25
+ #
26
+ # require "mini_magick"
24
27
  #
25
28
  # plugin :store_dimensions, analyzer: ->(io, analyzers) do
26
29
  # dimensions = analyzers[:fastimage].call(io)
27
30
  # dimensions || MiniMagick::Image.new(io).dimensions
28
31
  # end
29
32
  #
33
+ # You can also use methods for extracting the dimensions directly:
34
+ #
35
+ # # or YourUploader.extract_dimensions(io)
36
+ # Shrine.extract_dimensions(io) # calls the defined analyzer
37
+ # #=> [300, 400]
38
+ #
39
+ # # or YourUploader.dimensions_analyzers
40
+ # Shrine.dimensions_analyzers[:fastimage].call(io) # calls a built-in analyzer
41
+ # #=> [300, 400]
42
+ #
30
43
  # [fastimage]: https://github.com/sdsykes/fastimage
31
44
  # [image bombs]: https://www.bamsoftware.com/hacks/deflate.html
32
45
  module StoreDimensions
@@ -34,6 +47,30 @@ class Shrine
34
47
  uploader.opts[:dimensions_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:dimensions_analyzer, :fastimage))
35
48
  end
36
49
 
50
+ module ClassMethods
51
+ # Determines the dimensions of the IO object by calling the specified
52
+ # analyzer.
53
+ def extract_dimensions(io)
54
+ analyzer = opts[:dimensions_analyzer]
55
+ analyzer = dimensions_analyzers[analyzer] if analyzer.is_a?(Symbol)
56
+ args = [io, dimensions_analyzers].take(analyzer.arity.abs)
57
+
58
+ dimensions = analyzer.call(*args)
59
+ io.rewind
60
+
61
+ dimensions
62
+ end
63
+
64
+ # Returns a hash of built-in dimensions analyzers, where keys are
65
+ # analyzer names and values are `#call`-able objects which accepts the
66
+ # IO object.
67
+ def dimensions_analyzers
68
+ @dimensions_analyzers ||= DimensionsAnalyzer::SUPPORTED_TOOLS.inject({}) do |hash, tool|
69
+ hash.merge!(tool => DimensionsAnalyzer.new(tool).method(:call))
70
+ end
71
+ end
72
+ end
73
+
37
74
  module InstanceMethods
38
75
  # We update the metadata with "width" and "height".
39
76
  def extract_metadata(io, context)
@@ -47,30 +84,14 @@ class Shrine
47
84
 
48
85
  private
49
86
 
50
- # If the `io` is an uploaded file, copies its dimensions, otherwise
51
- # calls the predefined or custom analyzer.
87
+ # Extracts dimensions using the specified analyzer.
52
88
  def extract_dimensions(io)
53
- analyzer = opts[:dimensions_analyzer]
54
- analyzer = dimensions_analyzers[analyzer] if analyzer.is_a?(Symbol)
55
- args = [io, dimensions_analyzers].take(analyzer.arity.abs)
56
-
57
- dimensions = analyzer.call(*args)
58
- io.rewind
59
-
60
- dimensions
89
+ self.class.extract_dimensions(io)
61
90
  end
62
91
 
92
+ # Returns a hash of built-in dimensions analyzers.
63
93
  def dimensions_analyzers
64
- Hash.new { |hash, key| method(:"_extract_dimensions_with_#{key}") }
65
- end
66
-
67
- def _extract_dimensions_with_fastimage(io)
68
- require "fastimage"
69
-
70
- dimensions = FastImage.size(io)
71
- io.rewind
72
-
73
- dimensions
94
+ self.class.dimensions_analyzers
74
95
  end
75
96
  end
76
97
 
@@ -87,6 +108,29 @@ class Shrine
87
108
  [width, height] if width || height
88
109
  end
89
110
  end
111
+
112
+ class DimensionsAnalyzer
113
+ SUPPORTED_TOOLS = [:fastimage]
114
+
115
+ def initialize(tool)
116
+ raise ArgumentError, "unsupported mime type analyzer tool: #{tool}" unless SUPPORTED_TOOLS.include?(tool)
117
+
118
+ @tool = tool
119
+ end
120
+
121
+ def call(io)
122
+ dimensions = send(:"extract_with_#{@tool}", io)
123
+ io.rewind
124
+ dimensions
125
+ end
126
+
127
+ private
128
+
129
+ def extract_with_fastimage(io)
130
+ require "fastimage"
131
+ FastImage.size(io)
132
+ end
133
+ end
90
134
  end
91
135
 
92
136
  register_plugin(:store_dimensions, StoreDimensions)