activestorage 7.0.8 → 7.1.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +188 -271
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +6 -6
  5. data/app/assets/javascripts/activestorage.esm.js +11 -7
  6. data/app/assets/javascripts/activestorage.js +12 -6
  7. data/app/controllers/active_storage/blobs/proxy_controller.rb +1 -0
  8. data/app/controllers/active_storage/disk_controller.rb +4 -2
  9. data/app/controllers/active_storage/representations/proxy_controller.rb +2 -1
  10. data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
  11. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  12. data/app/javascript/activestorage/blob_record.js +4 -1
  13. data/app/javascript/activestorage/direct_upload.js +3 -2
  14. data/app/javascript/activestorage/index.js +3 -1
  15. data/app/javascript/activestorage/ujs.js +3 -3
  16. data/app/jobs/active_storage/analyze_job.rb +1 -1
  17. data/app/jobs/active_storage/mirror_job.rb +1 -1
  18. data/app/jobs/active_storage/purge_job.rb +1 -1
  19. data/app/jobs/active_storage/transform_job.rb +12 -0
  20. data/app/models/active_storage/attachment.rb +90 -15
  21. data/app/models/active_storage/blob/analyzable.rb +4 -3
  22. data/app/models/active_storage/blob/identifiable.rb +1 -0
  23. data/app/models/active_storage/blob/representable.rb +7 -3
  24. data/app/models/active_storage/blob/servable.rb +22 -0
  25. data/app/models/active_storage/blob.rb +31 -67
  26. data/app/models/active_storage/current.rb +0 -10
  27. data/app/models/active_storage/filename.rb +2 -0
  28. data/app/models/active_storage/named_variant.rb +21 -0
  29. data/app/models/active_storage/preview.rb +11 -4
  30. data/app/models/active_storage/variant.rb +10 -7
  31. data/app/models/active_storage/variant_record.rb +0 -2
  32. data/app/models/active_storage/variant_with_record.rb +21 -7
  33. data/app/models/active_storage/variation.rb +5 -3
  34. data/config/routes.rb +6 -4
  35. data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
  36. data/lib/active_storage/analyzer/audio_analyzer.rb +16 -4
  37. data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
  38. data/lib/active_storage/analyzer/video_analyzer.rb +3 -1
  39. data/lib/active_storage/analyzer.rb +2 -0
  40. data/lib/active_storage/attached/changes/create_many.rb +8 -3
  41. data/lib/active_storage/attached/changes/create_one.rb +45 -3
  42. data/lib/active_storage/attached/many.rb +5 -4
  43. data/lib/active_storage/attached/model.rb +72 -43
  44. data/lib/active_storage/attached/one.rb +5 -4
  45. data/lib/active_storage/attached.rb +2 -0
  46. data/lib/active_storage/deprecator.rb +7 -0
  47. data/lib/active_storage/engine.rb +11 -7
  48. data/lib/active_storage/fixture_set.rb +7 -1
  49. data/lib/active_storage/gem_version.rb +4 -4
  50. data/lib/active_storage/log_subscriber.rb +12 -0
  51. data/lib/active_storage/previewer.rb +8 -1
  52. data/lib/active_storage/reflection.rb +3 -3
  53. data/lib/active_storage/service/azure_storage_service.rb +2 -0
  54. data/lib/active_storage/service/disk_service.rb +2 -0
  55. data/lib/active_storage/service/gcs_service.rb +11 -20
  56. data/lib/active_storage/service/mirror_service.rb +10 -5
  57. data/lib/active_storage/service/s3_service.rb +2 -0
  58. data/lib/active_storage/service.rb +4 -2
  59. data/lib/active_storage/transformers/transformer.rb +2 -0
  60. data/lib/active_storage/version.rb +1 -1
  61. data/lib/active_storage.rb +19 -3
  62. metadata +19 -28
@@ -508,7 +508,7 @@ function toArray(value) {
508
508
  }
509
509
 
