activestorage 5.2.8.1 → 6.0.0.beta1
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 activestorage might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -157
- data/MIT-LICENSE +1 -1
- data/README.md +6 -5
- data/app/assets/javascripts/activestorage.js +4 -1
- data/app/controllers/active_storage/base_controller.rb +3 -5
- data/app/controllers/active_storage/blobs_controller.rb +1 -1
- data/app/controllers/active_storage/disk_controller.rb +4 -1
- data/app/controllers/active_storage/representations_controller.rb +1 -1
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/javascript/activestorage/blob_record.js +6 -1
- data/app/jobs/active_storage/analyze_job.rb +4 -0
- data/app/jobs/active_storage/base_job.rb +0 -1
- data/app/jobs/active_storage/purge_job.rb +3 -0
- data/app/models/active_storage/attachment.rb +18 -9
- data/app/models/active_storage/blob/representable.rb +5 -5
- data/app/models/active_storage/blob.rb +63 -22
- data/app/models/active_storage/filename.rb +0 -6
- data/app/models/active_storage/preview.rb +3 -3
- data/app/models/active_storage/variant.rb +51 -52
- data/app/models/active_storage/variation.rb +23 -92
- data/config/routes.rb +13 -12
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +7 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +2 -4
- data/lib/active_storage/analyzer.rb +9 -4
- data/lib/active_storage/attached/changes/create_many.rb +46 -0
- data/lib/active_storage/attached/changes/create_one.rb +68 -0
- data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
- data/lib/active_storage/attached/changes/delete_many.rb +23 -0
- data/lib/active_storage/attached/changes/delete_one.rb +19 -0
- data/lib/active_storage/attached/changes.rb +16 -0
- data/lib/active_storage/attached/many.rb +16 -10
- data/lib/active_storage/attached/model.rb +140 -0
- data/lib/active_storage/attached/one.rb +16 -19
- data/lib/active_storage/attached.rb +7 -22
- data/lib/active_storage/downloader.rb +44 -0
- data/lib/active_storage/downloading.rb +8 -0
- data/lib/active_storage/engine.rb +36 -21
- data/lib/active_storage/errors.rb +22 -3
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
- data/lib/active_storage/previewer/video_previewer.rb +2 -3
- data/lib/active_storage/previewer.rb +21 -11
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service/azure_storage_service.rb +30 -14
- data/lib/active_storage/service/configurator.rb +3 -1
- data/lib/active_storage/service/disk_service.rb +20 -16
- data/lib/active_storage/service/gcs_service.rb +48 -46
- data/lib/active_storage/service/mirror_service.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +10 -9
- data/lib/active_storage/service.rb +5 -6
- data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
- data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
- data/lib/active_storage/transformers/transformer.rb +42 -0
- data/lib/active_storage.rb +13 -292
- data/lib/tasks/activestorage.rake +7 -0
- metadata +31 -19
- data/app/models/active_storage/filename/parameters.rb +0 -36
- data/lib/active_storage/attached/macros.rb +0 -110
| @@ -1,5 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require "active_storage/downloader"
         | 
| 4 | 
            +
             | 
| 3 5 | 
             
            # A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
         | 
| 4 6 | 
             
            # Blobs can be created in two ways:
         | 
| 5 7 | 
             
            #
         | 
| @@ -38,7 +40,7 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 38 40 | 
             
              end
         | 
| 39 41 |  | 
| 40 42 | 
             
              class << self
         | 
| 41 | 
            -
                # You can  | 
| 43 | 
            +
                # You can use the signed ID of a blob to refer to it on the client side without fear of tampering.
         | 
| 42 44 | 
             
                # This is particularly helpful for direct uploads where the client-side needs to refer to the blob
         | 
| 43 45 | 
             
                # that was created ahead of the upload itself on form submission.
         | 
| 44 46 | 
             
                #
         | 
| @@ -48,21 +50,25 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 48 50 | 
             
                end
         | 
| 49 51 |  | 
| 50 52 | 
             
                # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service.
         | 
| 51 | 
            -
                 | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 54 | 
            -
                    blob. | 
| 55 | 
            -
             | 
| 53 | 
            +
                # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
         | 
| 54 | 
            +
                def build_after_upload(io:, filename:, content_type: nil, metadata: nil, identify: true)
         | 
| 55 | 
            +
                  new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
         | 
| 56 | 
            +
                    blob.upload(io, identify: identify)
         | 
| 57 | 
            +
                  end
         | 
| 58 | 
            +
                end
         | 
| 56 59 |  | 
| 57 | 
            -
             | 
| 60 | 
            +
                def build_after_unfurling(io:, filename:, content_type: nil, metadata: nil, identify: true) #:nodoc:
         | 
| 61 | 
            +
                  new(filename: filename, content_type: content_type, metadata: metadata).tap do |blob|
         | 
| 62 | 
            +
                    blob.unfurl(io, identify: identify)
         | 
