activestorage 7.0.10 → 7.1.0.beta1

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.

Potentially problematic release.


This version of activestorage might be problematic. Click here for more details.

Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +123 -381
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +5 -5
  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/disk_controller.rb +4 -2
  8. data/app/controllers/active_storage/representations/proxy_controller.rb +1 -1
  9. data/app/controllers/concerns/active_storage/file_server.rb +4 -1
  10. data/app/javascript/activestorage/blob_record.js +4 -1
  11. data/app/javascript/activestorage/direct_upload.js +3 -2
  12. data/app/javascript/activestorage/index.js +3 -1
  13. data/app/javascript/activestorage/ujs.js +3 -3
  14. data/app/jobs/active_storage/transform_job.rb +12 -0
  15. data/app/models/active_storage/attachment.rb +87 -13
  16. data/app/models/active_storage/blob/analyzable.rb +4 -3
  17. data/app/models/active_storage/blob/identifiable.rb +1 -0
  18. data/app/models/active_storage/blob/representable.rb +7 -3
  19. data/app/models/active_storage/blob.rb +44 -50
  20. data/app/models/active_storage/current.rb +0 -10
  21. data/app/models/active_storage/filename.rb +2 -0
  22. data/app/models/active_storage/named_variant.rb +21 -0
  23. data/app/models/active_storage/preview.rb +6 -8
  24. data/app/models/active_storage/variant.rb +8 -3
  25. data/app/models/active_storage/variant_with_record.rb +16 -11
  26. data/app/models/active_storage/variation.rb +5 -3
  27. data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
  28. data/lib/active_storage/analyzer/audio_analyzer.rb +16 -4
  29. data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
  30. data/lib/active_storage/analyzer/video_analyzer.rb +3 -1
  31. data/lib/active_storage/analyzer.rb +2 -0
  32. data/lib/active_storage/attached/changes/create_many.rb +8 -3
  33. data/lib/active_storage/attached/changes/create_one.rb +14 -2
  34. data/lib/active_storage/attached/many.rb +5 -4
  35. data/lib/active_storage/attached/model.rb +59 -42
  36. data/lib/active_storage/attached/one.rb +5 -4
  37. data/lib/active_storage/attached.rb +2 -0
  38. data/lib/active_storage/deprecator.rb +7 -0
  39. data/lib/active_storage/engine.rb +11 -7
  40. data/lib/active_storage/fixture_set.rb +2 -4
  41. data/lib/active_storage/gem_version.rb +4 -4
  42. data/lib/active_storage/log_subscriber.rb +12 -0
  43. data/lib/active_storage/previewer.rb +8 -1
  44. data/lib/active_storage/reflection.rb +3 -3
  45. data/lib/active_storage/service/azure_storage_service.rb +2 -0
  46. data/lib/active_storage/service/disk_service.rb +2 -0
  47. data/lib/active_storage/service/gcs_service.rb +11 -20
  48. data/lib/active_storage/service/mirror_service.rb +10 -5
  49. data/lib/active_storage/service/s3_service.rb +2 -0
  50. data/lib/active_storage/service.rb +4 -2
  51. data/lib/active_storage/transformers/transformer.rb +2 -0
  52. data/lib/active_storage/version.rb +1 -1
  53. data/lib/active_storage.rb +19 -3
  54. metadata +22 -31
  55. data/app/models/active_storage/blob/servable.rb +0 -22
