shrine 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -0
- data/README.md +20 -16
- data/doc/creating_storages.md +0 -21
- data/doc/design.md +1 -0
- data/doc/direct_s3.md +26 -15
- data/doc/metadata.md +67 -22
- data/doc/multiple_files.md +3 -3
- data/doc/processing.md +1 -1
- data/doc/retrieving_uploads.md +184 -0
- data/lib/shrine.rb +268 -900
- data/lib/shrine/attacher.rb +271 -0
- data/lib/shrine/attachment.rb +97 -0
- data/lib/shrine/plugins.rb +29 -0
- data/lib/shrine/plugins/_urlsafe_serialization.rb +182 -0
- data/lib/shrine/plugins/activerecord.rb +16 -14
- data/lib/shrine/plugins/add_metadata.rb +58 -24
- data/lib/shrine/plugins/backgrounding.rb +6 -1
- data/lib/shrine/plugins/cached_attachment_data.rb +9 -9
- data/lib/shrine/plugins/copy.rb +12 -8
- data/lib/shrine/plugins/data_uri.rb +23 -20
- data/lib/shrine/plugins/default_url_options.rb +5 -4
- data/lib/shrine/plugins/determine_mime_type.rb +24 -23
- data/lib/shrine/plugins/download_endpoint.rb +61 -73
- data/lib/shrine/plugins/migration_helpers.rb +17 -17
- data/lib/shrine/plugins/module_include.rb +9 -8
- data/lib/shrine/plugins/presign_endpoint.rb +13 -7
- data/lib/shrine/plugins/processing.rb +1 -1
- data/lib/shrine/plugins/rack_response.rb +128 -36
- data/lib/shrine/plugins/refresh_metadata.rb +20 -5
- data/lib/shrine/plugins/remote_url.rb +8 -8
- data/lib/shrine/plugins/remove_attachment.rb +9 -9
- data/lib/shrine/plugins/sequel.rb +21 -18
- data/lib/shrine/plugins/tempfile.rb +68 -0
- data/lib/shrine/plugins/upload_endpoint.rb +3 -2
- data/lib/shrine/plugins/upload_options.rb +7 -6
- data/lib/shrine/plugins/validation_helpers.rb +2 -1
- data/lib/shrine/storage/file_system.rb +20 -17
- data/lib/shrine/storage/linter.rb +0 -7
- data/lib/shrine/storage/s3.rb +159 -50
- data/lib/shrine/uploaded_file.rb +258 -0
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +7 -19
- metadata +41 -21
| @@ -65,27 +65,27 @@ class Shrine | |
| 65 65 |  | 
| 66 66 | 
             
                      return if shrine_class.opts[:migration_helpers_delegate] == false
         | 
| 67 67 |  | 
| 68 | 
            -
                       | 
| 69 | 
            -
                        def update_#{name}(&block)
         | 
| 70 | 
            -
                          #{name}_attacher.update_stored(&block)
         | 
| 71 | 
            -
                        end
         | 
| 68 | 
            +
                      name = attachment_name
         | 
| 72 69 |  | 
| 73 | 
            -
             | 
| 74 | 
            -
             | 
| 75 | 
            -
             | 
| 70 | 
            +
                      define_method :"update_#{name}" do |&block|
         | 
| 71 | 
            +
                        send(:"#{name}_attacher").update_stored(&block)
         | 
| 72 | 
            +
                      end
         | 
| 76 73 |  | 
| 77 | 
            -
             | 
| 78 | 
            -
             | 
| 79 | 
            -
             | 
| 74 | 
            +
                      define_method :"#{name}_cache" do
         | 
| 75 | 
            +
                        send(:"#{name}_attacher").cache
         | 
| 76 | 
            +
                      end
         | 
| 80 77 |  | 
| 81 | 
            -
             | 
| 82 | 
            -
             | 
| 83 | 
            -
             | 
| 78 | 
            +
                      define_method :"#{name}_store" do
         | 
| 79 | 
            +
                        send(:"#{name}_attacher").store
         | 
| 80 | 
            +
                      end
         | 
| 84 81 |  | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 82 | 
            +
                      define_method :"#{name}_cached?" do
         | 