| 58 63 | 
             
                  end
         | 
| 59 64 | 
             
                end
         | 
| 60 65 |  | 
| 61 66 | 
             
                # Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built,
         | 
| 62 67 | 
             
                # then the +io+ is uploaded, then the blob is saved. This is done this way to avoid uploading (which may take
         | 
| 63 68 | 
             
                # time), while having an open database transaction.
         | 
| 64 | 
            -
                 | 
| 65 | 
            -
             | 
| 69 | 
            +
                # When providing a content type, pass <tt>identify: false</tt> to bypass automatic content type inference.
         | 
| 70 | 
            +
                def create_after_upload!(io:, filename:, content_type: nil, metadata: nil, identify: true)
         | 
| 71 | 
            +
                  build_after_upload(io: io, filename: filename, content_type: content_type, metadata: metadata, identify: identify).tap(&:save!)
         | 
| 66 72 | 
             
                end
         | 
| 67 73 |  | 
| 68 74 | 
             
                # Returns a saved blob _without_ uploading a file to the service. This blob will point to a key where there is
         | 
| @@ -73,6 +79,15 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 73 79 | 
             
                def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil)
         | 
| 74 80 | 
             
                  create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata
         | 
| 75 81 | 
             
                end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                # To prevent problems with case-insensitive filesystems, especially in combination
         | 
| 84 | 
            +
                # with databases which treat indices as case-sensitive, all blob keys generated are going
         | 
| 85 | 
            +
                # to only contain the base-36 character alphabet and will therefore be lowercase. To maintain
         | 
| 86 | 
            +
                # the same or higher amount of entropy as in the base-58 encoding used by `has_secure_token`
         | 
| 87 | 
            +
                # the number of bytes used is increased to 28 from the standard 24
         | 
| 88 | 
            +
                def generate_unique_secure_token
         | 
| 89 | 
            +
                  SecureRandom.base36(28)
         | 
| 90 | 
            +
                end
         | 
| 76 91 | 
             
              end
         | 
| 77 92 |  | 
| 78 93 | 
             
              # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
         | 
| @@ -81,9 +96,10 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 81 96 | 
             
                ActiveStorage.verifier.generate(id, purpose: :blob_id)
         | 
| 82 97 | 
             
              end
         | 
| 83 98 |  | 
| 84 | 
            -
              # Returns the key pointing to the file on the service that's associated with this blob. The key is  | 
| 85 | 
            -
              #  | 
| 86 | 
            -
              # to be revealed directly to the user. | 
| 99 | 
            +
              # Returns the key pointing to the file on the service that's associated with this blob. The key is the
         | 
| 100 | 
            +
              # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
         | 
| 101 | 
            +
              # This key is not intended to be revealed directly to the user.
         | 
| 102 | 
            +
              # Always refer to blobs using the signed_id or a verified form of the key.
         | 
| 87 103 | 
             
              def key
         | 
| 88 104 | 
             
                # We can't wait until the record is first saved to have a key for it
         | 
| 89 105 | 
             
                self[:key] ||= self.class.generate_unique_secure_token
         | 
| @@ -121,7 +137,7 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 121 137 | 
             
              # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
         | 
| 122 138 | 
             
              # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
         | 
| 123 139 | 
             
              # it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
         | 
| 124 | 
            -
              def service_url(expires_in:  | 
| 140 | 
            +
              def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline, filename: nil, **options)
         | 
| 125 141 | 
             
                filename = ActiveStorage::Filename.wrap(filename || self.filename)
         | 
| 126 142 |  | 
| 127 143 | 
             
                service.url key, expires_in: expires_in, filename: filename, content_type: content_type_for_service_url,
         | 
| @@ -130,7 +146,7 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 130 146 |  | 
| 131 147 | 
             
              # Returns a URL that can be used to directly upload a file for this blob on the service. This URL is intended to be
         | 
| 132 148 | 
             
              # short-lived for security and only generated on-demand by the client-side JavaScript responsible for doing the uploading.
         | 