@@ -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 button = event.target.closest("button, input");
771
- if (button && button.type === "submit" && button.form) {
772
- submitButtonsByForm.set(button.form, button);
774
+ const {target: target} = event;
775
+ if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
776
+ submitButtonsByForm.set(target.form, target);
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 button = event.target.closest("button, input");
754
- if (button && button.type === "submit" && button.form) {
755
- submitButtonsByForm.set(button.form, button);
757
+ const {target: target} = event;
758
+ if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
759
+ submitButtonsByForm.set(target.form, target);
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
@@ -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)
@@ -12,7 +12,7 @@ class ActiveStorage::Representations::ProxyController < ActiveStorage::Represent
12
12
 
13
13
  def show
14
14
  http_cache_forever public: true do
15
- send_blob_stream @representation, disposition: params[:disposition]
15
+ send_blob_stream @representation.image, disposition: params[:disposition]
16
16
  end
17
17
  end
18
18
  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 button = event.target.closest("button, input")
19
- if (button && button.type === "submit" && button.form) {
20
- submitButtonsByForm.set(button.form, button)
18
+ const { target } = event
19
+ if ((target.tagName == "INPUT" || target.tagName == "BUTTON") && target.type == "submit" && target.form) {
20
+ submitButtonsByForm.set(target.form, target)
21
21
  }
22
22
  }
23
23
 
@@ -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: :exponentially_longer
8
+
9
+ def perform(blob, transformations)
10
+ blob.variant(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.
@@ -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
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Blob
4
+ #
3
5
  # A blob is a record that contains the metadata about a file and a key for where that file resides on the service.
4
6
  # Blobs can be created in two ways:
5
7
  #
@@ -15,25 +17,9 @@
15
17
  # update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
16
18
  # If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
17
19
  class ActiveStorage::Blob < ActiveStorage::Record
18
- # We use constant paths in the following include calls to avoid a gotcha of
19
- # classic mode: If the parent application defines a top-level Analyzable, for
20
- # example, and ActiveStorage::Blob::Analyzable is not yet loaded, a bare
21
- #
22
- # include Analyzable
23
- #
24
- # would resolve to the top-level one, const_missing would not be triggered,
25
- # and therefore ActiveStorage::Blob::Analyzable would not be autoloaded.
26
- #
27
- # By using qualified names, we ensure const_missing is invoked if needed.
28
- # Please, note that Ruby 2.5 or newer is required, so Object is not checked
29
- # when looking up the ancestors of ActiveStorage::Blob.
30
- #
31
- # Zeitwerk mode does not have this gotcha. If we ever drop classic mode, this
32
- # can be simplified, bare constant names would just work.
33
- include ActiveStorage::Blob::Analyzable
34
- include ActiveStorage::Blob::Identifiable
35
- include ActiveStorage::Blob::Representable
36
- include ActiveStorage::Blob::Servable
20
+ include Analyzable
21
+ include Identifiable
22
+ include Representable
37
23
 
38
24
  self.table_name = "active_storage_blobs"
39
25
 
@@ -45,14 +31,24 @@ class ActiveStorage::Blob < ActiveStorage::Record
45
31
  class_attribute :services, default: {}
46
32
  class_attribute :service, instance_accessor: false
47
33
 
34
+ ##
35
+ # :method:
36
+ #
37
+ # Returns the associated +ActiveStorage::Attachment+s.
48
38
  has_many :attachments
49
39
 
40
+ ##
41
+ # :singleton-method:
42
+ #
43
+ # Returns the blobs that aren't attached to any record.
50
44
  scope :unattached, -> { where.missing(:attachments) }
51
45
 
52
46
  after_initialize do
53
47
  self.service_name ||= self.class.service&.name
54
48
  end
55
49
 
50
+ after_update :touch_attachment_records
51
+
56
52
  after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
57
53
 
58
54
  before_destroy(prepend: true) do
@@ -142,10 +138,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
142
138
 
143
139
  def scope_for_strict_loading # :nodoc:
144
140
  if strict_loading_by_default? && ActiveStorage.track_variants
145
- includes(
146
- variant_records: { image_attachment: :blob },
147
- preview_image_attachment: { blob: { variant_records: { image_attachment: :blob } } }
148
- )
141
+ includes(variant_records: { image_attachment: :blob }, preview_image_attachment: :blob)
149
142
  else
150
143
  all
151
144
  end
@@ -170,7 +163,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
170
163
  end
171
164
 
172
165
  # Returns the key pointing to the file on the service that's associated with this blob. The key is the
173
- # secure-token format from Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
166
+ # secure-token format from \Rails in lower case. So it'll look like: xtapjjcjiudrlk3tmwyjgpuobabd.
174
167
  # This key is not intended to be revealed directly to the user.
175
168
  # Always refer to blobs using the signed_id or a verified form of the key.
176
169
  def key
@@ -233,6 +226,16 @@ class ActiveStorage::Blob < ActiveStorage::Record
233
226
  service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum, custom_metadata: custom_metadata
234
227
  end
235
228
 
229
+ def content_type_for_serving # :nodoc:
230
+ forcibly_serve_as_binary? ? ActiveStorage.binary_content_type : content_type
231
+ end
232
+
233
+ def forced_disposition_for_serving # :nodoc:
234
+ if forcibly_serve_as_binary? || !allowed_inline?
235
+ :attachment
236
+ end
237
+ end
238
+
236
239
 
237
240
  # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be
238
241
  # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob,
@@ -303,7 +306,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
303
306
  end
304
307
 
305
308
  def mirror_later # :nodoc:
306
- ActiveStorage::MirrorJob.perform_later(key, checksum: checksum) if service.respond_to?(:mirror)
309
+ service.mirror_later key, checksum: checksum if service.respond_to?(:mirror_later)
307
310
  end
308
311
 
309
312
  # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
@@ -334,33 +337,10 @@ class ActiveStorage::Blob < ActiveStorage::Record
334
337
  services.fetch(service_name)
335
338
  end
336
339
 
337
- def content_type=(value)
338
- unless ActiveStorage.silence_invalid_content_types_warning
339
- if INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7.include?(value)
340
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
341
- #{value} is not a valid content type, it should not be used when creating a blob, and support for it will be removed in Rails 7.1.
342
- If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.variable_content_types`.
343
- Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
344
- MSG
345
- end
346
-
347
- if INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7.include?(value)
348
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
349
- #{value} is not a valid content type, it should not be used when creating a blob, and support for it will be removed in Rails 7.1.
350
- If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.content_types_to_serve_as_binary`.
351
- Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
352
- MSG
353
- end
354
- end
355
-
356
- super
357
- end
358
-
359
- INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7 = ["image/jpg", "image/pjpeg", "image/bmp"]
360
- INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7 = ["text/javascript"]
361
-
362
340
  private
363
341
  def compute_checksum_in_chunks(io)
342
+ raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
343
+
364
344
  OpenSSL::Digest::MD5.new.tap do |checksum|
365
345
  while chunk = io.read(5.megabytes)
366
346
  checksum << chunk
@@ -374,6 +354,14 @@ class ActiveStorage::Blob < ActiveStorage::Record
374
354
  Marcel::MimeType.for io, name: filename.to_s, declared_type: content_type
375
355
  end
376
356
 
357
+ def forcibly_serve_as_binary?
358
+ ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
359
+ end
360
+
361
+ def allowed_inline?
362
+ ActiveStorage.content_types_allowed_inline.include?(content_type)
363
+ end
364
+
377
365
  def web_image?
378
366
  ActiveStorage.web_image_content_types.include?(content_type)
379
367
  end
@@ -388,6 +376,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
388
376
  end
389
377
  end
390
378
 
379
+ def touch_attachment_records
380
+ attachments.includes(:record).each do |attachment|
381
+ attachment.touch
382
+ end
383
+ end
384
+
391
385
  def update_service_metadata
392
386
  service.update_metadata key, **service_metadata if service_metadata.any?
393
387
  end
@@ -2,14 +2,4 @@
2
2
 
3
3
  class ActiveStorage::Current < ActiveSupport::CurrentAttributes # :nodoc:
4
4
  attribute :url_options
5
-
6
- def host=(host)
7
- ActiveSupport::Deprecation.warn("ActiveStorage::Current.host= is deprecated, instead use ActiveStorage::Current.url_options=")
8
- self.url_options = { host: host }
9
- end
10
-
11
- def host
12
- ActiveSupport::Deprecation.warn("ActiveStorage::Current.host is deprecated, instead use ActiveStorage::Current.url_options")
13
- self.url_options&.dig(:host)
14
- end
15
5
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # = Active Storage \Filename
4
+ #
3
5
  # Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
4
6
  # A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
5
7
  class ActiveStorage::Filename