activestorage 7.0.8 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +156 -303
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +6 -6
  5. data/app/assets/javascripts/activestorage.esm.js +8 -4
  6. data/app/assets/javascripts/activestorage.js +9 -3
  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 +1 -0
  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/jobs/active_storage/analyze_job.rb +1 -1
  16. data/app/jobs/active_storage/mirror_job.rb +1 -1
  17. data/app/jobs/active_storage/purge_job.rb +1 -1
  18. data/app/jobs/active_storage/transform_job.rb +12 -0
  19. data/app/models/active_storage/attachment.rb +87 -13
  20. data/app/models/active_storage/blob/analyzable.rb +4 -3
  21. data/app/models/active_storage/blob/identifiable.rb +1 -0
  22. data/app/models/active_storage/blob/representable.rb +7 -3
  23. data/app/models/active_storage/blob.rb +26 -46
  24. data/app/models/active_storage/current.rb +0 -10
  25. data/app/models/active_storage/filename.rb +2 -0
  26. data/app/models/active_storage/named_variant.rb +21 -0
  27. data/app/models/active_storage/preview.rb +5 -3
  28. data/app/models/active_storage/variant.rb +8 -7
  29. data/app/models/active_storage/variant_with_record.rb +19 -7
  30. data/app/models/active_storage/variation.rb +5 -3
  31. data/config/routes.rb +6 -4
  32. data/db/migrate/20170806125915_create_active_storage_tables.rb +1 -1
  33. data/lib/active_storage/analyzer/audio_analyzer.rb +16 -4
  34. data/lib/active_storage/analyzer/image_analyzer.rb +2 -0
  35. data/lib/active_storage/analyzer/video_analyzer.rb +3 -1
  36. data/lib/active_storage/analyzer.rb +2 -0
  37. data/lib/active_storage/attached/changes/create_many.rb +8 -3
  38. data/lib/active_storage/attached/changes/create_one.rb +45 -3
  39. data/lib/active_storage/attached/many.rb +5 -4
  40. data/lib/active_storage/attached/model.rb +66 -43
  41. data/lib/active_storage/attached/one.rb +5 -4
  42. data/lib/active_storage/attached.rb +2 -0
  43. data/lib/active_storage/deprecator.rb +7 -0
  44. data/lib/active_storage/engine.rb +11 -7
  45. data/lib/active_storage/fixture_set.rb +2 -0
  46. data/lib/active_storage/gem_version.rb +3 -3
  47. data/lib/active_storage/log_subscriber.rb +12 -0
  48. data/lib/active_storage/previewer.rb +8 -1
  49. data/lib/active_storage/reflection.rb +3 -3
  50. data/lib/active_storage/service/azure_storage_service.rb +2 -0
  51. data/lib/active_storage/service/disk_service.rb +2 -0
  52. data/lib/active_storage/service/gcs_service.rb +11 -20
  53. data/lib/active_storage/service/mirror_service.rb +10 -5
  54. data/lib/active_storage/service/s3_service.rb +2 -0
  55. data/lib/active_storage/service.rb +4 -2
  56. data/lib/active_storage/transformers/transformer.rb +2 -0
  57. data/lib/active_storage/version.rb +1 -1
  58. data/lib/active_storage.rb +19 -3
  59. metadata +17 -27
