activestorage 5.2.7.1 → 6.1.4.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +225 -93
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +43 -8
  5. data/app/assets/javascripts/activestorage.js +5 -2
  6. data/app/controllers/active_storage/base_controller.rb +13 -4
  7. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  8. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +3 -3
  9. data/app/controllers/active_storage/direct_uploads_controller.rb +2 -2
  10. data/app/controllers/active_storage/disk_controller.rb +13 -22
  11. data/app/controllers/active_storage/representations/base_controller.rb +14 -0
  12. data/app/controllers/active_storage/representations/proxy_controller.rb +13 -0
  13. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +3 -5
  14. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  15. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  16. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  17. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  18. data/app/javascript/activestorage/blob_record.js +7 -2
  19. data/app/jobs/active_storage/analyze_job.rb +5 -0
  20. data/app/jobs/active_storage/base_job.rb +0 -1
  21. data/app/jobs/active_storage/mirror_job.rb +15 -0
  22. data/app/jobs/active_storage/purge_job.rb +3 -0
  23. data/app/models/active_storage/attachment.rb +35 -16
  24. data/app/models/active_storage/blob/analyzable.rb +6 -2
  25. data/app/models/active_storage/blob/identifiable.rb +7 -6
  26. data/app/models/active_storage/blob/representable.rb +36 -6
  27. data/app/models/active_storage/blob.rb +186 -68
  28. data/app/models/active_storage/filename.rb +0 -6
  29. data/app/models/active_storage/preview.rb +37 -12
  30. data/app/models/active_storage/record.rb +7 -0
  31. data/app/models/active_storage/variant.rb +53 -67
  32. data/app/models/active_storage/variant_record.rb +8 -0
  33. data/app/models/active_storage/variant_with_record.rb +54 -0
  34. data/app/models/active_storage/variation.rb +30 -94
  35. data/config/routes.rb +66 -15
  36. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  37. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  38. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  39. data/lib/active_storage/analyzer/image_analyzer.rb +14 -4
  40. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  41. data/lib/active_storage/analyzer/video_analyzer.rb +17 -8
  42. data/lib/active_storage/analyzer.rb +15 -4
  43. data/lib/active_storage/attached/changes/create_many.rb +47 -0
  44. data/lib/active_storage/attached/changes/create_one.rb +82 -0
  45. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  46. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  47. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  48. data/lib/active_storage/attached/changes.rb +16 -0
  49. data/lib/active_storage/attached/many.rb +19 -12
  50. data/lib/active_storage/attached/model.rb +212 -0
  51. data/lib/active_storage/attached/one.rb +19 -21
  52. data/lib/active_storage/attached.rb +7 -22
  53. data/lib/active_storage/downloader.rb +43 -0
  54. data/lib/active_storage/engine.rb +60 -38
  55. data/lib/active_storage/errors.rb +25 -3
  56. data/lib/active_storage/gem_version.rb +4 -4
  57. data/lib/active_storage/log_subscriber.rb +6 -0
  58. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  59. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
  60. data/lib/active_storage/previewer/video_previewer.rb +17 -10
  61. data/lib/active_storage/previewer.rb +34 -14
  62. data/lib/active_storage/reflection.rb +64 -0
  63. data/lib/active_storage/service/azure_storage_service.rb +65 -44
  64. data/lib/active_storage/service/configurator.rb +6 -2
  65. data/lib/active_storage/service/disk_service.rb +57 -44
  66. data/lib/active_storage/service/gcs_service.rb +68 -64
  67. data/lib/active_storage/service/mirror_service.rb +31 -7
  68. data/lib/active_storage/service/registry.rb +32 -0
  69. data/lib/active_storage/service/s3_service.rb +56 -24
  70. data/lib/active_storage/service.rb +44 -12
  71. data/lib/active_storage/transformers/image_processing_transformer.rb +45 -0
  72. data/lib/active_storage/transformers/transformer.rb +39 -0
  73. data/lib/active_storage.rb +31 -296
  74. data/lib/tasks/activestorage.rake +11 -0
  75. metadata +82 -16
  76. data/app/models/active_storage/filename/parameters.rb +0 -36
  77. data/lib/active_storage/attached/macros.rb +0 -110
  78. data/lib/active_storage/downloading.rb +0 -39
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class Downloader #:nodoc:
5
+ attr_reader :service
6
+
7
+ def initialize(service)
8
+ @service = service
9
+ end
10
+
11
+ def open(key, checksum:, name: "ActiveStorage-", tmpdir: nil)
12
+ open_tempfile(name, tmpdir) do |file|
13
+ download key, file
14
+ verify_integrity_of file, checksum: checksum
15
+ yield file
16
+ end
17
+ end
18
+
19
+ private
20
+ def open_tempfile(name, tmpdir = nil)
21
+ file = Tempfile.open(name, tmpdir)
22
+
23
+ begin
24
+ yield file
25
+ ensure
26
+ file.close!
27
+ end
28
+ end
29
+
30
+ def download(key, file)
31
+ file.binmode
32
+ service.download(key) { |chunk| file.write(chunk) }
33
+ file.flush
34
+ file.rewind
35
+ end
36
+
37
+ def verify_integrity_of(file, checksum:)
38
+ unless Digest::MD5.file(file).base64digest == checksum
39
+ raise ActiveStorage::IntegrityError
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails"
4
+ require "action_controller/railtie"
5
+ require "active_job/railtie"
6
+ require "active_record/railtie"
7
+
4
8
  require "active_storage"