| 83 | 
            +
                        send(:"#{name}_attacher").cached?
         | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                      define_method :"#{name}_stored?" do
         | 
| 87 | 
            +
                        send(:"#{name}_attacher").stored?
         | 
| 88 | 
            +
                      end
         | 
| 89 89 | 
             
                    end
         | 
| 90 90 | 
             
                  end
         | 
| 91 91 |  | 
| @@ -19,15 +19,16 @@ class Shrine | |
| 19 19 | 
             
                #       def included(model)
         | 
| 20 20 | 
             
                #         super
         | 
| 21 21 | 
             
                #
         | 
| 22 | 
            -
                #          | 
| 23 | 
            -
                # | 
| 24 | 
            -
                # | 
| 25 | 
            -
                # | 
| 26 | 
            -
                # | 
| 27 | 
            -
                # | 
| 28 | 
            -
                # | 
| 22 | 
            +
                #         name = attachment_name
         | 
| 23 | 
            +
                #
         | 
| 24 | 
            +
                #         define_method :"#{name}_size" do |version|
         | 
| 25 | 
            +
                #           attachment = send(name)
         | 
| 26 | 
            +
                #           if attachment.is_a?(Hash)
         | 
| 27 | 
            +
                #             attachment[version].size
         | 
| 28 | 
            +
                #           elsif attachment
         | 
| 29 | 
            +
                #             attachment.size
         | 
| 29 30 | 
             
                #           end
         | 
| 30 | 
            -
                #          | 
| 31 | 
            +
                #         end
         | 
| 31 32 | 
             
                #       end
         | 
| 32 33 | 
             
                #     end
         | 
| 33 34 | 
             
                #
         | 
| @@ -75,16 +75,20 @@ class Shrine | |
| 75 75 | 
             
                # `:presign_options`, here is an example for S3 storage:
         | 
| 76 76 | 
             
                #
         | 
| 77 77 | 
             
                #     plugin :presign_endpoint, presign_options: -> (request) do
         | 
| 78 | 
            -
                #        | 
| 79 | 
            -
                #        | 
| 78 | 
            +
                #       # Uppy will send the "filename" and "type" query parameters
         | 
| 79 | 
            +
                #       filename = request.params["filename"]
         | 
| 80 | 
            +
                #       type     = request.params["type"]
         | 
| 80 81 | 
             
                #
         | 
| 81 82 | 
             
                #       {
         | 
| 82 | 
            -
                #         content_length_range: 0..(10*1024*1024), | 
| 83 | 
            -
                #         content_disposition:  | 
| 84 | 
            -
                #         content_type:        type, | 
| 83 | 
            +
                #         content_length_range: 0..(10*1024*1024),                  # limit filesize to 10MB
         | 
| 84 | 
            +
                #         content_disposition: ContentDisposition.inline(filename), # download with original filename
         | 
| 85 | 
            +
                #         content_type:        type,                                # set correct content type
         | 
| 85 86 | 
             
                #       }
         | 
| 86 87 | 
             
                #     end
         | 
| 87 88 | 
             
                #
         | 
| 89 | 
            +
                # The example above uses the [content_disposition] gem to correctly format
         | 
| 90 | 
            +
                # the `Content-Disposition` header value.
         | 
| 91 | 
            +
                #
         | 
| 88 92 | 
             
                # The `:presign_options` can be a Proc or a Hash.
         | 
| 89 93 | 
             
                #
         | 
| 90 94 | 
             
                # ## Presign
         | 
| @@ -116,6 +120,7 @@ class Shrine | |
| 116 120 | 
             
                # [Amazon S3]: https://aws.amazon.com/s3/
         | 
| 117 121 | 
             
                # [Google Cloud Storage]: https://cloud.google.com/storage/
         | 
| 118 122 | 
             
                # [Microsoft Azure Storage]: https://azure.microsoft.com/en-us/services/storage/
         | 
| 123 | 
            +
                # [content_disposition]: https://github.com/shrinerb/content_disposition
         | 
| 119 124 | 
             
                module PresignEndpoint
         | 
| 120 125 | 
             
                  def self.configure(uploader, opts = {})
         | 