| 133 | 
            -
              def service_url_for_direct_upload(expires_in:  | 
| 149 | 
            +
              def service_url_for_direct_upload(expires_in: ActiveStorage.service_urls_expire_in)
         | 
| 134 150 | 
             
                service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum
         | 
| 135 151 | 
             
              end
         | 
| 136 152 |  | 
| @@ -146,16 +162,24 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 146 162 | 
             
              #
         | 
| 147 163 | 
             
              # Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the
         | 
| 148 164 | 
             
              # checksum does not match what the service receives, an exception will be raised. We also measure the size of the +io+
         | 
| 149 | 
            -
              # and store that in +byte_size+ on the blob record.
         | 
| 165 | 
            +
              # and store that in +byte_size+ on the blob record. The content type is automatically extracted from the +io+ unless
         | 
| 166 | 
            +
              # you specify a +content_type+ and pass +identify+ as false.
         | 
| 150 167 | 
             
              #
         | 
| 151 168 | 
             
              # Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+
         | 
| 152 169 | 
             
              # and +create_after_upload!+.
         | 
| 153 | 
            -
              def upload(io)
         | 
| 170 | 
            +
              def upload(io, identify: true)
         | 
| 171 | 
            +
                unfurl io, identify: identify
         | 
| 172 | 
            +
                upload_without_unfurling io
         | 
| 173 | 
            +
              end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
              def unfurl(io, identify: true) #:nodoc:
         | 
| 154 176 | 
             
                self.checksum     = compute_checksum_in_chunks(io)
         | 
| 155 | 
            -
                self.content_type = extract_content_type(io)
         | 
| 177 | 
            +
                self.content_type = extract_content_type(io) if content_type.nil? || identify
         | 
| 156 178 | 
             
                self.byte_size    = io.size
         | 
| 157 179 | 
             
                self.identified   = true
         | 
| 180 | 
            +
              end
         | 
| 158 181 |  | 
| 182 | 
            +
              def upload_without_unfurling(io) #:nodoc:
         | 
| 159 183 | 
             
                service.upload key, io, checksum: checksum, **service_metadata
         | 
| 160 184 | 
             
              end
         | 
| 161 185 |  | 
| @@ -165,9 +189,26 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 165 189 | 
             
                service.download key, &block
         | 
| 166 190 | 
             
              end
         | 
| 167 191 |  | 
| 192 | 
            +
              # Downloads the blob to a tempfile on disk. Yields the tempfile.
         | 
| 193 | 
            +
              #
         | 
| 194 | 
            +
              # The tempfile's name is prefixed with +ActiveStorage-+ and the blob's ID. Its extension matches that of the blob.
         | 
| 195 | 
            +
              #
         | 
| 196 | 
            +
              # By default, the tempfile is created in <tt>Dir.tmpdir</tt>. Pass +tempdir:+ to create it in a different directory:
         | 
| 197 | 
            +
              #
         | 
| 198 | 
            +
              #   blob.open(tempdir: "/path/to/tmp") do |file|
         | 
| 199 | 
            +
              #     # ...
         | 
| 200 | 
            +
              #   end
         | 
| 201 | 
            +
              #
         | 
| 202 | 
            +
              # The tempfile is automatically closed and unlinked after the given block is executed.
         | 
| 203 | 
            +
              #
         | 
| 204 | 
            +
              # Raises ActiveStorage::IntegrityError if the downloaded data does not match the blob's checksum.
         | 
| 205 | 
            +
              def open(tempdir: nil, &block)
         | 
| 206 | 
            +
                ActiveStorage::Downloader.new(self, tempdir: tempdir).download_blob_to_tempfile(&block)
         | 
| 207 | 
            +
              end
         | 
| 208 | 
            +
             | 
| 168 209 |  | 
| 169 | 
            -
              # Deletes the  | 
| 170 | 
            -
              # deleted as well or you will essentially have a dead reference. It's recommended to use  | 
| 210 | 
            +
              # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
         | 
| 211 | 
            +
              # deleted as well or you will essentially have a dead reference. It's recommended to use #purge and #purge_later
         | 
| 171 212 | 
             
              # methods in most circumstances.
         | 
| 172 213 | 
             
              def delete
         | 
| 173 214 | 
             
                service.delete(key)
         | 
| @@ -176,15 +217,15 @@ class ActiveStorage::Blob < ActiveRecord::Base | |
| 176 217 |  | 
| 177 218 | 
             
              # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted
         | 
| 178 219 | 
             
              # blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may
         | 
| 179 | 
            -
              # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use  | 
| 220 | 
            +
              # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use #purge_later instead.
         | 
| 180 221 | 
             
              def purge
         | 
| 181 222 | 
             
                destroy
         | 
| 182 223 | 
             
                delete
         | 
| 183 224 | 
             
              rescue ActiveRecord::InvalidForeignKey
         | 
| 184 225 | 
             
              end
         | 
| 185 226 |  | 
| 186 | 
            -
              # Enqueues an ActiveStorage::PurgeJob  | 
| 187 | 
            -
              #  | 
| 227 | 
            +
              # Enqueues an ActiveStorage::PurgeJob to call #purge. This is the recommended way to purge blobs from a transaction,
         | 
| 228 | 
            +
              # an Active Record callback, or in any other real-time scenario.
         | 
| 188 229 | 
             
              def purge_later
         | 
| 189 230 | 
             
                ActiveStorage::PurgeJob.perform_later(self)
         | 
| 190 231 | 
             
              end
         | 
| @@ -3,8 +3,6 @@ | |
| 3 3 | 
             
            # Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
         | 
| 4 4 | 
             
            # A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
         | 
| 5 5 | 
             
            class ActiveStorage::Filename
         | 
| 6 | 
            -
              require_dependency "active_storage/filename/parameters"
         | 
| 7 | 
            -
             | 
| 8 6 | 
             
              include Comparable
         | 
| 9 7 |  | 
| 10 8 | 
             
              class << self
         | 
| @@ -60,10 +58,6 @@ class ActiveStorage::Filename | |
| 60 58 | 
             
                @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
         | 
| 61 59 | 
             
              end
         | 
| 62 60 |  | 
| 63 | 
            -
              def parameters #:nodoc:
         | 
| 64 | 
            -
                Parameters.new self
         | 
| 65 | 
            -
              end
         | 
| 66 | 
            -
             | 
| 67 61 | 
             
              # Returns the sanitized version of the filename.
         | 
| 68 62 | 
             
              def to_s
         | 
| 69 63 | 
             
                sanitized.to_s
         | 
| @@ -22,8 +22,8 @@ | |
| 22 22 | 
             
            # Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
         | 
| 23 23 | 
             
            #
         | 
| 24 24 | 
             
            # The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires
         | 
| 25 | 
            -
            # { | 
| 26 | 
            -
            # and the other requires { | 
| 25 | 
            +
            # {FFmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org],
         | 
| 26 | 
            +
            # and the other requires {muPDF}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or muPDF.
         | 
| 27 27 | 
             
            #
         | 
| 28 28 | 
             
            # These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
         | 
| 29 29 | 
             
            # install and use third-party software, make sure you understand the licensing implications of doing so.
         | 
| @@ -38,7 +38,7 @@ class ActiveStorage::Preview | |
| 38 38 |  | 
| 39 39 | 
             
              # Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
         | 
| 40 40 | 
             
              #
         | 
| 41 | 
            -
              #   blob.preview( | 
| 41 | 
            +
              #   blob.preview(resize_to_fit: [100, 100]).processed.service_url
         | 
| 42 42 | 
             
              #
         | 
| 43 43 | 
             
              # Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
         | 
| 44 44 | 
             
              # image is stored with the blob, it is only generated once.
         | 
| @@ -1,24 +1,33 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require " | 
| 3 | 
            +
            require "ostruct"
         | 
| 4 4 |  | 
| 5 5 | 
             
            # Image blobs can have variants that are the result of a set of transformations applied to the original.
         | 
| 6 6 | 
             
            # These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
         | 
| 7 7 | 
             
            # original.
         | 
| 8 8 | 
             
            #
         | 
| 9 | 
            -
            # Variants rely on { | 
| 10 | 
            -
            # of the file, so you must add <tt>gem " | 
| 9 | 
            +
            # Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
         | 
| 10 | 
            +
            # of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
         | 
| 11 | 
            +
            # default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
         | 
| 12 | 
            +
            # {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
         | 
| 13 | 
            +
            # {libvips}[http://jcupitt.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/jcupitt/ruby-vips]
         | 
| 14 | 
            +
            # gem).
         | 
| 11 15 | 
             
            #
         | 
| 12 | 
            -
            # | 
| 13 | 
            -
            #  | 
| 14 | 
            -
            # | 
| 15 | 
            -
            # | 
| 16 | 
            +
            #   Rails.application.config.active_storage.variant_processor
         | 
| 17 | 
            +
            #   # => :mini_magick
         | 
| 18 | 
            +
            #
         | 
| 19 | 
            +
            #   Rails.application.config.active_storage.variant_processor = :vips
         | 
| 20 | 
            +
            #   # => :vips
         | 
| 21 | 
            +
            #
         | 
| 22 | 
            +
            # Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
         | 
| 23 | 
            +
            # you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
         | 
| 24 | 
            +
            # in a template, for example. Delay the processing to an on-demand controller, like the one provided in
         | 
| 16 25 | 
             
            # ActiveStorage::RepresentationsController.
         | 
| 17 26 | 
             
            #
         | 
| 18 27 | 
             
            # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
         | 
| 19 28 | 
             
            # by Active Storage like so:
         | 
| 20 29 | 
             
            #
         | 
| 21 | 
            -
            #   <%= image_tag Current.user.avatar.variant( | 
| 30 | 
            +
            #   <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
         | 
| 22 31 | 
             
            #
         | 
| 23 32 | 
             
            # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
         | 
| 24 33 | 
             
            # can then produce on-demand.
         | 
| @@ -27,19 +36,24 @@ require "active_storage/downloading" | |
| 27 36 | 
             
            # has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
         | 
| 28 37 | 
             
            # the transformations, upload the variant to the service, and return itself again. Example:
         | 
| 29 38 | 
             
            #
         | 
| 30 | 
            -
            #   avatar.variant( | 
| 39 | 
            +
            #   avatar.variant(resize_to_fit: [100, 100]).processed.service_url
         | 
| 31 40 | 
             
            #
         | 
| 32 41 | 
             
            # This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
         | 
| 33 42 | 
             
            # Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
         | 
| 34 43 | 
             
            #
         | 
| 35 | 
            -
            #  | 
| 36 | 
            -
            #  | 
| 44 | 
            +
            # You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
         | 
| 45 | 
            +
            # ImageProcessing gem (such as +resize_to_fit+):
         | 
| 46 | 
            +
            #
         | 
| 47 | 
            +
            #   avatar.variant(resize_to_fit: [800, 800], monochrome: true, rotate: "-90")
         | 
| 48 | 
            +
            #
         | 
| 49 | 
            +
            # Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
         | 
| 37 50 | 
             
            #
         | 
| 38 | 
            -
            # | 
| 51 | 
            +
            # * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
         | 
| 52 | 
            +
            # * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
         | 
| 53 | 
            +
            # * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
         | 
| 54 | 
            +
            # * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
         | 
| 39 55 | 
             
            class ActiveStorage::Variant
         | 
| 40 | 
            -
               | 
| 41 | 
            -
             | 
| 42 | 
            -
              WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif )
         | 