5
9
 
6
10
  require "active_storage/previewer/poppler_pdf_previewer"
@@ -10,6 +14,10 @@ require "active_storage/previewer/video_previewer"
10
14
  require "active_storage/analyzer/image_analyzer"
11
15
  require "active_storage/analyzer/video_analyzer"
12
16
 
17
+ require "active_storage/service/registry"
18
+
19
+ require "active_storage/reflection"
20
+
13
21
  module ActiveStorage
14
22
  class Engine < Rails::Engine # :nodoc:
15
23
  isolate_namespace ActiveStorage
@@ -18,14 +26,26 @@ module ActiveStorage
18
26
  config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
19
27
  config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
20
28
  config.active_storage.paths = ActiveSupport::OrderedOptions.new
29
+ config.active_storage.queues = ActiveSupport::InheritableOptions.new
21
30
 
22
31
  config.active_storage.variable_content_types = %w(
23
32
  image/png
24
33
  image/gif
25
34
  image/jpg
26
35
  image/jpeg
36
+ image/pjpeg
37
+ image/tiff
38
+ image/bmp
27
39
  image/vnd.adobe.photoshop
28
40
  image/vnd.microsoft.icon
41
+ image/webp
42
+ )
43
+
44
+ config.active_storage.web_image_content_types = %w(
45
+ image/png
46
+ image/jpeg
47
+ image/jpg
48
+ image/gif
29
49
  )
30
50
 
