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.

Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +198 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +162 -0
  5. data/app/assets/javascripts/activestorage.js +942 -0
  6. data/app/controllers/active_storage/base_controller.rb +8 -0
  7. data/app/controllers/active_storage/blobs_controller.rb +14 -0
  8. data/app/controllers/active_storage/direct_uploads_controller.rb +23 -0
  9. data/app/controllers/active_storage/disk_controller.rb +66 -0
  10. data/app/controllers/active_storage/representations_controller.rb +14 -0
  11. data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
  12. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  13. data/app/javascript/activestorage/blob_record.js +73 -0
  14. data/app/javascript/activestorage/blob_upload.js +35 -0
  15. data/app/javascript/activestorage/direct_upload.js +48 -0
  16. data/app/javascript/activestorage/direct_upload_controller.js +67 -0
  17. data/app/javascript/activestorage/direct_uploads_controller.js +50 -0
  18. data/app/javascript/activestorage/file_checksum.js +53 -0
  19. data/app/javascript/activestorage/helpers.js +51 -0
  20. data/app/javascript/activestorage/index.js +11 -0
  21. data/app/javascript/activestorage/ujs.js +86 -0
  22. data/app/jobs/active_storage/analyze_job.rb +12 -0
  23. data/app/jobs/active_storage/base_job.rb +4 -0
  24. data/app/jobs/active_storage/purge_job.rb +13 -0
  25. data/app/models/active_storage/attachment.rb +50 -0
  26. data/app/models/active_storage/blob.rb +278 -0
  27. data/app/models/active_storage/blob/analyzable.rb +57 -0
  28. data/app/models/active_storage/blob/identifiable.rb +31 -0
  29. data/app/models/active_storage/blob/representable.rb +93 -0
  30. data/app/models/active_storage/current.rb +5 -0
  31. data/app/models/active_storage/filename.rb +77 -0
  32. data/app/models/active_storage/preview.rb +89 -0
  33. data/app/models/active_storage/variant.rb +131 -0
  34. data/app/models/active_storage/variation.rb +80 -0
  35. data/config/routes.rb +32 -0
  36. data/db/migrate/20170806125915_create_active_storage_tables.rb +26 -0
  37. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
  38. data/lib/active_storage.rb +73 -0
  39. data/lib/active_storage/analyzer.rb +38 -0
  40. data/lib/active_storage/analyzer/image_analyzer.rb +52 -0
  41. data/lib/active_storage/analyzer/null_analyzer.rb +13 -0
  42. data/lib/active_storage/analyzer/video_analyzer.rb +118 -0
  43. data/lib/active_storage/attached.rb +25 -0
  44. data/lib/active_storage/attached/changes.rb +16 -0
  45. data/lib/active_storage/attached/changes/create_many.rb +46 -0
  46. data/lib/active_storage/attached/changes/create_one.rb +69 -0
  47. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  48. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  49. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  50. data/lib/active_storage/attached/many.rb +65 -0
  51. data/lib/active_storage/attached/model.rb +147 -0
  52. data/lib/active_storage/attached/one.rb +79 -0
  53. data/lib/active_storage/downloader.rb +43 -0
  54. data/lib/active_storage/downloading.rb +47 -0
  55. data/lib/active_storage/engine.rb +149 -0
  56. data/lib/active_storage/errors.rb +26 -0
  57. data/lib/active_storage/gem_version.rb +17 -0
  58. data/lib/active_storage/log_subscriber.rb +58 -0
  59. data/lib/active_storage/previewer.rb +84 -0
  60. data/lib/active_storage/previewer/mupdf_previewer.rb +36 -0
  61. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +35 -0
  62. data/lib/active_storage/previewer/video_previewer.rb +26 -0
  63. data/lib/active_storage/reflection.rb +64 -0
  64. data/lib/active_storage/service.rb +141 -0
  65. data/lib/active_storage/service/azure_storage_service.rb +165 -0
  66. data/lib/active_storage/service/configurator.rb +34 -0
  67. data/lib/active_storage/service/disk_service.rb +166 -0
  68. data/lib/active_storage/service/gcs_service.rb +141 -0
  69. data/lib/active_storage/service/mirror_service.rb +55 -0
  70. data/lib/active_storage/service/s3_service.rb +116 -0
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
  72. data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
  73. data/lib/active_storage/transformers/transformer.rb +42 -0
  74. data/lib/active_storage/version.rb +10 -0
  75. data/lib/tasks/activestorage.rake +22 -0
  76. 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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc:
4
+ attribute :host
5
+ 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