| 56 | 
            +
              WEB_IMAGE_CONTENT_TYPES = %w[ image/png image/jpeg image/jpg image/gif ]
         | 
| 43 57 |  | 
| 44 58 | 
             
              attr_reader :blob, :variation
         | 
| 45 59 | 
             
              delegate :service, to: :blob
         | 
| @@ -67,7 +81,7 @@ class ActiveStorage::Variant | |
| 67 81 | 
             
              # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
         | 
| 68 82 | 
             
              # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
         | 
| 69 83 | 
             
              # for its redirection.
         | 
| 70 | 
            -
              def service_url(expires_in:  | 
| 84 | 
            +
              def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
         | 
| 71 85 | 
             
                service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
         | 
| 72 86 | 
             
              end
         | 
| 73 87 |  | 
| @@ -82,51 +96,36 @@ class ActiveStorage::Variant | |
| 82 96 | 
             
                end
         | 
| 83 97 |  | 
| 84 98 | 
             
                def process
         | 
| 85 | 
            -
                   | 
| 86 | 
            -
                    transform  | 
| 87 | 
            -
                    format image
         | 
| 88 | 
            -
                    upload image
         | 
| 99 | 
            +
                  blob.open do |image|
         | 
| 100 | 
            +
                    transform(image) { |output| upload(output) }
         | 
| 89 101 | 
             
                  end
         | 
| 90 102 | 
             
                end
         | 
| 91 103 |  | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
                  if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
         | 
| 95 | 
            -
                    blob.filename
         | 
| 96 | 
            -
                  else
         | 
| 97 | 
            -
                    ActiveStorage::Filename.new("#{blob.filename.base}.png")
         | 
| 98 | 
            -
                  end
         | 
| 104 | 
            +
                def transform(image, &block)
         | 
| 105 | 
            +
                  variation.transform(image, format: format, &block)
         | 
| 99 106 | 
             
                end
         | 
| 100 107 |  | 
| 101 | 
            -
                def  | 
| 102 | 
            -
                   | 
| 108 | 
            +
                def upload(file)
         | 
| 109 | 
            +
                  service.upload(key, file)
         | 
| 103 110 | 
             
                end
         | 
| 104 111 |  | 
| 105 112 |  | 
| 106 | 
            -
                def  | 
| 107 | 
            -
                   | 
| 108 | 
            -
             | 
| 109 | 
            -
             | 
| 110 | 
            -
             | 
| 111 | 
            -
             | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 113 | 
            +
                def specification
         | 
| 114 | 
            +
                  @specification ||=
         | 
| 115 | 
            +
                    if WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
         | 
| 116 | 
            +
                      Specification.new \
         | 
| 117 | 
            +
                        filename: blob.filename,
         | 
| 118 | 
            +
                        content_type: blob.content_type,
         | 
| 119 | 
            +
                        format: nil
         | 
| 120 | 
            +
                    else
         | 
| 121 | 
            +
                      Specification.new \
         | 
| 122 | 
            +
                        filename: ActiveStorage::Filename.new("#{blob.filename.base}.png"),
         | 
| 123 | 
            +
                        content_type: "image/png",
         | 
| 124 | 
            +
                        format: "png"
         | 
| 125 | 
            +
                    end
         | 
| 119 126 | 
             
                end
         | 
| 120 127 |  | 
| 121 | 
            -
                 | 
| 122 | 
            -
                  variation.transform(image)
         | 
| 123 | 
            -
                end
         | 
| 128 | 
            +
                delegate :filename, :content_type, :format, to: :specification
         | 
| 124 129 |  | 
| 125 | 
            -
                 | 
| 126 | 
            -
                  image.format("PNG") unless WEB_IMAGE_CONTENT_TYPES.include?(blob.content_type)
         | 
| 127 | 
            -
                end
         | 
| 128 | 
            -
             | 
| 129 | 
            -
                def upload(image)
         | 
| 130 | 
            -
                  File.open(image.path, "r") { |file| service.upload(key, file) }
         | 
| 131 | 
            -
                end
         | 
| 130 | 
            +
                class Specification < OpenStruct; end
         | 
| 132 131 | 
             
            end
         | 
| @@ -6,23 +6,12 @@ | |
| 6 6 | 
             
            # In case you do need to use this directly, it's instantiated using a hash of transformations where
         | 
| 7 7 | 
             
            # the key is the command and the value is the arguments. Example:
         | 
| 8 8 | 
             
            #
         | 
| 9 | 
            -
            #   ActiveStorage::Variation.new( | 
| 9 | 
            +
            #   ActiveStorage::Variation.new(resize_to_fit: [100, 100], monochrome: true, trim: true, rotate: "-90")
         | 
| 10 10 | 
             
            #
         | 
| 11 | 
            -
            #  | 
| 12 | 
            -
            #
         | 
| 13 | 
            -
            #   ActiveStorage::Variation.new(combine_options: {
         | 
| 14 | 
            -
            #     resize: "100x100^",
         | 
| 15 | 
            -
            #     gravity: "center",
         | 
| 16 | 
            -
            #     crop: "100x100+0+0",
         | 
| 17 | 
            -
            #   })
         | 
| 18 | 
            -
            #
         | 
| 19 | 
            -
            # A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php.
         | 
| 11 | 
            +
            # The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
         | 
| 20 12 | 
             
            class ActiveStorage::Variation
         | 
| 21 13 | 
             
              attr_reader :transformations
         | 
| 22 14 |  | 
| 23 | 
            -
              class UnsupportedImageProcessingMethod < StandardError; end
         | 
| 24 | 
            -
              class UnsupportedImageProcessingArgument < StandardError; end
         | 
| 25 | 
            -
             | 
| 26 15 | 
             
              class << self
         | 
| 27 16 | 
             
                # Returns a Variation instance based on the given variator. If the variator is a Variation, it is
         | 
| 28 17 | 
             
                # returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
         | 
| @@ -54,24 +43,13 @@ class ActiveStorage::Variation | |
| 54 43 | 
             
                @transformations = transformations
         | 
| 55 44 | 
             
              end
         | 
| 56 45 |  | 
| 57 | 
            -
              # Accepts  | 
| 58 | 
            -
              #  | 
| 59 | 
            -
               | 
| 46 | 
            +
              # Accepts a File object, performs the +transformations+ against it, and
         | 
| 47 | 
            +
              # saves the transformed image into a temporary file. If +format+ is specified
         | 
| 48 | 
            +
              # it will be the format of the result image, otherwise the result image
         | 
| 49 | 
            +
              # retains the source format.
         | 
| 50 | 
            +
              def transform(file, format: nil, &block)
         | 
| 60 51 | 
             
                ActiveSupport::Notifications.instrument("transform.active_storage") do
         | 
| 61 | 
            -
                   | 
| 62 | 
            -
                    validate_transformation(name, argument_or_subtransformations)
         | 
| 63 | 
            -
                    image.mogrify do |command|
         | 
| 64 | 
            -
                      if name.to_s == "combine_options"
         | 
| 65 | 
            -
                        argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
         | 
| 66 | 
            -
                          validate_transformation(subtransformation_name, subtransformation_argument)
         | 
| 67 | 
            -
                          pass_transform_argument(command, subtransformation_name, subtransformation_argument)
         | 
| 68 | 
            -
                        end
         | 
| 69 | 
            -
                      else
         | 
| 70 | 
            -
                        validate_transformation(name, argument_or_subtransformations)
         | 
| 71 | 
            -
                        pass_transform_argument(command, name, argument_or_subtransformations)
         | 
| 72 | 
            -
                      end
         | 
| 73 | 
            -
                    end
         | 
| 74 | 
            -
                  end
         | 
| 52 | 
            +
                  transformer.transform(file, format: format, &block)
         | 
| 75 53 | 
             
                end
         | 
| 76 54 | 
             
              end
         | 
| 77 55 |  | 
| @@ -81,69 +59,22 @@ class ActiveStorage::Variation | |
| 81 59 | 
             
              end
         | 
| 82 60 |  | 
| 83 61 | 
             
              private
         | 
| 84 | 
            -
                def  | 
| 85 | 
            -
                  if  | 
| 86 | 
            -
                     | 
| 87 | 
            -
             | 
| 88 | 
            -
                     | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
             | 
| 92 | 
            -
             | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
                  method_name = name.to_s.gsub("-","_")
         | 
| 98 | 
            -
             | 
| 99 | 
            -
                  unless ActiveStorage.supported_image_processing_methods.any? { |method| method_name == method }
         | 
| 100 | 
            -
                    raise UnsupportedImageProcessingMethod, <<~ERROR.squish
         | 
| 101 | 
            -
                      One or more of the provided transformation methods is not supported.
         | 
| 102 | 
            -
                    ERROR
         | 
| 103 | 
            -
                  end
         | 
| 104 | 
            -
             | 
| 105 | 
            -
                  if argument.present?
         | 
| 106 | 
            -
                    if argument.is_a?(String) || argument.is_a?(Symbol)
         | 
| 107 | 
            -
                      validate_arg_string(argument)
         | 
| 108 | 
            -
                    elsif argument.is_a?(Array)
         | 
| 109 | 
            -
                      validate_arg_array(argument)
         | 
| 110 | 
            -
                    elsif argument.is_a?(Hash)
         | 
| 111 | 
            -
                      validate_arg_hash(argument)
         | 
| 112 | 
            -
                    end
         | 
| 113 | 
            -
                  end
         | 
| 114 | 
            -
                end
         | 
| 115 | 
            -
             | 
| 116 | 
            -
                def validate_arg_string(argument)
         | 
| 117 | 
            -
                  if ActiveStorage.unsupported_image_processing_arguments.any? { |bad_arg| argument.to_s.downcase.include?(bad_arg) }; raise UnsupportedImageProcessingArgument end
         | 
| 118 | 
            -
                end
         | 
| 119 | 
            -
             | 
| 120 | 
            -
                def validate_arg_array(argument)
         | 
| 121 | 
            -
                  argument.each do |arg|
         | 
| 122 | 
            -
                    if arg.is_a?(Integer) || arg.is_a?(Float)
         | 
| 123 | 
            -
                      next
         | 
| 124 | 
            -
                    elsif arg.is_a?(String) || arg.is_a?(Symbol)
         | 
| 125 | 
            -
                      validate_arg_string(arg)
         | 
| 126 | 
            -
                    elsif arg.is_a?(Array)
         | 
| 127 | 
            -
                      validate_arg_array(arg)
         | 
| 128 | 
            -
                    elsif arg.is_a?(Hash)
         | 
| 129 | 
            -
                      validate_arg_hash(arg)
         | 
| 130 | 
            -
                    end
         | 
| 131 | 
            -
                  end
         | 
| 132 | 
            -
                end
         | 
| 133 | 
            -
             | 
| 134 | 
            -
                def validate_arg_hash(argument)
         | 
| 135 | 
            -
                  argument.each do |key, value|
         | 
| 136 | 
            -
                    validate_arg_string(key)
         | 
| 137 | 
            -
             | 
| 138 | 
            -
                    if value.is_a?(Integer) || value.is_a?(Float)
         | 
| 139 | 
            -
                      next
         | 
| 140 | 
            -
                    elsif value.is_a?(String) || value.is_a?(Symbol)
         | 
| 141 | 
            -
                      validate_arg_string(value)
         | 
| 142 | 
            -
                    elsif value.is_a?(Array)
         | 
| 143 | 
            -
                      validate_arg_array(value)
         | 
| 144 | 
            -
                    elsif value.is_a?(Hash)
         | 
| 145 | 
            -
                      validate_arg_hash(value)
         | 
| 62 | 
            +
                def transformer
         | 
| 63 | 
            +
                  if ActiveStorage.variant_processor
         | 
| 64 | 
            +
                    begin
         | 
| 65 | 
            +
                      require "image_processing"
         | 
| 66 | 
            +
                    rescue LoadError
         | 
| 67 | 
            +
                      ActiveSupport::Deprecation.warn <<~WARNING
         | 
| 68 | 
            +
                        Generating image variants will require the image_processing gem in Rails 6.1.
         | 
| 69 | 
            +
                        Please add `gem 'image_processing', '~> 1.2'` to your Gemfile.
         | 
| 70 | 
            +
                      WARNING
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                      ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
         | 
| 73 | 
            +
                    else
         | 
| 74 | 
            +
                      ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations)
         | 