31
51
  config.active_storage.content_types_to_serve_as_binary = %w(
@@ -46,6 +66,8 @@ module ActiveStorage
46
66
  image/gif
47
67
  image/jpg
48
68
  image/jpeg
69
+ image/tiff
70
+ image/bmp
49
71
  image/vnd.adobe.photoshop
50
72
  image/vnd.microsoft.icon
51
73
  application/pdf
@@ -55,30 +77,25 @@ module ActiveStorage
55
77
 
56
78
  initializer "active_storage.configs" do
57
79
  config.after_initialize do |app|
58
- ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
59
- ActiveStorage.queue = app.config.active_storage.queue
60
- ActiveStorage.previewers = app.config.active_storage.previewers || []
61
- ActiveStorage.analyzers = app.config.active_storage.analyzers || []
62
- ActiveStorage.paths = app.config.active_storage.paths || {}
63
-
64
- ActiveStorage.supported_image_processing_methods += app.config.active_storage.supported_image_processing_methods || []
65
- ActiveStorage.unsupported_image_processing_arguments = app.config.active_storage.unsupported_image_processing_arguments || %w(
66
- -debug
67
- -display
68
- -distribute-cache
69
- -help
70
- -path
71
- -print
72
- -set
73
- -verbose
74
- -version
75
- -write
76
- -write-mask
77
- )
80
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
81
+ ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
82
+ ActiveStorage.previewers = app.config.active_storage.previewers || []
83
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
84
+ ActiveStorage.paths = app.config.active_storage.paths || {}
85
+ ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
86
+ ActiveStorage.draw_routes = app.config.active_storage.draw_routes != false
87
+ ActiveStorage.resolve_model_to_route = app.config.active_storage.resolve_model_to_route || :rails_storage_redirect
88
+
78
89
  ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
90
+ ActiveStorage.web_image_content_types = app.config.active_storage.web_image_content_types || []
79
91
  ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
92
+ ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
80
93
  ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
81
94
  ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
95
+ ActiveStorage.video_preview_arguments = app.config.active_storage.video_preview_arguments || "-y -vframes 1 -f image2"
96
+
97
+ ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
98
+ ActiveStorage.track_variants = app.config.active_storage.track_variants || false
82
99
  end
83
100
  end
84
101
 
@@ -86,7 +103,7 @@ module ActiveStorage
86
103
  require "active_storage/attached"
87
104
 
88
105
  ActiveSupport.on_load(:active_record) do
89
- extend ActiveStorage::Attached::Macros
106
+ include ActiveStorage::Attached::Model
90
107
  end
91
108
  end
92
109
 
@@ -98,29 +115,34 @@ module ActiveStorage
98
115
 
99
116
  initializer "active_storage.services" do
100
117
  ActiveSupport.on_load(:active_storage_blob) do
101
- if config_choice = Rails.configuration.active_storage.service
102
- configs = Rails.configuration.active_storage.service_configurations ||= begin
103
- config_file = Pathname.new(Rails.root.join("config/storage.yml"))
118
+ configs = Rails.configuration.active_storage.service_configurations ||=
119
+ begin
120
+ config_file = Rails.root.join("config/storage/#{Rails.env}.yml")
121
+ config_file = Rails.root.join("config/storage.yml") unless config_file.exist?
104
122
  raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist?
105
123
 
106
- require "yaml"
107
- require "erb"
108
-
109
- YAML.load(ERB.new(config_file.read).result) || {}
110
- rescue Psych::SyntaxError => e
111
- raise "YAML syntax error occurred while parsing #{config_file}. " \
112
- "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
113
- "Error: #{e.message}"
124
+ ActiveSupport::ConfigurationFile.parse(config_file)
114
125
  end
115
126
 
116
- ActiveStorage::Blob.service =
117
- begin
118
- ActiveStorage::Service.configure config_choice, configs
119
- rescue => e
120
- raise e, "Cannot load `Rails.config.active_storage.service`:\n#{e.message}", e.backtrace
121
- end
127
+ ActiveStorage::Blob.services = ActiveStorage::Service::Registry.new(configs)
128
+
129
+ if config_choice = Rails.configuration.active_storage.service
130
+ ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(config_choice)
122
131
  end
123
132
  end
124
133
  end
134
+
135
+ initializer "active_storage.queues" do
136
+ config.after_initialize do |app|
137
+ ActiveStorage.queues = app.config.active_storage.queues || {}
138
+ end
139
+ end
140
+
141
+ initializer "active_storage.reflection" do
142
+ ActiveSupport.on_load(:active_record) do
143
+ include Reflection::ActiveRecordExtensions
144
+ ActiveRecord::Reflection.singleton_class.prepend(Reflection::ReflectionExtension)
145
+ end
146
+ end
125
147
  end
126
148
  end
@@ -1,7 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveStorage
4
- class InvariableError < StandardError; end
5
- class UnpreviewableError < StandardError; end
6
- class UnrepresentableError < StandardError; end
4
+ # Generic base class for all Active Storage exceptions.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when ActiveStorage::Blob#variant is called on a blob that isn't variable.
8
+ # Use ActiveStorage::Blob#variable? to determine whether a blob is variable.
9
+ class InvariableError < Error; end
10
+
11
+ # Raised when ActiveStorage::Blob#preview is called on a blob that isn't previewable.
12
+ # Use ActiveStorage::Blob#previewable? to determine whether a blob is previewable.
13
+ class UnpreviewableError < Error; end
14
+
15
+ # Raised when ActiveStorage::Blob#representation is called on a blob that isn't representable.
16
+ # Use ActiveStorage::Blob#representable? to determine whether a blob is representable.
17
+ class UnrepresentableError < Error; end
18
+
19
+ # Raised when uploaded or downloaded data does not match a precomputed checksum.
20
+ # Indicates that a network error or a software bug caused data corruption.
21
+ class IntegrityError < Error; end
22
+
23
+ # Raised when ActiveStorage::Blob#download is called on a blob where the
24
+ # backing file is no longer present in its service.
25
+ class FileNotFoundError < Error; end
26
+
27
+ # Raised when a Previewer is unable to generate a preview image.
28
+ class PreviewError < Error; end
7
29
  end
@@ -7,10 +7,10 @@ module ActiveStorage
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 7
13
- PRE = "1"
10
+ MAJOR = 6
11
+ MINOR = 1
12
+ TINY = 4
13
+ PRE = "6"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -32,6 +32,12 @@ module ActiveStorage
32
32
  debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE)
