activestorage 7.0.0 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +158 -178
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +7 -7
  5. data/app/assets/javascripts/activestorage.esm.js +10 -18
  6. data/app/assets/javascripts/activestorage.js +11 -17
  7. data/app/controllers/active_storage/base_controller.rb +1 -1
  8. data/app/controllers/active_storage/blobs/proxy_controller.rb +2 -0
  9. data/app/controllers/active_storage/direct_uploads_controller.rb +1 -7
  10. data/app/controllers/active_storage/disk_controller.rb +4 -2
  11. data/app/controllers/active_storage/representations/proxy_controller.rb +3 -0
  12. data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
  13. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  14. data/app/controllers/concerns/active_storage/streaming.rb +1 -0
  15. data/app/javascript/activestorage/blob_record.js +6 -10
  16. data/app/javascript/activestorage/direct_upload.js +3 -4
  17. data/app/javascript/activestorage/direct_upload_controller.js +1 -9
  18. data/app/javascript/activestorage/index.js +3 -1
  19. data/app/jobs/active_storage/analyze_job.rb +1 -1
  20. data/app/jobs/active_storage/mirror_job.rb +1 -1
  21. data/app/jobs/active_storage/purge_job.rb +1 -1
  22. data/app/jobs/active_storage/transform_job.rb +12 -0
  23. data/app/models/active_storage/attachment.rb +88 -14
  24. data/app/models/active_storage/blob/analyzable.rb +4 -3
  25. data/app/models/active_storage/blob/identifiable.rb +1 -0
  26. data/app/models/active_storage/blob/representable.rb +7 -3
  27. data/app/models/active_storage/blob.rb +27 -47
  28. data/app/models/active_storage/current.rb +0 -10
  29. data/app/models/active_storage/filename.rb +2 -0
  30. data/app/models/active_storage/named_variant.rb +21 -0
  31. data/app/models/active_storage/preview.rb +5 -3
  32. data/app/models/active_storage/variant.rb +11 -10
  33. data/app/models/active_storage/variant_with_record.rb +20 -8
  34. data/app/models/active_storage/variation.rb +6 -4
  35. data/config/routes.rb +6 -4
  36. data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
  37. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +4 -0
  38. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +2 -0
  39. data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +2 -0
  40. data/lib/active_storage/analyzer/audio_analyzer.rb +17 -5
  41. data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +9 -7
  42. data/lib/active_storage/analyzer/image_analyzer/vips.rb +9 -7
  43. data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
  44. data/lib/active_storage/analyzer/video_analyzer.rb +15 -6
  45. data/lib/active_storage/analyzer.rb +2 -0
  46. data/lib/active_storage/attached/changes/create_many.rb +8 -3
  47. data/lib/active_storage/attached/changes/create_one.rb +45 -3
  48. data/lib/active_storage/attached/many.rb +5 -4
  49. data/lib/active_storage/attached/model.rb +66 -43
  50. data/lib/active_storage/attached/one.rb +5 -4
  51. data/lib/active_storage/attached.rb +2 -0
  52. data/lib/active_storage/deprecator.rb +7 -0
  53. data/lib/active_storage/engine.rb +31 -9
  54. data/lib/active_storage/errors.rb +0 -3
  55. data/lib/active_storage/fixture_set.rb +7 -8
  56. data/lib/active_storage/gem_version.rb +2 -2
  57. data/lib/active_storage/log_subscriber.rb +12 -0
  58. data/lib/active_storage/previewer/video_previewer.rb +2 -0
  59. data/lib/active_storage/previewer.rb +8 -1
  60. data/lib/active_storage/reflection.rb +3 -3
  61. data/lib/active_storage/service/azure_storage_service.rb +2 -0
  62. data/lib/active_storage/service/disk_service.rb +2 -0
  63. data/lib/active_storage/service/gcs_service.rb +11 -20
  64. data/lib/active_storage/service/mirror_service.rb +10 -5
  65. data/lib/active_storage/service/s3_service.rb +2 -0
  66. data/lib/active_storage/service.rb +4 -2
  67. data/lib/active_storage/transformers/image_processing_transformer.rb +65 -0
  68. data/lib/active_storage/transformers/transformer.rb +2 -0
  69. data/lib/active_storage/version.rb +1 -1
  70. data/lib/active_storage.rb +310 -4
  71. metadata +21 -32
  72. data/lib/active_storage/direct_upload_token.rb +0 -59