| 146 75 | 
             
                    end
         | 
| 76 | 
            +
                  else
         | 
| 77 | 
            +
                    ActiveStorage::Transformers::MiniMagickTransformer.new(transformations)
         | 
| 147 78 | 
             
                  end
         | 
| 148 79 | 
             
                end
         | 
| 149 80 | 
             
            end
         | 
    
        data/config/routes.rb
    CHANGED
    
    | @@ -1,17 +1,15 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            Rails.application.routes.draw do
         | 
| 4 | 
            -
               | 
| 5 | 
            -
             | 
| 6 | 
            -
              direct :rails_blob do |blob, options|
         | 
| 7 | 
            -
                route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
         | 
| 8 | 
            -
              end
         | 
| 9 | 
            -
             | 
| 10 | 
            -
              resolve("ActiveStorage::Blob")       { |blob, options| route_for(:rails_blob, blob, options) }
         | 
| 11 | 
            -
              resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
         | 
| 4 | 
            +
              scope ActiveStorage.routes_prefix do
         | 
| 5 | 
            +
                get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
         | 
| 12 6 |  | 
| 7 | 
            +
                get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
         | 
| 13 8 |  | 
| 14 | 
            -
               | 
| 9 | 
            +
                get  "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
         | 
| 10 | 
            +
                put  "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
         | 
