activestorage 5.2.7.1 → 6.1.4.6
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +225 -93
- data/MIT-LICENSE +1 -1
- data/README.md +43 -8
- data/app/assets/javascripts/activestorage.js +5 -2
- data/app/controllers/active_storage/base_controller.rb +13 -4
- data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
- data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +3 -3
- data/app/controllers/active_storage/direct_uploads_controller.rb +2 -2
- data/app/controllers/active_storage/disk_controller.rb +13 -22
- data/app/controllers/active_storage/representations/base_controller.rb +14 -0
- data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
- data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +3 -5
- data/app/controllers/concerns/active_storage/file_server.rb +18 -0
- data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
- data/app/javascript/activestorage/blob_record.js +7 -2
- data/app/jobs/active_storage/analyze_job.rb +5 -0
- data/app/jobs/active_storage/base_job.rb +0 -1
- data/app/jobs/active_storage/mirror_job.rb +15 -0
- data/app/jobs/active_storage/purge_job.rb +3 -0
- data/app/models/active_storage/attachment.rb +35 -16
- data/app/models/active_storage/blob/analyzable.rb +6 -2
- data/app/models/active_storage/blob/identifiable.rb +7 -6
- data/app/models/active_storage/blob/representable.rb +36 -6
- data/app/models/active_storage/blob.rb +186 -68
- data/app/models/active_storage/filename.rb +0 -6
- data/app/models/active_storage/preview.rb +37 -12
- data/app/models/active_storage/record.rb +7 -0
- data/app/models/active_storage/variant.rb +53 -67
- data/app/models/active_storage/variant_record.rb +8 -0
- data/app/models/active_storage/variant_with_record.rb +54 -0
- data/app/models/active_storage/variation.rb +30 -94
- data/config/routes.rb +66 -15
- data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
- data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
- data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +14 -4
- data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +17 -8
- data/lib/active_storage/analyzer.rb +15 -4
- data/lib/active_storage/attached/changes/create_many.rb +47 -0
- data/lib/active_storage/attached/changes/create_one.rb +82 -0
- data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
- data/lib/active_storage/attached/changes/delete_many.rb +27 -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 +19 -12
- data/lib/active_storage/attached/model.rb +212 -0
- data/lib/active_storage/attached/one.rb +19 -21
- data/lib/active_storage/attached.rb +7 -22
- data/lib/active_storage/downloader.rb +43 -0
- data/lib/active_storage/engine.rb +60 -38
- data/lib/active_storage/errors.rb +25 -3
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/log_subscriber.rb +6 -0
- data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
- data/lib/active_storage/previewer/video_previewer.rb +17 -10
- data/lib/active_storage/previewer.rb +34 -14
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service/azure_storage_service.rb +65 -44
- data/lib/active_storage/service/configurator.rb +6 -2
- data/lib/active_storage/service/disk_service.rb +57 -44
- data/lib/active_storage/service/gcs_service.rb +68 -64
- data/lib/active_storage/service/mirror_service.rb +31 -7
- data/lib/active_storage/service/registry.rb +32 -0
- data/lib/active_storage/service/s3_service.rb +56 -24
- data/lib/active_storage/service.rb +44 -12
- data/lib/active_storage/transformers/image_processing_transformer.rb +45 -0
- data/lib/active_storage/transformers/transformer.rb +39 -0
- data/lib/active_storage.rb +31 -296
- data/lib/tasks/activestorage.rake +11 -0
- metadata +82 -16
- data/app/models/active_storage/filename/parameters.rb +0 -36
- data/lib/active_storage/attached/macros.rb +0 -110
- data/lib/active_storage/downloading.rb +0 -39
@@ -1,24 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_storage/downloading"
|
4
|
-
|
5
3
|
# Image blobs can have variants that are the result of a set of transformations applied to the original.
|
6
4
|
# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
|
7
5
|
# original.
|
8
6
|
#
|
9
|
-
# Variants rely on {
|
10
|
-
# of the file, so you must add <tt>gem "
|
7
|
+
# Variants rely on {ImageProcessing}[https://github.com/janko-m/image_processing] gem for the actual transformations
|
8
|
+
# of the file, so you must add <tt>gem "image_processing"</tt> to your Gemfile if you wish to use variants. By
|
9
|
+
# default, images will be processed with {ImageMagick}[http://imagemagick.org] using the
|
10
|
+
# {MiniMagick}[https://github.com/minimagick/minimagick] gem, but you can also switch to the
|
11
|
+
# {libvips}[http://libvips.github.io/libvips/] processor operated by the {ruby-vips}[https://github.com/libvips/ruby-vips]
|
12
|
+
# gem).
|
13
|
+
#
|
14
|
+
# Rails.application.config.active_storage.variant_processor
|
15
|
+
# # => :mini_magick
|
16
|
+
#
|
17
|
+
# Rails.application.config.active_storage.variant_processor = :vips
|
18
|
+
# # => :vips
|
11
19
|
#
|
12
|
-
# Note that to create a variant it's necessary to download the entire blob file from the service
|
13
|
-
#
|
14
|
-
#
|
15
|
-
# template, for example. Delay the processing to an on-demand controller, like the one provided in
|
20
|
+
# Note that to create a variant it's necessary to download the entire blob file from the service. Because of this process,
|
21
|
+
# you also want to be considerate about when the variant is actually processed. You shouldn't be processing variants inline
|
22
|
+
# in a template, for example. Delay the processing to an on-demand controller, like the one provided in
|
16
23
|
# ActiveStorage::RepresentationsController.
|
17
24
|
#
|
18
25
|
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
|
19
26
|
# by Active Storage like so:
|
20
27
|
#
|
21
|
-
# <%= image_tag Current.user.avatar.variant(
|
28
|
+
# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
|
22
29
|
#
|
23
30
|
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
|
24
31
|
# can then produce on-demand.
|
@@ -27,22 +34,26 @@ require "active_storage/downloading"
|
|
27
34
|
# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
|
28
35
|
# the transformations, upload the variant to the service, and return itself again. Example:
|
29
36
|
#
|
30
|
-
# avatar.variant(
|
37
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.url
|
31
38
|
#
|
32
39
|
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
|
33
40
|
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
|
34
41
|
#
|
35
|
-
#
|
36
|
-
#
|
42
|
+
# You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
|
43
|
+
# ImageProcessing gem (such as +resize_to_limit+):
|
44
|
+
#
|
45
|
+
# avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
|
46
|
+
#
|
47
|
+
# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
|
37
48
|
#
|
38
|
-
#
|
49
|
+
# * {ImageProcessing::MiniMagick}[https://github.com/janko-m/image_processing/blob/master/doc/minimagick.md#methods]
|
50
|
+
# * {ImageMagick reference}[https://www.imagemagick.org/script/mogrify.php]
|
51
|
+
# * {ImageProcessing::Vips}[https://github.com/janko-m/image_processing/blob/master/doc/vips.md#methods]
|
52
|
+
# * {ruby-vips reference}[http://www.rubydoc.info/gems/ruby-vips/Vips/Image]
|
39
53
|
class ActiveStorage::Variant
|
40
|
-
include ActiveStorage::Downloading
|
41
|
-
|
42
|
-
WEB_IMAGE_CONTENT_TYPES = %w( image/png image/jpeg image/jpg image/gif )
|
43
|
-
|
44
54
|
attr_reader :blob, :variation
|
45
55
|
delegate :service, to: :blob
|
56
|
+
delegate :content_type, to: :variation
|
46
57
|
|
47
58
|
def initialize(blob, variation_or_variation_key)
|
48
59
|
@blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
|
@@ -59,18 +70,34 @@ class ActiveStorage::Variant
|
|
59
70
|
"variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
|
60
71
|
end
|
61
72
|
|
62
|
-
# Returns the URL of the variant on the service.
|
63
|
-
# with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
|
64
|
-
# Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
|
65
|
-
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
|
73
|
+
# Returns the URL of the blob variant on the service. See {ActiveStorage::Blob#url} for details.
|
66
74
|
#
|
67
75
|
# Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
|
68
76
|
# for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
|
69
77
|
# for its redirection.
|
70
|
-
def
|
78
|
+
def url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
|
71
79
|
service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
|
72
80
|
end
|
73
81
|
|
82
|
+
alias_method :service_url, :url
|
83
|
+
deprecate service_url: :url
|
84
|
+
|
85
|
+
# Downloads the file associated with this variant. If no block is given, the entire file is read into memory and returned.
|
86
|
+
# That'll use a lot of RAM for very large files. If a block is given, then the download is streamed and yielded in chunks.
|
87
|
+
def download(&block)
|
88
|
+
service.download key, &block
|
89
|
+
end
|
90
|
+
|
91
|
+
def filename
|
92
|
+
ActiveStorage::Filename.new "#{blob.filename.base}.#{variation.format.downcase}"
|
93
|
+
end
|
94
|
+
|
95
|
+
alias_method :content_type_for_serving, :content_type
|
96
|
+
|
97
|
+
def forced_disposition_for_serving #:nodoc:
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
74
101
|
# Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
|
75
102
|
def image
|
76
103
|
self
|
@@ -82,51 +109,10 @@ class ActiveStorage::Variant
|
|
82
109
|
end
|
83
110
|
|
84
111
|
def process
|
85
|
-
|
86
|
-
transform
|
87
|
-
|
88
|
-
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
|
93
|
-
def filename
|
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
|
99
|
-
end
|
100
|
-
|
101
|
-
def content_type
|
102
|
-
blob.content_type.presence_in(WEB_IMAGE_CONTENT_TYPES) || "image/png"
|
103
|
-
end
|
104
|
-
|
105
|
-
|
106
|
-
def open_image(&block)
|
107
|
-
image = download_image
|
108
|
-
|
109
|
-
begin
|
110
|
-
yield image
|
111
|
-
ensure
|
112
|
-
image.destroy!
|
112
|
+
blob.open do |input|
|
113
|
+
variation.transform(input) do |output|
|
114
|
+
service.upload(key, output, content_type: content_type)
|
115
|
+
end
|
113
116
|
end
|
114
117
|
end
|
115
|
-
|
116
|
-
def download_image
|
117
|
-
require "mini_magick"
|
118
|
-
MiniMagick::Image.create(blob.filename.extension_with_delimiter) { |file| download_blob_to(file) }
|
119
|
-
end
|
120
|
-
|
121
|
-
def transform(image)
|
122
|
-
variation.transform(image)
|
123
|
-
end
|
124
|
-
|
125
|
-
def format(image)
|
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
|
132
118
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorage::VariantWithRecord
|
4
|
+
attr_reader :blob, :variation
|
5
|
+
|
6
|
+
def initialize(blob, variation)
|
7
|
+
@blob, @variation = blob, ActiveStorage::Variation.wrap(variation)
|
8
|
+
end
|
9
|
+
|
10
|
+
def processed
|
11
|
+
process
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def process
|
16
|
+
transform_blob { |image| create_or_find_record(image: image) } unless processed?
|
17
|
+
end
|
18
|
+
|
19
|
+
def processed?
|
20
|
+
record.present?
|
21
|
+
end
|
22
|
+
|
23
|
+
def image
|
24
|
+
record&.image
|
25
|
+
end
|
26
|
+
|
27
|
+
delegate :key, :url, :download, to: :image, allow_nil: true
|
28
|
+
|
29
|
+
alias_method :service_url, :url
|
30
|
+
deprecate service_url: :url
|
31
|
+
|
32
|
+
private
|
33
|
+
def transform_blob
|
34
|
+
blob.open do |input|
|
35
|
+
variation.transform(input) do |output|
|
36
|
+
yield io: output, filename: "#{blob.filename.base}.#{variation.format.downcase}",
|
37
|
+
content_type: variation.content_type, service_name: blob.service.name
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_or_find_record(image:)
|
43
|
+
@record =
|
44
|
+
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
|
45
|
+
blob.variant_records.create_or_find_by!(variation_digest: variation.digest) do |record|
|
46
|
+
record.image.attach(image)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def record
|
52
|
+
@record ||= blob.variant_records.find_by(variation_digest: variation.digest)
|
53
|
+
end
|
54
|
+
end
|
@@ -1,28 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "mini_mime"
|
4
|
+
|
3
5
|
# A set of transformations that can be applied to a blob to create a variant. This class is exposed via
|
4
6
|
# the ActiveStorage::Blob#variant method and should rarely be used directly.
|
5
7
|
#
|
6
8
|
# In case you do need to use this directly, it's instantiated using a hash of transformations where
|
7
9
|
# the key is the command and the value is the arguments. Example:
|
8
10
|
#
|
9
|
-
# ActiveStorage::Variation.new(
|
10
|
-
#
|
11
|
-
# You can also combine multiple transformations in one step, e.g. for center-weighted cropping:
|
11
|
+
# ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
|
12
12
|
#
|
13
|
-
#
|
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.
|
13
|
+
# The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
|
20
14
|
class ActiveStorage::Variation
|
21
15
|
attr_reader :transformations
|
22
16
|
|
23
|
-
class UnsupportedImageProcessingMethod < StandardError; end
|
24
|
-
class UnsupportedImageProcessingArgument < StandardError; end
|
25
|
-
|
26
17
|
class << self
|
27
18
|
# Returns a Variation instance based on the given variator. If the variator is a Variation, it is
|
28
19
|
# returned unmodified. If it is a String, it is passed to ActiveStorage::Variation.decode. Otherwise,
|
@@ -51,99 +42,44 @@ class ActiveStorage::Variation
|
|
51
42
|
end
|
52
43
|
|
53
44
|
def initialize(transformations)
|
54
|
-
@transformations = transformations
|
45
|
+
@transformations = transformations.deep_symbolize_keys
|
55
46
|
end
|
56
47
|
|
57
|
-
|
58
|
-
|
59
|
-
|
48
|
+
def default_to(defaults)
|
49
|
+
self.class.new transformations.reverse_merge(defaults)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Accepts a File object, performs the +transformations+ against it, and
|
53
|
+
# saves the transformed image into a temporary file.
|
54
|
+
def transform(file, &block)
|
60
55
|
ActiveSupport::Notifications.instrument("transform.active_storage") do
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
else
|
70
|
-
validate_transformation(name, argument_or_subtransformations)
|
71
|
-
pass_transform_argument(command, name, argument_or_subtransformations)
|
72
|
-
end
|
73
|
-
end
|
56
|
+
transformer.transform(file, format: format, &block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def format
|
61
|
+
transformations.fetch(:format, :png).tap do |format|
|
62
|
+
if MiniMime.lookup_by_extension(format.to_s).nil?
|
63
|
+
raise ArgumentError, "Invalid variant format (#{format.inspect})"
|
74
64
|
end
|
75
65
|
end
|
76
66
|
end
|
77
67
|
|
68
|
+
def content_type
|
69
|
+
MiniMime.lookup_by_extension(format.to_s).content_type
|
70
|
+
end
|
71
|
+
|
78
72
|
# Returns a signed key for all the +transformations+ that this variation was instantiated with.
|
79
73
|
def key
|
80
74
|
self.class.encode(transformations)
|
81
75
|
end
|
82
76
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
command.public_send(method, argument)
|
87
|
-
else
|
88
|
-
command.public_send(method)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def eligible_argument?(argument)
|
93
|
-
argument.present? && argument != true
|
94
|
-
end
|
95
|
-
|
96
|
-
def validate_transformation(name, argument)
|
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)
|
77
|
+
def digest
|
78
|
+
Digest::SHA1.base64digest Marshal.dump(transformations)
|
79
|
+
end
|
137
80
|
|
138
|
-
|
139
|
-
|
140
|
-
|
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)
|
146
|
-
end
|
147
|
-
end
|
81
|
+
private
|
82
|
+
def transformer
|
83
|
+
ActiveStorage::Transformers::ImageProcessingTransformer.new(transformations.except(:format))
|
148
84
|
end
|
149
85
|
end
|
data/config/routes.rb
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
Rails.application.routes.draw do
|
4
|
-
|
4
|
+
scope ActiveStorage.routes_prefix do
|
5
|
+
get "/blobs/redirect/:signed_id/*filename" => "active_storage/blobs/redirect#show", as: :rails_service_blob
|
6
|
+
get "/blobs/proxy/:signed_id/*filename" => "active_storage/blobs/proxy#show", as: :rails_service_blob_proxy
|
7
|
+
get "/blobs/:signed_id/*filename" => "active_storage/blobs/redirect#show"
|
5
8
|
|
6
|
-
|
7
|
-
|
8
|
-
|
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) }
|
9
|
+
get "/representations/redirect/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/redirect#show", as: :rails_blob_representation
|
10
|
+
get "/representations/proxy/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/proxy#show", as: :rails_blob_representation_proxy
|
11
|
+
get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/redirect#show"
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
|
14
|
+
put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
|
15
|
+
post "/direct_uploads" => "active_storage/direct_uploads#create", as: :rails_direct_uploads
|
16
|
+
end
|
15
17
|
|
16
18
|
direct :rails_representation do |representation, options|
|
17
19
|
signed_blob_id = representation.blob.signed_id
|
@@ -21,11 +23,60 @@ Rails.application.routes.draw do
|
|
21
23
|
route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
|
22
24
|
end
|
23
25
|
|
24
|
-
resolve("ActiveStorage::Variant") { |variant, options| route_for(
|
25
|
-
resolve("ActiveStorage::
|
26
|
+
resolve("ActiveStorage::Variant") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
|
27
|
+
resolve("ActiveStorage::VariantWithRecord") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
|
28
|
+
resolve("ActiveStorage::Preview") { |preview, options| route_for(ActiveStorage.resolve_model_to_route, preview, options) }
|
26
29
|
|
30
|
+
direct :rails_blob do |blob, options|
|
31
|
+
route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
resolve("ActiveStorage::Blob") { |blob, options| route_for(ActiveStorage.resolve_model_to_route, blob, options) }
|
35
|
+
resolve("ActiveStorage::Attachment") { |attachment, options| route_for(ActiveStorage.resolve_model_to_route, attachment.blob, options) }
|
27
36
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
37
|
+
direct :rails_storage_proxy do |model, options|
|
38
|
+
if model.respond_to?(:signed_id)
|
39
|
+
route_for(
|
40
|
+
:rails_service_blob_proxy,
|
41
|
+
model.signed_id,
|
42
|
+
model.filename,
|
43
|
+
options
|
44
|
+
)
|
45
|
+
else
|
46
|
+
signed_blob_id = model.blob.signed_id
|
47
|
+
variation_key = model.variation.key
|
48
|
+
filename = model.blob.filename
|
49
|
+
|
50
|
+
route_for(
|
51
|
+
:rails_blob_representation_proxy,
|
52
|
+
signed_blob_id,
|
53
|
+
variation_key,
|
54
|
+
filename,
|
55
|
+
options
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
direct :rails_storage_redirect do |model, options|
|
61
|
+
if model.respond_to?(:signed_id)
|
62
|
+
route_for(
|
63
|
+
:rails_service_blob,
|
64
|
+
model.signed_id,
|
65
|
+
model.filename,
|
66
|
+
options
|
67
|
+
)
|
68
|
+
else
|
69
|
+
signed_blob_id = model.blob.signed_id
|
70
|
+
variation_key = model.variation.key
|
71
|
+
filename = model.blob.filename
|
72
|
+
|
73
|
+
route_for(
|
74
|
+
:rails_blob_representation,
|
75
|
+
signed_blob_id,
|
76
|
+
variation_key,
|
77
|
+
filename,
|
78
|
+
options
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end if ActiveStorage.draw_routes
|
@@ -1,13 +1,14 @@
|
|
1
1
|
class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
2
2
|
def change
|
3
3
|
create_table :active_storage_blobs do |t|
|
4
|
-
t.string :key,
|
5
|
-
t.string :filename,
|
4
|
+
t.string :key, null: false
|
5
|
+
t.string :filename, null: false
|
6
6
|
t.string :content_type
|
7
7
|
t.text :metadata
|
8
|
-
t.
|
9
|
-
t.
|
10
|
-
t.
|
8
|
+
t.string :service_name, null: false
|
9
|
+
t.bigint :byte_size, null: false
|
10
|
+
t.string :checksum, null: false
|
11
|
+
t.datetime :created_at, null: false
|
11
12
|
|
12
13
|
t.index [ :key ], unique: true
|
13
14
|
end
|
@@ -22,5 +23,13 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
|
|
22
23
|
t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
|
23
24
|
t.foreign_key :active_storage_blobs, column: :blob_id
|
24
25
|
end
|
26
|
+
|
27
|
+
create_table :active_storage_variant_records do |t|
|
28
|
+
t.belongs_to :blob, null: false, index: false
|
29
|
+
t.string :variation_digest, null: false
|
30
|
+
|
31
|
+
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
|
32
|
+
t.foreign_key :active_storage_blobs, column: :blob_id
|
33
|
+
end
|
25
34
|
end
|
26
35
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
2
|
+
def up
|
3
|
+
unless column_exists?(:active_storage_blobs, :service_name)
|
4
|
+
add_column :active_storage_blobs, :service_name, :string
|
5
|
+
|
6
|
+
if configured_service = ActiveStorage::Blob.service.name
|
7
|
+
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
8
|
+
end
|
9
|
+
|
10
|
+
change_column :active_storage_blobs, :service_name, :string, null: false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def down
|
15
|
+
remove_column :active_storage_blobs, :service_name
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
2
|
+
def change
|
3
|
+
create_table :active_storage_variant_records do |t|
|
4
|
+
t.belongs_to :blob, null: false, index: false
|
5
|
+
t.string :variation_digest, null: false
|
6
|
+
|
7
|
+
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
|
8
|
+
t.foreign_key :active_storage_blobs, column: :blob_id
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -25,17 +25,27 @@ module ActiveStorage
|
|
25
25
|
{ width: image.width, height: image.height }
|
26
26
|
end
|
27
27
|
end
|
28
|
-
rescue LoadError
|
29
|
-
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
|
30
|
-
{}
|
31
28
|
end
|
32
29
|
|
33
30
|
private
|
34
31
|
def read_image
|
35
32
|
download_blob_to_tempfile do |file|
|
36
33
|
require "mini_magick"
|
37
|
-
|
34
|
+
image = MiniMagick::Image.new(file.path)
|
35
|
+
|
36
|
+
if image.valid?
|
37
|
+
yield image
|
38
|
+
else
|
39
|
+
logger.info "Skipping image analysis because ImageMagick doesn't support the file"
|
40
|
+
{}
|
41
|
+
end
|
38
42
|
end
|
43
|
+
rescue LoadError
|
44
|
+
logger.info "Skipping image analysis because the mini_magick gem isn't installed"
|
45
|
+
{}
|
46
|
+
rescue MiniMagick::Error => error
|
47
|
+
logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
|
48
|
+
{}
|
39
49
|
end
|
40
50
|
|
41
51
|
def rotated_image?(image)
|
@@ -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
|
#
|
@@ -13,12 +11,12 @@ module ActiveStorage
|
|
13
11
|
#
|
14
12
|
# Example:
|
15
13
|
#
|
16
|
-
# ActiveStorage::VideoAnalyzer.new(blob).metadata
|
14
|
+
# ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
|
17
15
|
# # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
|
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?
|
@@ -46,7 +44,8 @@ module ActiveStorage
|
|
46
44
|
end
|
47
45
|
|
48
46
|
def duration
|
49
|
-
|
47
|
+
duration = video_stream["duration"] || container["duration"]
|
48
|
+
Float(duration) if duration
|
50
49
|
end
|
51
50
|
|
52
51
|
def angle
|
@@ -100,16 +99,26 @@ module ActiveStorage
|
|
100
99
|
probe["streams"] || []
|
101
100
|
end
|
102
101
|
|
102
|
+
def container
|
103
|
+
probe["format"] || {}
|
104
|
+
end
|
105
|
+
|
103
106
|
def probe
|
104
|
-
download_blob_to_tempfile { |file| probe_from(file) }
|
107
|
+
@probe ||= download_blob_to_tempfile { |file| probe_from(file) }
|
105
108
|
end
|
106
109
|
|
107
110
|
def probe_from(file)
|
108
|
-
IO.popen([ ffprobe_path,
|
111
|
+
IO.popen([ ffprobe_path,
|
112
|
+
"-print_format", "json",
|
113
|
+
"-show_streams",
|
114
|
+
"-show_format",
|
115
|
+
"-v", "error",
|
116
|
+
file.path
|
117
|
+
]) do |output|
|
109
118
|
JSON.parse(output.read)
|
110
119
|
end
|
111
120
|
rescue Errno::ENOENT
|
112
|
-
logger.info "Skipping video analysis because
|
121
|
+
logger.info "Skipping video analysis because FFmpeg isn't installed"
|
113
122
|
{}
|
114
123
|
end
|
115
124
|
|