@@ -503,7 +503,7 @@
503
503
  }
504
504
  }
505
505
  class BlobRecord {
506
- constructor(file, checksum, url, directUploadToken, attachmentName) {
506
+ constructor(file, checksum, url, customHeaders = {}) {
507
507
  this.file = file;
508
508
  this.attributes = {
509
509
  filename: file.name,
@@ -511,14 +511,15 @@
511
511
  byte_size: file.size,
512
512
  checksum: checksum
513
513
  };
514
- this.directUploadToken = directUploadToken;
515
- this.attachmentName = attachmentName;
516
514
  this.xhr = new XMLHttpRequest;
517
515
  this.xhr.open("POST", url, true);
518
516
  this.xhr.responseType = "json";
519
517
  this.xhr.setRequestHeader("Content-Type", "application/json");
520
518
  this.xhr.setRequestHeader("Accept", "application/json");
521
519
  this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
520
+ Object.keys(customHeaders).forEach((headerKey => {
521
+ this.xhr.setRequestHeader(headerKey, customHeaders[headerKey]);
522
+ }));
522
523
  const csrfToken = getMetaValue("csrf-token");
523
524
  if (csrfToken != undefined) {
524
525
  this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
@@ -540,9 +541,7 @@
540
541
  create(callback) {
541
542
  this.callback = callback;
542
543
  this.xhr.send(JSON.stringify({
543
- blob: this.attributes,
544
- direct_upload_token: this.directUploadToken,
545
- attachment_name: this.attachmentName
544
+ blob: this.attributes
546
545
  }));
547
546
  }
548
547
  requestDidLoad(event) {
@@ -600,13 +599,12 @@
600
599
  }
601
600
  let id = 0;
602
601
  class DirectUpload {
603
- constructor(file, url, serviceName, attachmentName, delegate) {
602
+ constructor(file, url, delegate, customHeaders = {}) {
604
603
  this.id = ++id;
605
604
  this.file = file;
606
605
  this.url = url;
607
- this.serviceName = serviceName;
608
- this.attachmentName = attachmentName;
609
606
  this.delegate = delegate;
607
+ this.customHeaders = customHeaders;
610
608
  }
611
609
  create(callback) {
612
610
  FileChecksum.create(this.file, ((error, checksum) => {
@@ -614,7 +612,7 @@
614
612
  callback(error);
615
613
  return;
616
614
  }
617
- const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName);
615
+ const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders);
618
616
  notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
619
617
  blob.create((error => {
620
618
  if (error) {
@@ -643,7 +641,7 @@
643
641
  constructor(input, file) {
644
642
  this.input = input;
645
643
  this.file = file;
646
- this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this);
644
+ this.directUpload = new DirectUpload(this.file, this.url, this);
647
645
  this.dispatch("initialize");
648
646
  }
649
647
  start(callback) {
@@ -674,12 +672,6 @@
674
672
  get url() {
675
673
  return this.input.getAttribute("data-direct-upload-url");
676
674
  }
677
- get directUploadToken() {
678
- return this.input.getAttribute("data-direct-upload-token");
679
- }
680
- get attachmentName() {
681
- return this.input.getAttribute("data-direct-upload-attachment-name");
682
- }
683
675
  dispatch(name, detail = {}) {
684
676
  detail.file = this.file;
685
677
  detail.id = this.directUpload.id;
@@ -828,6 +820,8 @@
828
820
  }
829
821
  setTimeout(autostart, 1);
830
822
  exports.DirectUpload = DirectUpload;
823
+ exports.DirectUploadController = DirectUploadController;
824
+ exports.DirectUploadsController = DirectUploadsController;
831
825
  exports.start = start;
832
826
  Object.defineProperty(exports, "__esModule", {
833
827
  value: true
@@ -2,7 +2,7 @@
2
2
 
3
3
  # The base class for all Active Storage controllers.
4
4
  class ActiveStorage::BaseController < ActionController::Base
5
- include ActiveStorage::SetCurrent, ActiveStorage::Streaming
5
+ include ActiveStorage::SetCurrent
6
6
 
7
7
  protect_from_forgery with: :exception
8
8
 
@@ -8,6 +8,8 @@
8
8
  # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
9
9
  class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
10
10
  include ActiveStorage::SetBlob
11
+ include ActiveStorage::Streaming
12
+ include ActiveStorage::DisableSession
11
13
 
12
14
  def show
13
15
  if request.headers["Range"].present?
@@ -4,10 +4,8 @@
4
4
  # When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference
5
5
  # the blob that was created up front.
6
6
  class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
7
- include ActiveStorage::DirectUploadToken
8
-
9
7
  def create
10
- blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args.merge(service_name: verified_service_name))
8
+ blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args)
11
9
  render json: direct_upload_json(blob)
12
10
  end
13
11
 
@@ -16,10 +14,6 @@ class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
16
14
  params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys
17
15
  end
18
16
 
19
- def verified_service_name
20
- ActiveStorage::DirectUploadToken.verify_direct_upload_token(params[:direct_upload_token], params[:attachment_name], session)
21
- end
22
-
23
17
  def direct_upload_json(blob)
24
18
  blob.as_json(root: false, methods: :signed_id).merge(direct_upload: {
25
19
  url: blob.service_url_for_direct_upload,
@@ -42,11 +42,13 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
42
42
  end
43
43
 
44
44
  def decode_verified_key
45
- ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
45
+ key = ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
46
+ key&.deep_symbolize_keys
46
47
  end
47
48
 
48
49
  def decode_verified_token
49
- ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
50
+ token = ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
51
+ token&.deep_symbolize_keys
50
52
  end
51
53
 
52
54
  def acceptable_content?(token)
@@ -7,6 +7,9 @@
7
7
  # require a higher level of protection consider implementing
8
8
  # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
9
9
  class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
10
+ include ActiveStorage::Streaming
11
+ include ActiveStorage::DisableSession
12
+
10
13
  def show
11
14
  http_cache_forever public: true do
12
15
  send_blob_stream @representation.image, disposition: params[:disposition]
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This concern disables the session in order to allow caching by default in some CDNs as CloudFlare.
4
+ module ActiveStorage::DisableSession
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action do
9
+ request.session_options[:skip] = true
10
+ end
11
+ end
12
+ end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/hash/except"
4
+
3
5
  module ActiveStorage::FileServer # :nodoc:
4
6
  private
5
7
  def serve_file(path, content_type:, disposition:)
6
- Rack::File.new(nil).serving(request, path).tap do |(status, headers, body)|
8
+ ::Rack::Files.new(nil).serving(request, path).tap do |(status, headers, body)|
7
9
  self.status = status
8
10
  self.response_body = body
9
11
 
@@ -11,6 +13,7 @@ module ActiveStorage::FileServer # :nodoc:
11
13
  response.headers[name] = value
12
14
  end
13
15
 
16
+ response.headers.except!("X-Cascade", "x-cascade") if status == 416
14
17
  response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
15
18
  response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
16
19
  end
@@ -3,6 +3,7 @@
3
3
  require "securerandom"
4
4
 
5
5
  module ActiveStorage::Streaming
6
+ extend ActiveSupport::Concern
6
7
  DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
7
8
 
8
9
  include ActionController::DataStreaming
@@ -1,25 +1,25 @@
1
1
  import { getMetaValue } from "./helpers"
2
2
 
3
3
  export class BlobRecord {
4
- constructor(file, checksum, url, directUploadToken, attachmentName) {
4
+ constructor(file, checksum, url, customHeaders = {}) {
5
5
  this.file = file
6
6
 
7
7
  this.attributes = {
8
8
  filename: file.name,
9
9
  content_type: file.type || "application/octet-stream",
10
10
  byte_size: file.size,
11
- checksum: checksum,
11
+ checksum: checksum
12
12
  }
13
13
 
14
- this.directUploadToken = directUploadToken
15
- this.attachmentName = attachmentName
16
-
17
14
  this.xhr = new XMLHttpRequest
18
15
  this.xhr.open("POST", url, true)
19
16
  this.xhr.responseType = "json"
20
17
  this.xhr.setRequestHeader("Content-Type", "application/json")
21
18
  this.xhr.setRequestHeader("Accept", "application/json")
22
19
  this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
20
+ Object.keys(customHeaders).forEach((headerKey) => {
21
+ this.xhr.setRequestHeader(headerKey, customHeaders[headerKey])
22
+ })
23
23
 
24
24
  const csrfToken = getMetaValue("csrf-token")
25
25
  if (csrfToken != undefined) {
@@ -46,11 +46,7 @@ export class BlobRecord {
46
46
 
47
47
  create(callback) {
48
48
  this.callback = callback
49
- this.xhr.send(JSON.stringify({
50
- blob: this.attributes,
51
- direct_upload_token: this.directUploadToken,
52
- attachment_name: this.attachmentName
53
- }))
49
+ this.xhr.send(JSON.stringify({ blob: this.attributes }))
54
50
  }
55
51
 
56
52
  requestDidLoad(event) {
@@ -5,13 +5,12 @@ import { BlobUpload } from "./blob_upload"
5
5
  let id = 0
6
6
 
7
7
  export class DirectUpload {
8
- constructor(file, url, serviceName, attachmentName, delegate) {
8
+ constructor(file, url, delegate, customHeaders = {}) {
9
9
  this.id = ++id
10
10
  this.file = file
11
11
  this.url = url
12
- this.serviceName = serviceName
13
- this.attachmentName = attachmentName
14
12
  this.delegate = delegate
13
+ this.customHeaders = customHeaders
15
14
  }
16
15
 
17
16
  create(callback) {
@@ -21,7 +20,7 @@ export class DirectUpload {
21
20
  return
22
21
  }
23
22
 
24
- const blob = new BlobRecord(this.file, checksum, this.url, this.serviceName, this.attachmentName)
23
+ const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders)
25
24
  notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
26
25
 
27
26
  blob.create(error => {
@@ -5,7 +5,7 @@ export class DirectUploadController {
5
5
  constructor(input, file) {
6
6
  this.input = input
7
7
  this.file = file
8
- this.directUpload = new DirectUpload(this.file, this.url, this.directUploadToken, this.attachmentName, this)
8
+ this.directUpload = new DirectUpload(this.file, this.url, this)
9
9
  this.dispatch("initialize")
10
10
  }
11
11
 
@@ -41,14 +41,6 @@ export class DirectUploadController {
41
41
  return this.input.getAttribute("data-direct-upload-url")
42
42
  }
43
43
 
44
- get directUploadToken() {
45
- return this.input.getAttribute("data-direct-upload-token")
46
- }
47
-
48
- get attachmentName() {
49
- return this.input.getAttribute("data-direct-upload-attachment-name")
50
- }
51
-
52
44
  dispatch(name, detail = {}) {
53
45
  detail.file = this.file
54
46
  detail.id = this.directUpload.id
@@ -1,6 +1,8 @@
1
1
  import { start } from "./ujs"
2
2
  import { DirectUpload } from "./direct_upload"
3
- export { start, DirectUpload }
3
+ import { DirectUploadController } from "./direct_upload_controller"
4
+ import { DirectUploadsController } from "./direct_uploads_controller"
5
+ export { start, DirectUpload, DirectUploadController, DirectUploadsController }
4
6
 
5
7
  function autostart() {
6
8
  if (window.ActiveStorage) {
@@ -5,7 +5,7 @@ class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
5
5
  queue_as { ActiveStorage.queues[:analysis] }
6
6
 
7
7
  discard_on ActiveRecord::RecordNotFound
8
- retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
8
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
9
9
 
10
10
  def perform(blob)
11
11
  blob.analyze
@@ -7,7 +7,7 @@ class ActiveStorage::MirrorJob < ActiveStorage::BaseJob
7
7
  queue_as { ActiveStorage.queues[:mirror] }
8
8
 
9
9
  discard_on ActiveStorage::FileNotFoundError
10
- retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :exponentially_longer
10
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
11
11
 
12
12
  def perform(key, checksum:)
13
13
  ActiveStorage::Blob.service.try(:mirror, key, checksum: checksum)
@@ -5,7 +5,7 @@ class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
5
5
  queue_as { ActiveStorage.queues[:purge] }
6
6
 
7
7
  discard_on ActiveRecord::RecordNotFound
8
- retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :exponentially_longer
8
+ retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :polynomially_longer
9
9
 
10
10
  def perform(blob)
11
11
  blob.purge
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::TransformJob < ActiveStorage::BaseJob
4
+ queue_as { ActiveStorage.queues[:transform] }
5
+
6
+ discard_on ActiveRecord::RecordNotFound
7
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
8
+
9
+ def perform(blob, transformations)
10
+ blob.variant(transformations).processed
11
+ end
12
+ end
@@ -2,11 +2,13 @@
2
2
 
3
3
  require "active_support/core_ext/module/delegation"
4
4
 
5
+ # = Active Storage \Attachment
6
+ #
5
7
  # Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
6
8
  # but it is possible to associate many different records with the same blob. A foreign-key constraint
7
9
  # on the attachments table prevents blobs from being purged if they’re still attached to any records.
8
10
  #
9
- # Attachments also have access to all methods from {ActiveStorage::Blob}[rdoc-ref:ActiveStorage::Blob].
11
+ # Attachments also have access to all methods from ActiveStorage::Blob.
10
12
  #
11
13
  # If you wish to preload attachments or blobs, you can use these scopes:
12
14
  #
@@ -18,16 +20,31 @@ require "active_support/core_ext/module/delegation"
18
20
  class ActiveStorage::Attachment < ActiveStorage::Record
19
21
  self.table_name = "active_storage_attachments"
20
22
 
23
+ ##
24
+ # :method:
25
+ #
26
+ # Returns the associated record.
21
27
  belongs_to :record, polymorphic: true, touch: true
28
+
29
+ ##
30
+ # :method:
31
+ #
32
+ # Returns the associated ActiveStorage::Blob.
22
33
  belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
23
34
 
24
35
  delegate_missing_to :blob
25
36
  delegate :signed_id, to: :blob
26
37
 
27
- after_create_commit :mirror_blob_later, :analyze_blob_later
38
+ after_create_commit :mirror_blob_later, :analyze_blob_later, :transform_variants_later
28
39
  after_destroy_commit :purge_dependent_blob_later
29
40
 
30
- scope :with_all_variant_records, -> { includes(blob: :variant_records) }
41
+ ##
42
+ # :singleton-method:
43
+ #
44
+ # Eager load all variant records on an attachment at once.
45
+ #
46
+ # User.first.avatars.with_all_variant_records
47
+ scope :with_all_variant_records, -> { includes(blob: { variant_records: { image_attachment: :blob } }) }
31
48
 
32
49
  # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
33
50
  def purge
@@ -49,23 +66,61 @@ class ActiveStorage::Attachment < ActiveStorage::Record
49
66
 
50
67
  # Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
51
68
  # instance for the attachment with the set of +transformations+ provided.
69
+ # Example:
70
+ #
71
+ # avatar.variant(resize_to_limit: [100, 100]).processed.url
72
+ #
73
+ # or if you are using pre-defined variants:
74
+ #
75
+ # avatar.variant(:thumb).processed.url
76
+ #
52
77
  # See ActiveStorage::Blob::Representable#variant for more information.
53
78
  #
54
79
  # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
55
80
  # unknown pre-defined variant of the attachment.
56
81
  def variant(transformations)
57
- case transformations
58
- when Symbol
59
- variant_name = transformations
60
- transformations = variants.fetch(variant_name) do
61
- record_model_name = record.to_model.model_name.name
62
- raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
63
- end
64
- end
65
-
82
+ transformations = transformations_by_name(transformations)
66
83
  blob.variant(transformations)
67
84
  end
68
85
 
86
+ # Returns an ActiveStorage::Preview instance for the attachment with the set
87
+ # of +transformations+ provided.
88
+ # Example:
89
+ #
90
+ # video.preview(resize_to_limit: [100, 100]).processed.url
91
+ #
92
+ # or if you are using pre-defined variants:
93
+ #
94
+ # video.preview(:thumb).processed.url
95
+ #
96
+ # See ActiveStorage::Blob::Representable#preview for more information.
97
+ #
98
+ # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
99
+ # unknown pre-defined variant of the attachment.
100
+ def preview(transformations)
101
+ transformations = transformations_by_name(transformations)
102
+ blob.preview(transformations)
103
+ end
104
+
105
+ # Returns an ActiveStorage::Preview or an ActiveStorage::Variant for the
106
+ # attachment with set of +transformations+ provided.
107
+ # Example:
108
+ #
109
+ # avatar.representation(resize_to_limit: [100, 100]).processed.url
110
+ #
111
+ # or if you are using pre-defined variants:
112
+ #
113
+ # avatar.representation(:thumb).processed.url
114
+ #
115
+ # See ActiveStorage::Blob::Representable#representation for more information.
116
+ #
117
+ # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
118
+ # unknown pre-defined variant of the attachment.
119
+ def representation(transformations)
120
+ transformations = transformations_by_name(transformations)
121
+ blob.representation(transformations)
122
+ end
123
+
69
124
  private
70
125
  def analyze_blob_later
71
126
  blob.analyze_later unless blob.analyzed?
@@ -75,6 +130,12 @@ class ActiveStorage::Attachment < ActiveStorage::Record
75
130
  blob.mirror_later
76
131
  end
77
132
 
133
+ def transform_variants_later
134
+ named_variants.each do |_name, named_variant|
135
+ blob.preprocessed(named_variant.transformations) if named_variant.preprocessed?(record)
136
+ end
137
+ end
138
+
78
139
  def purge_dependent_blob_later
79
140
  blob&.purge_later if dependent == :purge_later
80
141
  end
@@ -83,8 +144,21 @@ class ActiveStorage::Attachment < ActiveStorage::Record
83
144
  record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
84
145
  end
85
146
 
86
- def variants
87
- record.attachment_reflections[name]&.variants
147
+ def named_variants
148
+ record.attachment_reflections[name]&.named_variants
149
+ end
150
+
151
+ def transformations_by_name(transformations)
152
+ case transformations
153
+ when Symbol
154
+ variant_name = transformations
155
+ named_variants.fetch(variant_name) do
156
+ record_model_name = record.to_model.model_name.name
157
+ raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
158
+ end.transformations
159
+ else
160
+ transformations
161
+ end
88
162
  end
89
163
  end
90
164
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_storage/analyzer/null_analyzer"
4
4
 
5
+ # = Active Storage \Blob \Analyzable
5
6
  module ActiveStorage::Blob::Analyzable
6
7
  # Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
7
8
  # with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
@@ -12,7 +13,7 @@ module ActiveStorage::Blob::Analyzable
12
13
  # first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
13
14
  # metadata is extracted from it.
14
15
  #
15
- # In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
16
+ # In a \Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
16
17
  # in an initializer:
17
18
  #
18
19
  # # Add a custom analyzer for Microsoft Office documents:
@@ -21,9 +22,9 @@ module ActiveStorage::Blob::Analyzable
21
22
  # # Remove the built-in video analyzer:
22
23
  # Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
23
24
  #
24
- # Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
25
+ # Outside of a \Rails application, manipulate +ActiveStorage.analyzers+ instead.
25
26
  #
26
- # You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
27
+ # You won't ordinarily need to call this method from a \Rails application. New blobs are automatically and asynchronously
27
28
  # analyzed via #analyze_later when they're attached for the first time.
28
29
  def analyze
29
30
  update! metadata: metadata.merge(extract_metadata_via_analyzer)
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Blob \Identifiable
3
4
  module ActiveStorage::Blob::Identifiable
4
5
  def identify
5
6
  identify_without_saving
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mini_mime"
3
+ require "marcel"
4
4
 
5
5
  module ActiveStorage::Blob::Representable
6
6
  extend ActiveSupport::Concern
@@ -98,6 +98,10 @@ module ActiveStorage::Blob::Representable
98
98
  variable? || previewable?
99
99
  end
100
100
 
101
+ def preprocessed(transformations) # :nodoc:
102
+ ActiveStorage::TransformJob.perform_later(self, transformations)
103
+ end
104
+
101
105
  private
102
106
  def default_variant_transformations
103
107
  { format: default_variant_format }
@@ -112,10 +116,10 @@ module ActiveStorage::Blob::Representable
112
116
  end
113
117
 
114
118
  def format
115
- if filename.extension.present? && MiniMime.lookup_by_extension(filename.extension)&.content_type == content_type
119
+ if filename.extension.present? && Marcel::MimeType.for(extension: filename.extension) == content_type
116
120
  filename.extension
117
121
  else
118
- MiniMime.lookup_by_content_type(content_type)&.extension
122
+ Marcel::Magic.new(content_type.to_s).extensions.first
119
123
  end
120
124
  end
121
125