510
510
  class BlobRecord {
511
- constructor(file, checksum, url) {
511
+ constructor(file, checksum, url, customHeaders = {}) {
512
512
  this.file = file;
513
513
  this.attributes = {
514
514
  filename: file.name,
@@ -522,6 +522,9 @@ class BlobRecord {
522
522
  this.xhr.setRequestHeader("Content-Type", "application/json");
523
523
  this.xhr.setRequestHeader("Accept", "application/json");
524
524
  this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
525
+ Object.keys(customHeaders).forEach((headerKey => {
526
+ this.xhr.setRequestHeader(headerKey, customHeaders[headerKey]);
527
+ }));
525
528
  const csrfToken = getMetaValue("csrf-token");
526
529
  if (csrfToken != undefined) {
527
530
  this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
@@ -604,11 +607,12 @@ class BlobUpload {
604
607
  let id = 0;
605
608
 
606
609
  class DirectUpload {
607
- constructor(file, url, delegate) {
610
+ constructor(file, url, delegate, customHeaders = {}) {
608
611
  this.id = ++id;
609
612
  this.file = file;
610
613
  this.url = url;
611
614
  this.delegate = delegate;
615
+ this.customHeaders = customHeaders;
612
616
  }
613
617
  create(callback) {
614
618
  FileChecksum.create(this.file, ((error, checksum) => {
@@ -616,7 +620,7 @@ class DirectUpload {
616
620
  callback(error);
617
621
  return;
618
622
  }
619
- const blob = new BlobRecord(this.file, checksum, this.url);
623
+ const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders);
620
624
  notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
621
625
  blob.create((error => {
622
626
  if (error) {
@@ -767,9 +771,9 @@ function start() {
767
771
  }
768
772
 
769
773
  function didClick(event) {
770
- const {target: target} = event;
771
- if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
772
- submitButtonsByForm.set(target.form, target);
774
+ const button = event.target.closest("button, input");
775
+ if (button && button.type === "submit" && button.form) {
776
+ submitButtonsByForm.set(button.form, button);
773
777
  }
774
778
  }
775
779
 
@@ -841,4 +845,4 @@ function autostart() {
841
845
 
842
846
  setTimeout(autostart, 1);
843
847
 
844
- export { DirectUpload, start };
848
+ export { DirectUpload, DirectUploadController, DirectUploadsController, start };
@@ -503,7 +503,7 @@
503
503
  }
504
504
  }
505
505
  class BlobRecord {
506
- constructor(file, checksum, url) {
506
+ constructor(file, checksum, url, customHeaders = {}) {
507
507
  this.file = file;
508
508
  this.attributes = {
509
509
  filename: file.name,
@@ -517,6 +517,9 @@
517
517
  this.xhr.setRequestHeader("Content-Type", "application/json");
518
518
  this.xhr.setRequestHeader("Accept", "application/json");
519
519
  this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
520
+ Object.keys(customHeaders).forEach((headerKey => {
521
+ this.xhr.setRequestHeader(headerKey, customHeaders[headerKey]);
522
+ }));
520
523
  const csrfToken = getMetaValue("csrf-token");
521
524
  if (csrfToken != undefined) {
522
525
  this.xhr.setRequestHeader("X-CSRF-Token", csrfToken);
@@ -596,11 +599,12 @@
596
599
  }
597
600
  let id = 0;
598
601
  class DirectUpload {
599
- constructor(file, url, delegate) {
602
+ constructor(file, url, delegate, customHeaders = {}) {
600
603
  this.id = ++id;
601
604
  this.file = file;
602
605
  this.url = url;
603
606
  this.delegate = delegate;
607
+ this.customHeaders = customHeaders;
604
608
  }
605
609
  create(callback) {
606
610
  FileChecksum.create(this.file, ((error, checksum) => {
@@ -608,7 +612,7 @@
608
612
  callback(error);
609
613
  return;
610
614
  }
611
- const blob = new BlobRecord(this.file, checksum, this.url);
615
+ const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders);
612
616
  notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr);
613
617
  blob.create((error => {
614
618
  if (error) {
@@ -750,9 +754,9 @@
750
754
  }
751
755
  }
752
756
  function didClick(event) {
753
- const {target: target} = event;
754
- if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
755
- submitButtonsByForm.set(target.form, target);
757
+ const button = event.target.closest("button, input");
758
+ if (button && button.type === "submit" && button.form) {
759
+ submitButtonsByForm.set(button.form, button);
756
760
  }
757
761
  }
758
762
  function didSubmitForm(event) {
@@ -816,6 +820,8 @@
816
820
  }
817
821
  setTimeout(autostart, 1);
818
822
  exports.DirectUpload = DirectUpload;
823
+ exports.DirectUploadController = DirectUploadController;
824
+ exports.DirectUploadsController = DirectUploadsController;
819
825
  exports.start = start;
820
826
  Object.defineProperty(exports, "__esModule", {
821
827
  value: true
@@ -9,6 +9,7 @@
9
9
  class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
10
10
  include ActiveStorage::SetBlob
11
11
  include ActiveStorage::Streaming
12
+ include ActiveStorage::DisableSession
12
13
 
13
14
  def show
14
15
  if request.headers["Range"].present?
@@ -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)
@@ -8,10 +8,11 @@
8
8
  # {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
9
9
  class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
10
10
  include ActiveStorage::Streaming
11
+ include ActiveStorage::DisableSession
11
12
 
12
13
  def show
13
14
  http_cache_forever public: true do
14
- send_blob_stream @representation.image, disposition: params[:disposition]
15
+ send_blob_stream @representation, disposition: params[:disposition]
15
16
  end
16
17
  end
17
18
  end
@@ -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
@@ -1,7 +1,7 @@
1
1
  import { getMetaValue } from "./helpers"
2
2
 
3
3
  export class BlobRecord {
4
- constructor(file, checksum, url) {
4
+ constructor(file, checksum, url, customHeaders = {}) {
5
5
  this.file = file
6
6
 
7
7
  this.attributes = {
@@ -17,6 +17,9 @@ export class BlobRecord {
17
17
  this.xhr.setRequestHeader("Content-Type", "application/json")
18
18
  this.xhr.setRequestHeader("Accept", "application/json")
19
19
  this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
20
+ Object.keys(customHeaders).forEach((headerKey) => {
21
+ this.xhr.setRequestHeader(headerKey, customHeaders[headerKey])
22
+ })
20
23
 
21
24
  const csrfToken = getMetaValue("csrf-token")
22
25
  if (csrfToken != undefined) {
@@ -5,11 +5,12 @@ import { BlobUpload } from "./blob_upload"
5
5
  let id = 0
6
6
 
7
7
  export class DirectUpload {
8
- constructor(file, url, delegate) {
8
+ constructor(file, url, delegate, customHeaders = {}) {
9
9
  this.id = ++id
10
10
  this.file = file
11
11
  this.url = url
12
12
  this.delegate = delegate
13
+ this.customHeaders = customHeaders
13
14
  }
14
15
 
15
16
  create(callback) {
@@ -19,7 +20,7 @@ export class DirectUpload {
19
20
  return
20
21
  }
21
22
 
22
- const blob = new BlobRecord(this.file, checksum, this.url)
23
+ const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders)
23
24
  notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
24
25
 
25
26
  blob.create(error => {
@@ -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) {
@@ -15,9 +15,9 @@ export function start() {
15
15
  }
16
16
 
17
17
  function didClick(event) {
18
- const { target } = event
19
- if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
20
- submitButtonsByForm.set(target.form, target)
18
+ const button = event.target.closest("button, input")
19
+ if (button && button.type === "submit" && button.form) {
20
+ submitButtonsByForm.set(button.form, button)
21
21
  }
22
22
  }
23
23
 
@@ -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, ActiveStorage::UnrepresentableError
7
+ retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
8
+
9
+ def perform(blob, transformations)
10
+ blob.representation(transformations).processed
11
+ end
12
+ end
@@ -2,6 +2,8 @@
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.
@@ -16,18 +18,34 @@ require "active_support/core_ext/module/delegation"
16
18
  # # preloads blobs and variant records (if using `ActiveStorage.track_variants`)
17
19
  # User.first.avatars.with_all_variant_records
18
20
  class ActiveStorage::Attachment < ActiveStorage::Record
19
- self.table_name = "active_storage_attachments"
20
-
21
+ ##
22
+ # :method:
23
+ #
24
+ # Returns the associated record.
21
25
  belongs_to :record, polymorphic: true, touch: true
26
+
27
+ ##
28
+ # :method:
29
+ #
30
+ # Returns the associated ActiveStorage::Blob.
22
31
  belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
23
32
 
24
33
  delegate_missing_to :blob
25
34
  delegate :signed_id, to: :blob
26
35
 
27
- after_create_commit :mirror_blob_later, :analyze_blob_later
36
+ after_create_commit :mirror_blob_later, :analyze_blob_later, :transform_variants_later
28
37
  after_destroy_commit :purge_dependent_blob_later
29
38
 
30
- scope :with_all_variant_records, -> { includes(blob: :variant_records) }
39
+ ##
40
+ # :singleton-method:
41
+ #
42
+ # Eager load all variant records on an attachment at once.
43
+ #
44
+ # User.first.avatars.with_all_variant_records
45
+ scope :with_all_variant_records, -> { includes(blob: {
46
+ variant_records: { image_attachment: :blob },
47
+ preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
48
+ }) }
31
49
 
32
50
  # Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
33
51
  def purge
@@ -49,23 +67,61 @@ class ActiveStorage::Attachment < ActiveStorage::Record
49
67
 
50
68
  # Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
51
69
  # instance for the attachment with the set of +transformations+ provided.
70
+ # Example:
71
+ #
72
+ # avatar.variant(resize_to_limit: [100, 100]).processed.url
73
+ #
74
+ # or if you are using pre-defined variants:
75
+ #
76
+ # avatar.variant(:thumb).processed.url
77
+ #
52
78
  # See ActiveStorage::Blob::Representable#variant for more information.
53
79
  #
54
80
  # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
55
81
  # unknown pre-defined variant of the attachment.
56
82
  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
-
83
+ transformations = transformations_by_name(transformations)
66
84
  blob.variant(transformations)
67
85
  end
68
86
 
87
+ # Returns an ActiveStorage::Preview instance for the attachment with the set
88
+ # of +transformations+ provided.
89
+ # Example:
90
+ #
91
+ # video.preview(resize_to_limit: [100, 100]).processed.url
92
+ #
93
+ # or if you are using pre-defined variants:
94
+ #
95
+ # video.preview(:thumb).processed.url
96
+ #
97
+ # See ActiveStorage::Blob::Representable#preview for more information.
98
+ #
99
+ # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
100
+ # unknown pre-defined variant of the attachment.
101
+ def preview(transformations)
102
+ transformations = transformations_by_name(transformations)
103
+ blob.preview(transformations)
104
+ end
105
+
106
+ # Returns an ActiveStorage::Preview or an ActiveStorage::Variant for the
107
+ # attachment with set of +transformations+ provided.
108
+ # Example:
109
+ #
110
+ # avatar.representation(resize_to_limit: [100, 100]).processed.url
111
+ #
112
+ # or if you are using pre-defined variants:
113
+ #
114
+ # avatar.representation(:thumb).processed.url
115
+ #
116
+ # See ActiveStorage::Blob::Representable#representation for more information.
117
+ #
118
+ # Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
119
+ # unknown pre-defined variant of the attachment.
120
+ def representation(transformations)
121
+ transformations = transformations_by_name(transformations)
122
+ blob.representation(transformations)
123
+ end
124
+
69
125
  private
70
126
  def analyze_blob_later
71
127
  blob.analyze_later unless blob.analyzed?
@@ -75,6 +131,12 @@ class ActiveStorage::Attachment < ActiveStorage::Record
75
131
  blob.mirror_later
76
132
  end
77
133
 
134
+ def transform_variants_later
135
+ named_variants.each do |_name, named_variant|
136
+ blob.preprocessed(named_variant.transformations) if named_variant.preprocessed?(record)
137
+ end
138
+ end
139
+
78
140
  def purge_dependent_blob_later
79
141
  blob&.purge_later if dependent == :purge_later
80
142
  end
@@ -83,8 +145,21 @@ class ActiveStorage::Attachment < ActiveStorage::Record
83
145
  record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
84
146
  end
85
147
 
86
- def variants
87
- record.attachment_reflections[name]&.variants
148
+ def named_variants
149
+ record.attachment_reflections[name]&.named_variants
150
+ end
151
+
152
+ def transformations_by_name(transformations)
153
+ case transformations
154
+ when Symbol
155
+ variant_name = transformations
156
+ named_variants.fetch(variant_name) do
157
+ record_model_name = record.to_model.model_name.name
158
+ raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
159
+ end.transformations
160
+ else
161
+ transformations
162
+ end
88
163
  end
89
164
  end
90
165
 
@@ -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) if representable?
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
 
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage::Blob::Servable # :nodoc:
4
+ def content_type_for_serving
5
+ forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
6
+ end
7
+
8
+ def forced_disposition_for_serving
9
+ if forcibly_serve_as_binary? || !allowed_inline?
10
+ :attachment
11
+ end
12
+ end
13
+
14
+ private
15
+ def forcibly_serve_as_binary?
16
+ ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
17
+ end
18
+
19
+ def allowed_inline?
20
+ ActiveStorage.content_types_allowed_inline.include?(content_type)
21
+ end
22
+ end