| 11 | 
            +
                post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
         | 
| 12 | 
            +
              end
         | 
| 15 13 |  | 
| 16 14 | 
             
              direct :rails_representation do |representation, options|
         | 
| 17 15 | 
             
                signed_blob_id = representation.blob.signed_id
         | 
| @@ -25,7 +23,10 @@ Rails.application.routes.draw do | |
| 25 23 | 
             
              resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
         | 
| 26 24 |  | 
| 27 25 |  | 
| 28 | 
            -
               | 
| 29 | 
            -
             | 
| 30 | 
            -
               | 
| 26 | 
            +
              direct :rails_blob do |blob, options|
         | 
| 27 | 
            +
                route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              resolve("ActiveStorage::Blob")       { |blob, options| route_for(:rails_blob, blob, options) }
         | 
| 31 | 
            +
              resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
         | 
| 31 32 | 
             
            end
         | 
| @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0]
         | 
| 2 | 
            +
              def up
         | 
| 3 | 
            +
                unless foreign_key_exists?(:active_storage_attachments, column: :blob_id)
         | 
| 4 | 
            +
                  add_foreign_key :active_storage_attachments, :active_storage_blobs, column: :blob_id
         | 
| 5 | 
            +
                end
         | 
| 6 | 
            +
              end
         | 
