activestorage 6.0.0
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 +7 -0
- data/CHANGELOG.md +198 -0
- data/MIT-LICENSE +20 -0
- data/README.md +162 -0
- data/app/assets/javascripts/activestorage.js +942 -0
- data/app/controllers/active_storage/base_controller.rb +8 -0
- data/app/controllers/active_storage/blobs_controller.rb +14 -0
- data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
- data/app/controllers/active_storage/disk_controller.rb +66 -0
- data/app/controllers/active_storage/representations_controller.rb +14 -0
- data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/javascript/activestorage/blob_record.js +73 -0
- data/app/javascript/activestorage/blob_upload.js +35 -0
- data/app/javascript/activestorage/direct_upload.js +48 -0
- data/app/javascript/activestorage/direct_upload_controller.js +67 -0
- data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
- data/app/javascript/activestorage/file_checksum.js +53 -0
- data/app/javascript/activestorage/helpers.js +51 -0
- data/app/javascript/activestorage/index.js +11 -0
- data/app/javascript/activestorage/ujs.js +86 -0
- data/app/jobs/active_storage/analyze_job.rb +12 -0
- data/app/jobs/active_storage/base_job.rb +4 -0
- data/app/jobs/active_storage/purge_job.rb +13 -0
- data/app/models/active_storage/attachment.rb +50 -0
- data/app/models/active_storage/blob.rb +278 -0
- data/app/models/active_storage/blob/analyzable.rb +57 -0
- data/app/models/active_storage/blob/identifiable.rb +31 -0
- data/app/models/active_storage/blob/representable.rb +93 -0
- data/app/models/active_storage/current.rb +5 -0
- data/app/models/active_storage/filename.rb +77 -0
- data/app/models/active_storage/preview.rb +89 -0
- data/app/models/active_storage/variant.rb +131 -0
- data/app/models/active_storage/variation.rb +80 -0
- data/config/routes.rb +32 -0
- data/db/migrate/20170806125915_create_active_storage_tables.rb +26 -0
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
- data/lib/active_storage.rb +73 -0
- data/lib/active_storage/analyzer.rb +38 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +52 -0
- data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +118 -0
- data/lib/active_storage/attached.rb +25 -0
- data/lib/active_storage/attached/changes.rb +16 -0
- data/lib/active_storage/attached/changes/create_many.rb +46 -0
- data/lib/active_storage/attached/changes/create_one.rb +69 -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/many.rb +65 -0
- data/lib/active_storage/attached/model.rb +147 -0
- data/lib/active_storage/attached/one.rb +79 -0
- data/lib/active_storage/downloader.rb +43 -0
- data/lib/active_storage/downloading.rb +47 -0
- data/lib/active_storage/engine.rb +149 -0
- data/lib/active_storage/errors.rb +26 -0
- data/lib/active_storage/gem_version.rb +17 -0
- data/lib/active_storage/log_subscriber.rb +58 -0
- data/lib/active_storage/previewer.rb +84 -0
- data/lib/active_storage/previewer/mupdf_previewer.rb +36 -0
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +35 -0
- data/lib/active_storage/previewer/video_previewer.rb +26 -0
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service.rb +141 -0
- data/lib/active_storage/service/azure_storage_service.rb +165 -0
- data/lib/active_storage/service/configurator.rb +34 -0
- data/lib/active_storage/service/disk_service.rb +166 -0
- data/lib/active_storage/service/gcs_service.rb +141 -0
- data/lib/active_storage/service/mirror_service.rb +55 -0
- data/lib/active_storage/service/s3_service.rb +116 -0
- 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/version.rb +10 -0
- data/lib/tasks/activestorage.rake +22 -0
- metadata +174 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_storage/analyzer/null_analyzer"
|
4
|
+
|
5
|
+
module ActiveStorage::Blob::Analyzable
|
6
|
+
# Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
|
7
|
+
# with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
|
8
|
+
# ActiveStorage::Analyzer::VideoAnalyzer for information about the specific attributes they extract and the third-party
|
9
|
+
# libraries they require.
|
10
|
+
#
|
11
|
+
# To choose the analyzer for a blob, Active Storage calls +accept?+ on each registered analyzer in order. It uses the
|
12
|
+
# first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
|
13
|
+
# metadata is extracted from it.
|
14
|
+
#
|
15
|
+
# In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
|
16
|
+
# in an initializer:
|
17
|
+
#
|
18
|
+
# # Add a custom analyzer for Microsoft Office documents:
|
19
|
+
# Rails.application.config.active_storage.analyzers.append DOCXAnalyzer
|
20
|
+
#
|
21
|
+
# # Remove the built-in video analyzer:
|
22
|
+
# Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
|
23
|
+
#
|
24
|
+
# Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
|
25
|
+
#
|
26
|
+
# You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
|
27
|
+
# analyzed via #analyze_later when they're attached for the first time.
|
28
|
+
def analyze
|
29
|
+
update! metadata: metadata.merge(extract_metadata_via_analyzer)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Enqueues an ActiveStorage::AnalyzeJob which calls #analyze.
|
33
|
+
#
|
34
|
+
# This method is automatically called for a blob when it's attached for the first time. You can call it to analyze a blob
|
35
|
+
# again (e.g. if you add a new analyzer or modify an existing one).
|
36
|
+
def analyze_later
|
37
|
+
ActiveStorage::AnalyzeJob.perform_later(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns true if the blob has been analyzed.
|
41
|
+
def analyzed?
|
42
|
+
analyzed
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def extract_metadata_via_analyzer
|
47
|
+
analyzer.metadata.merge(analyzed: true)
|
48
|
+
end
|
49
|
+
|
50
|
+
def analyzer
|
51
|
+
analyzer_class.new(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def analyzer_class
|
55
|
+
ActiveStorage.analyzers.detect { |klass| klass.accept?(self) } || ActiveStorage::Analyzer::NullAnalyzer
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::Blob::Identifiable
|
4
|
+
def identify
|
5
|
+
unless identified?
|
6
|
+
update! content_type: identify_content_type, identified: true
|
7
|
+
update_service_metadata
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def identified?
|
12
|
+
identified
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
def identify_content_type
|
17
|
+
Marcel::MimeType.for download_identifiable_chunk, name: filename.to_s, declared_type: content_type
|
18
|
+
end
|
19
|
+
|
20
|
+
def download_identifiable_chunk
|
21
|
+
if byte_size.positive?
|
22
|
+
service.download_chunk key, 0...4.kilobytes
|
23
|
+
else
|
24
|
+
""
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def update_service_metadata
|
29
|
+
service.update_metadata key, service_metadata if service_metadata.any?
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage::Blob::Representable
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
has_one_attached :preview_image
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
|
11
|
+
# files, and it allows any image to be transformed for size, colors, and the like. Example:
|
12
|
+
#
|
13
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.service_url
|
14
|
+
#
|
15
|
+
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
|
16
|
+
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
|
17
|
+
#
|
18
|
+
# Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
|
19
|
+
# specific variant that can be created by a controller on-demand. Like so:
|
20
|
+
#
|
21
|
+
# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
|
22
|
+
#
|
23
|
+
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
|
24
|
+
# can then produce on-demand.
|
25
|
+
#
|
26
|
+
# Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is
|
27
|
+
# variable, call ActiveStorage::Blob#variable?.
|
28
|
+
def variant(transformations)
|
29
|
+
if variable?
|
30
|
+
ActiveStorage::Variant.new(self, transformations)
|
31
|
+
else
|
32
|
+
raise ActiveStorage::InvariableError
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns true if ImageMagick can transform the blob (its content type is in +ActiveStorage.variable_content_types+).
|
37
|
+
def variable?
|
38
|
+
ActiveStorage.variable_content_types.include?(content_type)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Returns an ActiveStorage::Preview instance with the set of +transformations+ provided. A preview is an image generated
|
43
|
+
# from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
|
44
|
+
# extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
|
45
|
+
#
|
46
|
+
# blob.preview(resize_to_limit: [100, 100]).processed.service_url
|
47
|
+
#
|
48
|
+
# Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
|
49
|
+
# Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
|
50
|
+
# how to use the built-in version:
|
51
|
+
#
|
52
|
+
# <%= image_tag video.preview(resize_to_limit: [100, 100]) %>
|
53
|
+
#
|
54
|
+
# This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
|
55
|
+
# whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
|
56
|
+
def preview(transformations)
|
57
|
+
if previewable?
|
58
|
+
ActiveStorage::Preview.new(self, transformations)
|
59
|
+
else
|
60
|
+
raise ActiveStorage::UnpreviewableError
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns true if any registered previewer accepts the blob. By default, this will return true for videos and PDF documents.
|
65
|
+
def previewable?
|
66
|
+
ActiveStorage.previewers.any? { |klass| klass.accept?(self) }
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
|
71
|
+
#
|
72
|
+
# blob.representation(resize_to_limit: [100, 100]).processed.service_url
|
73
|
+
#
|
74
|
+
# Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
|
75
|
+
# ActiveStorage::Blob#representable? to determine whether a blob is representable.
|
76
|
+
#
|
77
|
+
# See ActiveStorage::Blob#preview and ActiveStorage::Blob#variant for more information.
|
78
|
+
def representation(transformations)
|
79
|
+
case
|
80
|
+
when previewable?
|
81
|
+
preview transformations
|
82
|
+
when variable?
|
83
|
+
variant transformations
|
84
|
+
else
|
85
|
+
raise ActiveStorage::UnrepresentableError
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Returns true if the blob is variable or previewable.
|
90
|
+
def representable?
|
91
|
+
variable? || previewable?
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
|
4
|
+
# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
|
5
|
+
class ActiveStorage::Filename
|
6
|
+
include Comparable
|
7
|
+
|
8
|
+
class << self
|
9
|
+
# Returns a Filename instance based on the given filename. If the filename is a Filename, it is
|
10
|
+
# returned unmodified. If it is a String, it is passed to ActiveStorage::Filename.new.
|
11
|
+
def wrap(filename)
|
12
|
+
filename.kind_of?(self) ? filename : new(filename)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(filename)
|
17
|
+
@filename = filename
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the part of the filename preceding any extension.
|
21
|
+
#
|
22
|
+
# ActiveStorage::Filename.new("racecar.jpg").base # => "racecar"
|
23
|
+
# ActiveStorage::Filename.new("racecar").base # => "racecar"
|
24
|
+
# ActiveStorage::Filename.new(".gitignore").base # => ".gitignore"
|
25
|
+
def base
|
26
|
+
File.basename @filename, extension_with_delimiter
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at the
|
30
|
+
# beginning) with the dot that precedes it. If the filename has no extension, an empty string is returned.
|
31
|
+
#
|
32
|
+
# ActiveStorage::Filename.new("racecar.jpg").extension_with_delimiter # => ".jpg"
|
33
|
+
# ActiveStorage::Filename.new("racecar").extension_with_delimiter # => ""
|
34
|
+
# ActiveStorage::Filename.new(".gitignore").extension_with_delimiter # => ""
|
35
|
+
def extension_with_delimiter
|
36
|
+
File.extname @filename
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns the extension of the filename (i.e. the substring following the last dot, excluding a dot at
|
40
|
+
# the beginning). If the filename has no extension, an empty string is returned.
|
41
|
+
#
|
42
|
+
# ActiveStorage::Filename.new("racecar.jpg").extension_without_delimiter # => "jpg"
|
43
|
+
# ActiveStorage::Filename.new("racecar").extension_without_delimiter # => ""
|
44
|
+
# ActiveStorage::Filename.new(".gitignore").extension_without_delimiter # => ""
|
45
|
+
def extension_without_delimiter
|
46
|
+
extension_with_delimiter.from(1).to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
alias_method :extension, :extension_without_delimiter
|
50
|
+
|
51
|
+
# Returns the sanitized filename.
|
52
|
+
#
|
53
|
+
# ActiveStorage::Filename.new("foo:bar.jpg").sanitized # => "foo-bar.jpg"
|
54
|
+
# ActiveStorage::Filename.new("foo/bar.jpg").sanitized # => "foo-bar.jpg"
|
55
|
+
#
|
56
|
+
# Characters considered unsafe for storage (e.g. \, $, and the RTL override character) are replaced with a dash.
|
57
|
+
def sanitized
|
58
|
+
@filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-")
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the sanitized version of the filename.
|
62
|
+
def to_s
|
63
|
+
sanitized.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
def as_json(*)
|
67
|
+
to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
def to_json
|
71
|
+
to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def <=>(other)
|
75
|
+
to_s.downcase <=> other.to_s.downcase
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Some non-image blobs can be previewed: that is, they can be presented as images. A video blob can be previewed by
|
4
|
+
# extracting its first frame, and a PDF blob can be previewed by extracting its first page.
|
5
|
+
#
|
6
|
+
# A previewer extracts a preview image from a blob. Active Storage provides previewers for videos and PDFs:
|
7
|
+
# ActiveStorage::Previewer::VideoPreviewer and ActiveStorage::Previewer::PDFPreviewer. Build custom previewers by
|
8
|
+
# subclassing ActiveStorage::Previewer and implementing the requisite methods. Consult the ActiveStorage::Previewer
|
9
|
+
# documentation for more details on what's required of previewers.
|
10
|
+
#
|
11
|
+
# To choose the previewer for a blob, Active Storage calls +accept?+ on each registered previewer in order. It uses the
|
12
|
+
# first previewer for which +accept?+ returns true when given the blob. In a Rails application, add or remove previewers
|
13
|
+
# by manipulating +Rails.application.config.active_storage.previewers+ in an initializer:
|
14
|
+
#
|
15
|
+
# Rails.application.config.active_storage.previewers
|
16
|
+
# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
|
17
|
+
#
|
18
|
+
# # Add a custom previewer for Microsoft Office documents:
|
19
|
+
# Rails.application.config.active_storage.previewers << DOCXPreviewer
|
20
|
+
# # => [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer, DOCXPreviewer ]
|
21
|
+
#
|
22
|
+
# Outside of a Rails application, modify +ActiveStorage.previewers+ instead.
|
23
|
+
#
|
24
|
+
# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer 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
|
+
#
|
28
|
+
# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
|
29
|
+
# install and use third-party software, make sure you understand the licensing implications of doing so.
|
30
|
+
class ActiveStorage::Preview
|
31
|
+
class UnprocessedError < StandardError; end
|
32
|
+
|
33
|
+
attr_reader :blob, :variation
|
34
|
+
|
35
|
+
def initialize(blob, variation_or_variation_key)
|
36
|
+
@blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
|
40
|
+
#
|
41
|
+
# blob.preview(resize_to_limit: [100, 100]).processed.service_url
|
42
|
+
#
|
43
|
+
# Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
|
44
|
+
# image is stored with the blob, it is only generated once.
|
45
|
+
def processed
|
46
|
+
process unless processed?
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the blob's attached preview image.
|
51
|
+
def image
|
52
|
+
blob.preview_image
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns the URL of the preview's variant on the service. Raises ActiveStorage::Preview::UnprocessedError if the
|
56
|
+
# preview has not been processed yet.
|
57
|
+
#
|
58
|
+
# This method synchronously processes a variant of the preview image, so do not call it in views. Instead, generate
|
59
|
+
# a stable URL that redirects to the short-lived URL returned by this method.
|
60
|
+
def service_url(**options)
|
61
|
+
if processed?
|
62
|
+
variant.service_url(options)
|
63
|
+
else
|
64
|
+
raise UnprocessedError
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
def processed?
|
70
|
+
image.attached?
|
71
|
+
end
|
72
|
+
|
73
|
+
def process
|
74
|
+
previewer.preview { |attachable| image.attach(attachable) }
|
75
|
+
end
|
76
|
+
|
77
|
+
def variant
|
78
|
+
ActiveStorage::Variant.new(image, variation).processed
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def previewer
|
83
|
+
previewer_class.new(blob)
|
84
|
+
end
|
85
|
+
|
86
|
+
def previewer_class
|
87
|
+
ActiveStorage.previewers.detect { |klass| klass.accept?(blob) }
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
# Image blobs can have variants that are the result of a set of transformations applied to the original.
|
6
|
+
# These variants are used to create thumbnails, fixed-size avatars, or any other derivative image from the
|
7
|
+
# original.
|
8
|
+
#
|
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).
|
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
|
25
|
+
# ActiveStorage::RepresentationsController.
|
26
|
+
#
|
27
|
+
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
|
28
|
+
# by Active Storage like so:
|
29
|
+
#
|
30
|
+
# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
|
31
|
+
#
|
32
|
+
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
|
33
|
+
# can then produce on-demand.
|
34
|
+
#
|
35
|
+
# When you do want to actually produce the variant needed, call +processed+. This will check that the variant
|
36
|
+
# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
|
37
|
+
# the transformations, upload the variant to the service, and return itself again. Example:
|
38
|
+
#
|
39
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.service_url
|
40
|
+
#
|
41
|
+
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
|
42
|
+
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
|
43
|
+
#
|
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_limit+):
|
46
|
+
#
|
47
|
+
# avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
|
48
|
+
#
|
49
|
+
# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
|
50
|
+
#
|
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]
|
55
|
+
class ActiveStorage::Variant
|
56
|
+
WEB_IMAGE_CONTENT_TYPES = %w[ image/png image/jpeg image/jpg image/gif ]
|
57
|
+
|
58
|
+
attr_reader :blob, :variation
|
59
|
+
delegate :service, to: :blob
|
60
|
+
|
61
|
+
def initialize(blob, variation_or_variation_key)
|
62
|
+
@blob, @variation = blob, ActiveStorage::Variation.wrap(variation_or_variation_key)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the variant instance itself after it's been processed or an existing processing has been found on the service.
|
66
|
+
def processed
|
67
|
+
process unless processed?
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns a combination key of the blob and the variation that together identifies a specific variant.
|
72
|
+
def key
|
73
|
+
"variants/#{blob.key}/#{Digest::SHA256.hexdigest(variation.key)}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly
|
77
|
+
# with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
|
78
|
+
# Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
|
79
|
+
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
|
80
|
+
#
|
81
|
+
# Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL
|
82
|
+
# for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method
|
83
|
+
# for its redirection.
|
84
|
+
def service_url(expires_in: ActiveStorage.service_urls_expire_in, disposition: :inline)
|
85
|
+
service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the receiving variant. Allows ActiveStorage::Variant and ActiveStorage::Preview instances to be used interchangeably.
|
89
|
+
def image
|
90
|
+
self
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
def processed?
|
95
|
+
service.exist?(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
def process
|
99
|
+
blob.open do |image|
|
100
|
+
transform(image) { |output| upload(output) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def transform(image, &block)
|
105
|
+
variation.transform(image, format: format, &block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def upload(file)
|
109
|
+
service.upload(key, file)
|
110
|
+
end
|
111
|
+
|
112
|
+
|
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
|
126
|
+
end
|
127
|
+
|
128
|
+
delegate :filename, :content_type, :format, to: :specification
|
129
|
+
|
130
|
+
class Specification < OpenStruct; end
|
131
|
+
end
|