33
33
  end
34
34
 
35
+ def service_mirror(event)
36
+ message = "Mirrored file at key: #{key_in(event)}"
37
+ message += " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum]
38
+ debug event, color(message, GREEN)
39
+ end
40
+
35
41
  def logger
36
42
  ActiveStorage.logger
37
43
  end
@@ -12,7 +12,7 @@ module ActiveStorage
12
12
  end
13
13
 
14
14
  def mutool_exists?
15
- return @mutool_exists unless @mutool_exists.nil?
15
+ return @mutool_exists if defined?(@mutool_exists) && !@mutool_exists.nil?
16
16
 
17
17
  system mutool_path, out: File::NULL, err: File::NULL
18
18
 
@@ -20,10 +20,10 @@ module ActiveStorage
20
20
  end
21
21
  end
22
22
 
23
- def preview
23
+ def preview(**options)
24
24
  download_blob_to_tempfile do |input|
25
25
  draw_first_page_from input do |output|
26
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
26
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png", **options
27
27
  end
28
28
  end
29
29
  end
@@ -12,16 +12,16 @@ module ActiveStorage
12
12
  end
13
13
 
14
14
  def pdftoppm_exists?
15
- return @pdftoppm_exists unless @pdftoppm_exists.nil?
15
+ return @pdftoppm_exists if defined?(@pdftoppm_exists)
16
16
 
17
17
  @pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL)
18
18
  end
19
19
  end
20
20
 
21
- def preview
21
+ def preview(**options)
22
22
  download_blob_to_tempfile do |input|
23
23
  draw_first_page_from input do |output|
24
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
24
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png", **options
25
25
  end
26
26
  end
27
27
  end
@@ -2,26 +2,33 @@
2
2
 
3
3
  module ActiveStorage
4
4
  class Previewer::VideoPreviewer < Previewer
5
- def self.accept?(blob)
6
- blob.video?
5
+ class << self
6
+ def accept?(blob)
7
+ blob.video? && ffmpeg_exists?
8
+ end
9
+
10
+ def ffmpeg_exists?
11
+ return @ffmpeg_exists if defined?(@ffmpeg_exists)
12
+
13
+ @ffmpeg_exists = system(ffmpeg_path, "-version", out: File::NULL, err: File::NULL)
14
+ end
15
+
16
+ def ffmpeg_path
17
+ ActiveStorage.paths[:ffmpeg] || "ffmpeg"
18
+ end
7
19
  end
8
20
 
9
- def preview
21
+ def preview(**options)
10
22
  download_blob_to_tempfile do |input|
11
23
  draw_relevant_frame_from input do |output|
12
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
24
+ yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg", **options
13
25
  end
14
26
  end
15
27
  end
16
28
 
17
29
  private
18
30
  def draw_relevant_frame_from(file, &block)
19
- draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png",
20
- "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
21
- end
22
-
23
- def ffmpeg_path
24
- ActiveStorage.paths[:ffmpeg] || "ffmpeg"
31
+ draw self.class.ffmpeg_path, "-i", file.path, *Shellwords.split(ActiveStorage.video_preview_arguments), "-", &block
25
32
  end
26
33
  end
27
34
  end
@@ -1,14 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_storage/downloading"
4
-
5
3
  module ActiveStorage
6
4
  # This is an abstract base class for previewers, which generate images from blobs. See
7
5
  # ActiveStorage::Previewer::MuPDFPreviewer and ActiveStorage::Previewer::VideoPreviewer for
8
6
  # examples of concrete subclasses.
9
7
  class Previewer
10
- include Downloading
11
-
12
8
  attr_reader :blob
13
9
 
14
10
  # Implement this method in a concrete subclass. Have it return true when given a blob from which
@@ -22,15 +18,21 @@ module ActiveStorage
22
18
  end
23
19
 
24
20
  # Override this method in a concrete subclass. Have it yield an attachable preview image (i.e.
25
- # anything accepted by ActiveStorage::Attached::One#attach).
26
- def preview
21
+ # anything accepted by ActiveStorage::Attached::One#attach). Pass the additional options to
22
+ # the underlying blob that is created.
23
+ def preview(**options)
27
24
  raise NotImplementedError