@@ -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) {
@@ -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) {
@@ -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,6 +8,7 @@
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
@@ -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) {
@@ -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,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,24 +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
20
+ include Analyzable
21
+ include Identifiable
22
+ include Representable
36
23
 
37
24
  self.table_name = "active_storage_blobs"
38
25
 
@@ -44,14 +31,24 @@ class ActiveStorage::Blob < ActiveStorage::Record
44
31
  class_attribute :services, default: {}
45
32
  class_attribute :service, instance_accessor: false
46
33
 
34
+ ##
35
+ # :method:
36
+ #
37
+ # Returns the associated ActiveStorage::Attachment instances.
47
38
  has_many :attachments
48
39
 
40
+ ##
41
+ # :singleton-method:
42
+ #
43
+ # Returns the blobs that aren't attached to any record.
49
44
  scope :unattached, -> { where.missing(:attachments) }
50
45
 
51
46
  after_initialize do
52
47
  self.service_name ||= self.class.service&.name
53
48
  end
54
49
 
50
+ after_update :touch_attachment_records
51
+
55
52
  after_update_commit :update_service_metadata, if: -> { content_type_previously_changed? || metadata_previously_changed? }
56
53
 
57
54
  before_destroy(prepend: true) do
@@ -161,12 +158,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
161
158
  end
162
159
 
163
160
  # Returns a signed ID for this blob that's suitable for reference on the client-side without fear of tampering.
164
- def signed_id(purpose: :blob_id, expires_in: nil)
161
+ def signed_id(purpose: :blob_id, expires_in: nil, expires_at: nil)
165
162
  super
166
163
  end
167
164
 
168
165
  # Returns the key pointing to the file on the service that's associated with this blob. The key is the
169
- # 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.
170
167
  # This key is not intended to be revealed directly to the user.
171
168
  # Always refer to blobs using the signed_id or a verified form of the key.
172
169
  def key
@@ -309,7 +306,7 @@ class ActiveStorage::Blob < ActiveStorage::Record
309
306
  end
310
307
 
311
308
  def mirror_later # :nodoc:
312
- 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)
313
310
  end
314
311
 
315
312
  # Deletes the files on the service associated with the blob. This should only be done if the blob is going to be
@@ -340,33 +337,10 @@ class ActiveStorage::Blob < ActiveStorage::Record
340
337
  services.fetch(service_name)
341
338
  end
342
339
 
343
- def content_type=(value)
344
- unless ActiveStorage.silence_invalid_content_types_warning
345
- if INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7.include?(value)
346
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
347
- #{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.
348
- If you want to keep supporting this content type past Rails 7.1, add it to `config.active_storage.variable_content_types`.
349
- Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
350
- MSG
351
- end
352
-
353
- if INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7.include?(value)
354
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
355
- #{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.
356
- 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`.
357
- Dismiss this warning by setting `config.active_storage.silence_invalid_content_types_warning = true`.
358
- MSG
359
- end
360
- end
361
-
362
- super
363
- end
364
-
365
- INVALID_VARIABLE_CONTENT_TYPES_DEPRECATED_IN_RAILS_7 = ["image/jpg", "image/pjpeg", "image/bmp"]
366
- INVALID_VARIABLE_CONTENT_TYPES_TO_SERVE_AS_BINARY_DEPRECATED_IN_RAILS_7 = ["text/javascript"]
367
-
368
340
  private
369
341
  def compute_checksum_in_chunks(io)
342
+ raise ArgumentError, "io must be rewindable" unless io.respond_to?(:rewind)
343
+
370
344
  OpenSSL::Digest::MD5.new.tap do |checksum|
371
345
  while chunk = io.read(5.megabytes)
372
346
  checksum << chunk
@@ -402,6 +376,12 @@ class ActiveStorage::Blob < ActiveStorage::Record
402
376
  end
403
377
  end
404
378
 
379
+ def touch_attachment_records
380
+ attachments.includes(:record).each do |attachment|
381
+ attachment.touch
382
+ end
383
+ end
384
+
405
385
  def update_service_metadata
406
386
  service.update_metadata key, **service_metadata if service_metadata.any?
407
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
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ActiveStorage::NamedVariant # :nodoc:
4
+ attr_reader :transformations, :preprocessed
5
+
6
+ def initialize(transformations)
7
+ @preprocessed = transformations[:preprocessed]
8
+ @transformations = transformations.except(:preprocessed)
9
+ end
10
+
11
+ def preprocessed?(record)
12
+ case preprocessed
13
+ when Symbol
14
+ record.send(preprocessed)
15
+ when Proc
16
+ preprocessed.call(record)
17
+ else
18
+ preprocessed
19
+ end
20
+ end
21
+ end