| 121 126 | 
             
                    uploader.opts[:presign_endpoint_presign_location] = opts.fetch(:presign_location, uploader.opts[:presign_endpoint_presign_location])
         | 
| @@ -133,14 +138,15 @@ class Shrine | |
| 133 138 | 
             
                    # Additional options can be given to override the options given on
         | 
| 134 139 | 
             
                    # plugin initialization.
         | 
| 135 140 | 
             
                    def presign_endpoint(storage_key, **options)
         | 
| 136 | 
            -
                      App.new( | 
| 141 | 
            +
                      App.new(
         | 
| 137 142 | 
             
                        shrine_class:     self,
         | 
| 138 143 | 
             
                        storage_key:      storage_key,
         | 
| 139 144 | 
             
                        presign_location: opts[:presign_endpoint_presign_location],
         | 
| 140 145 | 
             
                        presign_options:  opts[:presign_endpoint_presign_options],
         | 
| 141 146 | 
             
                        presign:          opts[:presign_endpoint_presign],
         | 
| 142 147 | 
             
                        rack_response:    opts[:presign_endpoint_rack_response],
         | 
| 143 | 
            -
             | 
| 148 | 
            +
                        **options
         | 
| 149 | 
            +
                      )
         | 
| 144 150 | 
             
                    end
         | 
| 145 151 | 
             
                  end
         | 
| 146 152 |  | 
| @@ -1,6 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            require "rack"
         | 
| 4 | 
            +
            require "content_disposition"
         | 
| 4 5 |  | 
| 5 6 | 
             
            class Shrine
         | 
| 6 7 | 
             
              module Plugins
         | 
| @@ -29,27 +30,52 @@ class Shrine | |
| 29 30 | 
             
                #     class FilesController < ActionController::Base
         | 
| 30 31 | 
             
                #       def download
         | 
| 31 32 | 
             
                #         # ...
         | 
| 32 | 
            -
                #          | 
| 33 | 
            +
                #         set_rack_response record.attachment.to_rack_response
         | 
| 34 | 
            +
                #       end
         | 
| 35 | 
            +
                #
         | 
| 36 | 
            +
                #       private
         | 
| 33 37 | 
             
                #
         | 
| 34 | 
            -
                # | 
| 35 | 
            -
                #          | 
| 36 | 
            -
                #         self. | 
| 38 | 
            +
                #       def set_rack_response((status, headers, body))
         | 
| 39 | 
            +
                #         self.status = status
         | 
| 40 | 
            +
                #         self.headers.merge!(headers)
         | 
| 41 | 
            +
                #         self.response_body = body
         | 
| 37 42 | 
             
                #       end
         | 
| 38 43 | 
             
                #     end
         | 
| 39 44 | 
             
                #
         | 
| 45 | 
            +
                # The `#each` method on the response body object will stream the uploaded
         | 
| 46 | 
            +
                # file directly from the storage. It also works with [Rack::Sendfile] when
         | 
| 47 | 
            +
                # using `FileSystem` storage.
         | 
| 48 | 
            +
                #
         | 
| 49 | 
            +
                # ## Type
         | 
| 50 | 
            +
                #
         | 
| 51 | 
            +
                # The response `Content-Type` header will default to the value of the
         | 
| 52 | 
            +
                # `mime_type` metadata. A custom content type can be provided via the
         | 
| 53 | 
            +
                # `:type` option:
         | 
| 54 | 
            +
                #
         | 
| 55 | 
            +
                #     _, headers, _ uploaded_file.to_rack_response(type: "text/plain; charset=utf-8")
         | 
| 56 | 
            +
                #     headers["Content-Type"] #=> "text/plain; charset=utf-8"
         | 
| 57 | 
            +
                #
         | 
| 58 | 
            +
                # ## Filename
         | 
| 59 | 
            +
                #
         | 
| 60 | 
            +
                # The download filename in the `Content-Disposition` header will default to
         | 
| 61 | 
            +
                # the value of the `filename` metadata. A custom download filename can be
         | 
| 62 | 
            +
                # provided via the `:filename` option:
         | 
| 63 | 
            +
                #
         | 
| 64 | 
            +
                #     _, headers, _ uploaded_file.to_rack_response(filename: "my-filename.txt")
         | 
| 65 | 
            +
                #     headers["Content-Disposition"] #=> "inline; filename=\"my-filename.txt\""
         | 
| 66 | 
            +
                #
         | 
| 40 67 | 
             
                # ## Disposition
         | 
| 41 68 | 
             
                #
         | 
| 42 | 
            -
                #  | 
| 43 | 
            -
                #  | 
| 44 | 
            -
                # file to be rendered inside the browser:
         | 
| 69 | 
            +
                # The default disposition in the "Content-Disposition" header is `inline`,
         | 
| 70 | 
            +
                # but it can be changed via the `:disposition` option:
         | 
| 45 71 | 
             
                #
         | 
| 46 | 
            -
                #      | 
| 72 | 
            +
                #     _, headers, _ = uploaded_file.to_rack_response(disposition: "attachment")
         | 
| 47 73 | 
             
                #     headers["Content-Disposition"] #=> "attachment; filename=\"file.txt\""
         | 
| 48 74 | 
             
                #
         | 
| 49 75 | 
             
                # ## Range
         | 
| 50 76 | 
             
                #
         | 
| 51 77 | 
             
                # [Partial responses][range requests] are also supported via the `:range`
         | 
| 52 | 
            -
                #  | 
| 78 | 
            +
                # option, which accepts a value of the `Range` request header.
         | 
| 53 79 | 
             
                #
         | 
| 54 80 | 
             
                #     env["HTTP_RANGE"] #=> "bytes=100-200"
         | 
| 55 81 | 
             
                #     status, headers, body = uploaded_file.to_rack_response(range: env["HTTP_RANGE"])
         | 
| @@ -59,35 +85,55 @@ class Shrine | |
| 59 85 | 
             
                #     body                      # partial content
         | 
| 60 86 | 
             
                #
         | 
| 61 87 | 
             
                # [range requests]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests
         | 
| 88 | 
            +
                # [Rack::Sendfile]: https://www.rubydoc.info/github/rack/rack/Rack/Sendfile
         | 
| 62 89 | 
             
                module RackResponse
         | 
| 63 90 | 
             
                  module FileMethods
         | 
| 64 91 | 
             
                    # Returns a Rack response triple for the uploaded file.
         | 
| 65 | 
            -
                    def to_rack_response( | 
| 66 | 
            -
                       | 
| 92 | 
            +
                    def to_rack_response(**options)
         | 
| 93 | 
            +
                      FileResponse.new(self).call(**options)
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  class FileResponse
         | 
| 98 | 
            +
                    attr_reader :file
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                    def initialize(file)
         | 
| 101 | 
            +
                      @file = file
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    # Returns a Rack response triple for the uploaded file.
         | 
| 105 | 
            +
                    def call(**options)
         | 
| 106 | 
            +
                      options[:range] = parse_content_range(options[:range]) if options[:range]
         | 
| 67 107 |  | 
| 68 | 
            -
                      status  =  | 
| 69 | 
            -
                      headers = rack_headers( | 
| 70 | 
            -
                      body    = rack_body( | 
| 108 | 
            +
                      status  = rack_status(**options)
         | 
| 109 | 
            +
                      headers = rack_headers(**options)
         | 
| 110 | 
            +
                      body    = rack_body(**options)
         | 
| 71 111 |  | 
| 72 112 | 
             
                      [status, headers, body]
         | 
| 73 113 | 
             
                    end
         | 
| 74 114 |  | 
| 75 115 | 
             
                    private
         | 
| 76 116 |  | 
| 117 | 
            +
                    # Returns "200 OK" on full request, and "206 Partial Content" on ranged
         | 
| 118 | 
            +
                    # request.
         | 
| 119 | 
            +
                    def rack_status(range: nil, **)
         | 
| 120 | 
            +
                      range ? 206 : 200
         | 
| 121 | 
            +
                    end
         | 
| 122 | 
            +
             | 
| 77 123 | 
             
                    # Returns a hash of "Content-Length", "Content-Type" and
         | 
| 78 124 | 
             
                    # "Content-Disposition" headers, whose values are extracted from
         | 
| 79 125 | 
             
                    # metadata. Also returns the correct "Content-Range" header on ranged
         | 
| 80 126 | 
             
                    # requests.
         | 
| 81 | 
            -
                    def rack_headers(disposition | 
| 82 | 
            -
                      length | 
| 83 | 
            -
                      type      | 
| 84 | 
            -
                      filename  | 
| 127 | 
            +
                    def rack_headers(filename: nil, type: nil, disposition: "inline", range: false)
         | 
| 128 | 
            +
                      length     = range ? range.size : size
         | 
| 129 | 
            +
                      type     ||= @file.mime_type || Rack::Mime.mime_type(".#{@file.extension}")
         | 
| 130 | 
            +
                      filename ||= @file.original_filename || @file.id.split("/").last
         | 
| 85 131 |  | 
| 86 132 | 
             
                      headers = {}
         | 
| 87 133 | 
             
                      headers["Content-Length"]      = length.to_s if length
         | 
| 88 134 | 
             
                      headers["Content-Type"]        = type
         | 
| 89 | 
            -
                      headers["Content-Disposition"] =  | 
| 90 | 
            -
                      headers["Content-Range"]       = "bytes #{range.begin}-#{range.end}/#{size | 
| 135 | 
            +
                      headers["Content-Disposition"] = content_disposition(disposition: disposition, filename: filename)
         | 
| 136 | 
            +
                      headers["Content-Range"]       = "bytes #{range.begin}-#{range.end}/#{size}" if range
         | 
| 91 137 | 
             
                      headers["Accept-Ranges"]       = "bytes" unless range == false
         | 
| 92 138 |  | 
| 93 139 | 
             
                      headers
         | 
| @@ -95,19 +141,69 @@ class Shrine | |
| 95 141 |  | 
| 96 142 | 
             
                    # Returns an object that responds to #each and #close, which yields
         | 
| 97 143 | 
             
                    # contents of the file.
         | 
| 98 | 
            -
                    def rack_body(range: nil)
         | 
| 144 | 
            +
                    def rack_body(range: nil, **)
         | 
| 145 | 
            +
                      FileBody.new(file, range: range)
         | 
| 146 | 
            +
                    end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                    # Parses the value of a "Range" HTTP header.
         | 
| 149 | 
            +
                    def parse_content_range(range_header)
         | 
| 150 | 
            +
                      if Rack.release >= "2.0"
         | 
| 151 | 
            +
                        ranges = Rack::Utils.get_byte_ranges(range_header, size)
         | 
| 152 | 
            +
                      else
         | 
| 153 | 
            +
                        ranges = Rack::Utils.byte_ranges({"HTTP_RANGE" => range_header}, size)
         | 
| 154 | 
            +
                      end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                      ranges.first if ranges && ranges.one?
         | 
| 157 | 
            +
                    end
         | 
| 158 | 
            +
             | 
| 159 | 
            +
                    def content_disposition(disposition:, filename:)
         | 
| 160 | 
            +
                      ContentDisposition.format(disposition: disposition, filename: filename)
         | 
| 161 | 
            +
                    end
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                    # Read size from metadata, otherwise retrieve the size from the storage.
         | 
| 164 | 
            +
                    def size
         | 
| 165 | 
            +
                      @file.size || @file.to_io.size
         | 
| 166 | 
            +
                    end
         | 
| 167 | 
            +
                  end
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  # Implements the interface of a Rack response body object.
         | 
| 170 | 
            +
                  class FileBody
         | 
| 171 | 
            +
                    attr_reader :file, :range
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                    def initialize(file, range: nil)
         | 
| 174 | 
            +
                      @file  = file
         | 
| 175 | 
            +
                      @range = range
         | 
| 176 | 
            +
                    end
         | 
| 177 | 
            +
             | 
| 178 | 
            +
                    # Streams the uploaded file directly from the storage.
         | 
| 179 | 
            +
                    def each(&block)
         | 
| 99 180 | 
             
                      if range
         | 
| 100 | 
            -
                         | 
| 181 | 
            +
                        read_partial_chunks(&block)
         | 
| 101 182 | 
             
                      else
         | 
| 102 | 
            -
                         | 
| 183 | 
            +
                        read_chunks(&block)
         | 
| 103 184 | 
             
                      end
         | 
| 185 | 
            +
                    end
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                    # Closes the file when response body is closed by the web server.
         | 
| 188 | 
            +
                    def close
         | 
| 189 | 
            +
                      file.close
         | 
| 190 | 
            +
                    end
         | 
| 104 191 |  | 
| 105 | 
            -
             | 
| 192 | 
            +
                    # Rack::Sendfile is activated when response body responds to #to_path.
         | 
| 193 | 
            +
                    def respond_to_missing?(name, include_private = false)
         | 
| 194 | 
            +
                      name == :to_path && path
         | 
| 106 195 | 
             
                    end
         | 
| 107 196 |  | 
| 197 | 
            +
                    # Rack::Sendfile is activated when response body responds to #to_path.
         | 
| 198 | 
            +
                    def method_missing(name, *args, &block)
         | 
| 199 | 
            +
                      name == :to_path && path or super
         | 
| 200 | 
            +
                    end
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                    private
         | 
| 203 | 
            +
             | 
| 108 204 | 
             
                    # Yields reasonably sized chunks of uploaded file's partial content
         | 
| 109 205 | 
             
                    # specified by the given index range.
         | 
| 110 | 
            -
                    def read_partial_chunks | 
| 206 | 
            +
                    def read_partial_chunks
         | 
| 111 207 | 
             
                      bytes_read = 0
         | 
| 112 208 |  | 
| 113 209 | 
             
                      read_chunks do |chunk|
         | 
| @@ -130,22 +226,18 @@ class Shrine | |
| 130 226 |  | 
| 131 227 | 
             
                    # Yields reasonably sized chunks of uploaded file's content.
         | 
| 132 228 | 
             
                    def read_chunks
         | 
| 133 | 
            -
                      if  | 
| 134 | 
            -
                         | 
| 229 | 
            +
                      if file.to_io.respond_to?(:each_chunk) # Down::ChunkedIO
         | 
| 230 | 
            +
                        file.to_io.each_chunk { |chunk| yield chunk }
         | 
| 135 231 | 
             
                      else
         | 
| 136 | 
            -
                        yield  | 
| 232 | 
            +
                        yield file.read(16*1024) until file.eof?
         | 
| 137 233 | 
             
                      end
         | 
| 138 234 | 
             
                    end
         | 
| 139 235 |  | 
| 140 | 
            -
                    #  | 
| 141 | 
            -
                    def  | 
| 142 | 
            -
                      if  | 
| 143 | 
            -
                         | 
| 144 | 
            -
                      else
         | 
| 145 | 
            -
                        ranges = Rack::Utils.byte_ranges({"HTTP_RANGE" => range_header}, size || io.size)
         | 
| 236 | 
            +
                    # Returns actual path on disk when FileSystem storage is used.
         | 
| 237 | 
            +
                    def path
         | 
| 238 | 
            +
                      if defined?(Storage::FileSystem) && file.storage.is_a?(Storage::FileSystem)
         | 
| 239 | 
            +
                        file.storage.path(file.id)
         | 
| 146 240 | 
             
                      end
         | 
| 147 | 
            -
             | 
| 148 | 
            -
                      ranges.first if ranges && ranges.one?
         | 
| 149 241 | 
             
                    end
         | 
| 150 242 | 
             
                  end
         | 
| 151 243 | 
             
                end
         | 
| @@ -7,9 +7,10 @@ class Shrine | |
| 7 7 | 
             
                #
         | 
| 8 8 | 
             
                #     plugin :refresh_metadata
         | 
| 9 9 | 
             
                #
         | 
| 10 | 
            -
                # It provides `UploadedFile#refresh_metadata!` method, which  | 
| 11 | 
            -
                # `Shrine#extract_metadata` with the uploaded | 
| 12 | 
            -
                # and updates the existing metadata hash with the | 
| 10 | 
            +
                # It provides `UploadedFile#refresh_metadata!` method, which triggers
         | 
| 11 | 
            +
                # metadata extraction (calls `Shrine#extract_metadata`) with the uploaded
         | 
| 12 | 
            +
                # file opened for reading, and updates the existing metadata hash with the
         | 
| 13 | 
            +
                # results.
         | 
| 13 14 | 
             
                #
         | 
| 14 15 | 
             
                #     uploaded_file.refresh_metadata!
         | 
| 15 16 | 
             
                #     uploaded_file.metadata # re-extracted metadata
         | 
| @@ -17,11 +18,25 @@ class Shrine | |
| 17 18 | 
             
                # For remote storages this will make an HTTP request to open the file for
         | 
| 18 19 | 
             
                # reading, but only the portion of the file needed for extracting each
         | 
| 19 20 | 
             
                # metadata value will be downloaded.
         | 
| 21 | 
            +
                #
         | 
| 22 | 
            +
                # If the uploaded file is already open, it is passed to metadata extraction
         | 
| 23 | 
            +
                # as is.
         | 
| 24 | 
            +
                #
         | 
| 25 | 
            +
                #     uploaded_file.open do
         | 
| 26 | 
            +
                #       uploaded_file.refresh_metadata!
         | 
| 27 | 
            +
                #       # ...
         | 
| 28 | 
            +
                #     end
         | 
| 20 29 | 
             
                module RefreshMetadata
         | 
| 21 30 | 
             
                  module FileMethods
         | 
| 22 31 | 
             
                    def refresh_metadata!(context = {})
         | 
| 23 | 
            -
                      refreshed_metadata = | 
| 24 | 
            -
             | 
| 32 | 
            +
                      refreshed_metadata =
         | 
| 33 | 
            +
                        if @io
         | 
| 34 | 
            +
                          uploader.extract_metadata(self, context)
         | 
| 35 | 
            +
                        else
         | 
| 36 | 
            +
                          open { uploader.extract_metadata(self, context) }
         | 
| 37 | 
            +
                        end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                      @data = @data.merge("metadata" => metadata.merge(refreshed_metadata))
         | 
| 25 40 | 
             
                    end
         | 
| 26 41 | 
             
                  end
         | 
| 27 42 | 
             
                end
         | 
| @@ -102,15 +102,15 @@ class Shrine | |
| 102 102 | 
             
                    def initialize(*)
         | 
| 103 103 | 
             
                      super
         | 
| 104 104 |  | 
| 105 | 
            -
                       | 
| 106 | 
            -
                        def #{@name}_remote_url=(url)
         | 
| 107 | 
            -
                          #{@name}_attacher.remote_url = url
         | 
| 108 | 
            -
                        end
         | 
| 105 | 
            +
                      name = attachment_name
         | 
| 109 106 |  | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 107 | 
            +
                      define_method :"#{name}_remote_url=" do |url|
         | 
| 108 | 
            +
                        send(:"#{name}_attacher").remote_url = url
         | 
| 109 | 
            +
                      end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                      define_method :"#{name}_remote_url" do
         | 
| 112 | 
            +
                        send(:"#{name}_attacher").remote_url
         | 
| 113 | 
            +
                      end
         | 
| 114 114 | 
             
                    end
         | 
| 115 115 | 
             
                  end
         | 
| 116 116 |  | 
| @@ -17,15 +17,15 @@ class Shrine | |
| 17 17 | 
             
                    def initialize(*)
         | 
| 18 18 | 
             
                      super
         | 
| 19 19 |  | 
| 20 | 
            -
                       | 
| 21 | 
            -
             | 
| 22 | 
            -
             | 
| 23 | 
            -
                         | 
| 24 | 
            -
             | 
| 25 | 
            -
             | 
| 26 | 
            -
             | 
| 27 | 
            -
                         | 
| 28 | 
            -
                       | 
| 20 | 
            +
                      name = attachment_name
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                      define_method :"remove_#{name}=" do |value|
         | 
| 23 | 
            +
                        send(:"#{name}_attacher").remove = value
         | 
| 24 | 
            +
                      end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      define_method :"remove_#{name}" do
         | 
| 27 | 
            +
                        send(:"#{name}_attacher").remove
         | 
| 28 | 
            +
                      end
         | 
| 29 29 | 
             
                    end
         | 
| 30 30 | 
             
                  end
         | 
| 31 31 |  |