28
25
  end
29
26
 
30
27
  private
28
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
29
+ def download_blob_to_tempfile(&block) #:doc:
30
+ blob.open tmpdir: tmpdir, &block
31
+ end
32
+
31
33
  # Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
32
34
  #
33
- # Use this method to shell out to a system library (e.g. mupdf or ffmpeg) for preview image
35
+ # Use this method to shell out to a system library (e.g. muPDF or FFmpeg) for preview image
34
36
  # generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
35
37
  #
36
38
  # def preview
@@ -41,18 +43,19 @@ module ActiveStorage
41
43
  # end
42
44
  # end
43
45
  #
44
- # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir.
46
+ # The output tempfile is opened in the directory returned by #tmpdir.
45
47
  def draw(*argv) #:doc:
46
- ActiveSupport::Notifications.instrument("preview.active_storage") do
47
- open_tempfile_for_drawing do |file|
48
+ open_tempfile do |file|
49
+ instrument :preview, key: blob.key do
48
50
  capture(*argv, to: file)
49
- yield file
50
51
  end
52
+
53
+ yield file
51
54
  end
52
55
  end
53
56
 
54
- def open_tempfile_for_drawing
55
- tempfile = Tempfile.open("ActiveStorage", tempdir)
57
+ def open_tempfile
58
+ tempfile = Tempfile.open("ActiveStorage-", tmpdir)
56
59
 
57
60
  begin
58
61
  yield tempfile
@@ -61,14 +64,31 @@ module ActiveStorage
61
64
  end
62
65
  end
63
66
 
67
+ def instrument(operation, payload = {}, &block)
68
+ ActiveSupport::Notifications.instrument "#{operation}.active_storage", payload, &block
69
+ end
70
+
64
71
  def capture(*argv, to:)
65
72
  to.binmode
66
- IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) }
73
+
74
+ open_tempfile do |err|
75
+ IO.popen(argv, err: err) { |out| IO.copy_stream(out, to) }
76
+ err.rewind
77
+
78
+ unless $?.success?
79
+ raise PreviewError, "#{argv.first} failed (status #{$?.exitstatus}): #{err.read.to_s.chomp}"
80
+ end
81
+ end
82
+
67
83
  to.rewind
68
84
  end
69
85
 
70
86
  def logger #:doc:
71
87
  ActiveStorage.logger
72
88
  end
89
+
90
+ def tmpdir #:doc:
91
+ Dir.tmpdir
92
+ end
73
93
  end
74
94
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ module Reflection
5
+ # Holds all the metadata about a has_one_attached attachment as it was
6
+ # specified in the Active Record class.
7
+ class HasOneAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
8
+ def macro
9
+ :has_one_attached
10
+ end
11
+ end
12
+
13
+ # Holds all the metadata about a has_many_attached attachment as it was
14
+ # specified in the Active Record class.
15
+ class HasManyAttachedReflection < ActiveRecord::Reflection::MacroReflection #:nodoc:
16
+ def macro
17
+ :has_many_attached
18
+ end
19
+ end
20
+
21
+ module ReflectionExtension # :nodoc:
22
+ def add_attachment_reflection(model, name, reflection)
23
+ model.attachment_reflections = model.attachment_reflections.merge(name.to_s => reflection)
24
+ end
25
+
26
+ private
27
+ def reflection_class_for(macro)
28
+ case macro
29
+ when :has_one_attached
30
+ HasOneAttachedReflection
31
+ when :has_many_attached
32
+ HasManyAttachedReflection
33
+ else
34
+ super
35
+ end
36
+ end
37
+ end
38
+
39
+ module ActiveRecordExtensions
40
+ extend ActiveSupport::Concern
41
+
42
+ included do
43
+ class_attribute :attachment_reflections, instance_writer: false, default: {}
44
+ end
45
+
46
+ module ClassMethods
47
+ # Returns an array of reflection objects for all the attachments in the
48
+ # class.
49
+ def reflect_on_all_attachments
50
+ attachment_reflections.values
51
+ end
52
+
53
+ # Returns the reflection object for the named +attachment+.
54
+ #
55
+ # User.reflect_on_attachment(:avatar)
56
+ # # => the avatar reflection
57
+ #
58
+ def reflect_on_attachment(attachment)
59
+ attachment_reflections[attachment.to_s]
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end