| 7 | 
            +
            end
         | 
| @@ -1,7 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require "active_support/core_ext/hash/compact"
         | 
| 4 | 
            -
             | 
| 5 3 | 
             
            module ActiveStorage
         | 
| 6 4 | 
             
              # Extracts the following from a video blob:
         | 
| 7 5 | 
             
              #
         | 
| @@ -18,7 +16,7 @@ module ActiveStorage | |
| 18 16 | 
             
              #
         | 
| 19 17 | 
             
              # When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
         | 
| 20 18 | 
             
              #
         | 
| 21 | 
            -
              # This analyzer requires the { | 
| 19 | 
            +
              # This analyzer requires the {FFmpeg}[https://www.ffmpeg.org] system library, which is not provided by Rails.
         | 
| 22 20 | 
             
              class Analyzer::VideoAnalyzer < Analyzer
         | 
| 23 21 | 
             
                def self.accept?(blob)
         | 
| 24 22 | 
             
                  blob.video?
         | 
| @@ -109,7 +107,7 @@ module ActiveStorage | |
| 109 107 | 
             
                      JSON.parse(output.read)
         | 
| 110 108 | 
             
                    end
         | 
| 111 109 | 
             
                  rescue Errno::ENOENT
         | 
| 112 | 
            -
                    logger.info "Skipping video analysis because  | 
| 110 | 
            +
                    logger.info "Skipping video analysis because FFmpeg isn't installed"
         | 
| 113 111 | 
             
                    {}
         | 
| 114 112 | 
             
                  end
         | 
| 115 113 |  | 
| @@ -1,13 +1,9 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require "active_storage/downloading"
         | 
| 4 | 
            -
             | 
| 5 3 | 
             
            module ActiveStorage
         | 
| 6 4 | 
             
              # This is an abstract base class for analyzers, which extract metadata from blobs. See
         | 
| 7 5 | 
             
              # ActiveStorage::Analyzer::ImageAnalyzer for an example of a concrete subclass.
         | 
| 8 6 | 
             
              class Analyzer
         | 
| 9 | 
            -
                include Downloading
         | 
| 10 | 
            -
             | 
| 11 7 | 
             
                attr_reader :blob
         | 
| 12 8 |  | 
| 13 9 | 
             
                # Implement this method in a concrete subclass. Have it return true when given a blob from which
         | 
| @@ -26,8 +22,17 @@ module ActiveStorage | |
| 26 22 | 
             
                end
         | 
| 27 23 |  | 
| 28 24 | 
             
                private
         | 
| 25 | 
            +
                  # Downloads the blob to a tempfile on disk. Yields the tempfile.
         | 
| 26 | 
            +
                  def download_blob_to_tempfile(&block) #:doc:
         | 
| 27 | 
            +
                    blob.open tempdir: tempdir, &block
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 29 30 | 
             
                  def logger #:doc:
         | 
| 30 31 | 
             
                    ActiveStorage.logger
         | 
| 31 32 | 
             
                  end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  def tempdir #:doc:
         | 
| 35 | 
            +
                    Dir.tmpdir
         | 
| 36 | 
            +
                  end
         | 
| 32 37 | 
             
              end
         | 
| 33 38 | 
             
            end
         |