activestorage 6.0.4 → 6.1.0.rc1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +141 -187
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +36 -4
  5. data/app/controllers/active_storage/base_controller.rb +11 -0
  6. data/app/controllers/active_storage/blobs/proxy_controller.rb +14 -0
  7. data/app/controllers/active_storage/{blobs_controller.rb → blobs/redirect_controller.rb} +2 -2
  8. data/app/controllers/active_storage/disk_controller.rb +8 -20
  9. data/app/controllers/active_storage/representations/proxy_controller.rb +19 -0
  10. data/app/controllers/active_storage/{representations_controller.rb → representations/redirect_controller.rb} +2 -2
  11. data/app/controllers/concerns/active_storage/file_server.rb +18 -0
  12. data/app/controllers/concerns/active_storage/set_blob.rb +1 -1
  13. data/app/controllers/concerns/active_storage/set_current.rb +2 -2
  14. data/app/controllers/concerns/active_storage/set_headers.rb +12 -0
  15. data/app/jobs/active_storage/mirror_job.rb +15 -0
  16. data/app/models/active_storage/attachment.rb +18 -10
  17. data/app/models/active_storage/blob.rb +114 -57
  18. data/app/models/active_storage/blob/analyzable.rb +6 -2
  19. data/app/models/active_storage/blob/identifiable.rb +7 -6
  20. data/app/models/active_storage/blob/representable.rb +34 -4
  21. data/app/models/active_storage/preview.rb +31 -10
  22. data/app/models/active_storage/record.rb +7 -0
  23. data/app/models/active_storage/variant.rb +28 -41
  24. data/app/models/active_storage/variant_record.rb +8 -0
  25. data/app/models/active_storage/variant_with_record.rb +54 -0
  26. data/app/models/active_storage/variation.rb +25 -20
  27. data/config/routes.rb +58 -8
  28. data/db/migrate/20170806125915_create_active_storage_tables.rb +14 -5
  29. data/db/update_migrate/20190112182829_add_service_name_to_active_storage_blobs.rb +17 -0
  30. data/db/update_migrate/20191206030411_create_active_storage_variant_records.rb +11 -0
  31. data/lib/active_storage.rb +5 -2
  32. data/lib/active_storage/analyzer.rb +6 -0
  33. data/lib/active_storage/analyzer/image_analyzer.rb +3 -0
  34. data/lib/active_storage/analyzer/null_analyzer.rb +4 -0
  35. data/lib/active_storage/analyzer/video_analyzer.rb +14 -3
  36. data/lib/active_storage/attached/changes/create_many.rb +1 -0
  37. data/lib/active_storage/attached/changes/create_one.rb +17 -4
  38. data/lib/active_storage/attached/many.rb +4 -3
  39. data/lib/active_storage/attached/model.rb +49 -10
  40. data/lib/active_storage/attached/one.rb +4 -3
  41. data/lib/active_storage/engine.rb +25 -27
  42. data/lib/active_storage/gem_version.rb +3 -3
  43. data/lib/active_storage/log_subscriber.rb +6 -0
  44. data/lib/active_storage/previewer.rb +3 -2
  45. data/lib/active_storage/previewer/mupdf_previewer.rb +3 -3
  46. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
  47. data/lib/active_storage/previewer/video_previewer.rb +2 -2
  48. data/lib/active_storage/service.rb +35 -7
  49. data/lib/active_storage/service/azure_storage_service.rb +40 -35
  50. data/lib/active_storage/service/configurator.rb +3 -1
  51. data/lib/active_storage/service/disk_service.rb +36 -31
  52. data/lib/active_storage/service/gcs_service.rb +18 -16
  53. data/lib/active_storage/service/mirror_service.rb +31 -7
  54. data/lib/active_storage/service/registry.rb +32 -0
  55. data/lib/active_storage/service/s3_service.rb +51 -23
  56. data/lib/active_storage/transformers/image_processing_transformer.rb +13 -7
  57. data/lib/active_storage/transformers/transformer.rb +0 -3
  58. metadata +57 -21
  59. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +0 -9
  60. data/lib/active_storage/downloading.rb +0 -47
  61. data/lib/active_storage/transformers/mini_magick_transformer.rb +0 -38
data/config/routes.rb CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  Rails.application.routes.draw do
4
4
  scope ActiveStorage.routes_prefix do
5
- get "/blobs/:signed_id/*filename" => "active_storage/blobs#show", as: :rails_service_blob
5
+ get "/blobs/redirect/:signed_id/*filename" => "active_storage/blobs/redirect#show", as: :rails_service_blob
6
+ get "/blobs/proxy/:signed_id/*filename" => "active_storage/blobs/proxy#show", as: :rails_service_blob_proxy
7
+ get "/blobs/:signed_id/*filename" => "active_storage/blobs/redirect#show"
6
8
 
7
- get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations#show", as: :rails_blob_representation
9
+ get "/representations/redirect/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/redirect#show", as: :rails_blob_representation
10
+ get "/representations/proxy/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/proxy#show", as: :rails_blob_representation_proxy
11
+ get "/representations/:signed_blob_id/:variation_key/*filename" => "active_storage/representations/redirect#show"
8
12
 
9
13
  get "/disk/:encoded_key/*filename" => "active_storage/disk#show", as: :rails_disk_service
10
14
  put "/disk/:encoded_token" => "active_storage/disk#update", as: :update_rails_disk_service
@@ -19,14 +23,60 @@ Rails.application.routes.draw do
19
23
  route_for(:rails_blob_representation, signed_blob_id, variation_key, filename, options)
20
24
  end
21
25
 
22
- resolve("ActiveStorage::Variant") { |variant, options| route_for(:rails_representation, variant, options) }
23
- resolve("ActiveStorage::Preview") { |preview, options| route_for(:rails_representation, preview, options) }
24
-
26
+ resolve("ActiveStorage::Variant") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
27
+ resolve("ActiveStorage::VariantWithRecord") { |variant, options| route_for(ActiveStorage.resolve_model_to_route, variant, options) }
28
+ resolve("ActiveStorage::Preview") { |preview, options| route_for(ActiveStorage.resolve_model_to_route, preview, options) }
25
29
 
26
30
  direct :rails_blob do |blob, options|
27
31
  route_for(:rails_service_blob, blob.signed_id, blob.filename, options)
28
32
  end
29
33
 
30
- resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
31
- resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
32
- end
34
+ resolve("ActiveStorage::Blob") { |blob, options| route_for(ActiveStorage.resolve_model_to_route, blob, options) }
35
+ resolve("ActiveStorage::Attachment") { |attachment, options| route_for(ActiveStorage.resolve_model_to_route, attachment.blob, options) }
36
+
37
+ direct :rails_storage_proxy do |model, options|
38
+ if model.respond_to?(:signed_id)
39
+ route_for(
40
+ :rails_service_blob_proxy,
41
+ model.signed_id,
42
+ model.filename,
43
+ options
44
+ )
45
+ else
46
+ signed_blob_id = model.blob.signed_id
47
+ variation_key = model.variation.key
48
+ filename = model.blob.filename
49
+
50
+ route_for(
51
+ :rails_blob_representation_proxy,
52
+ signed_blob_id,
53
+ variation_key,
54
+ filename,
55
+ options
56
+ )
57
+ end
58
+ end
59
+
60
+ direct :rails_storage_redirect do |model, options|
61
+ if model.respond_to?(:signed_id)
62
+ route_for(
63
+ :rails_service_blob,
64
+ model.signed_id,
65
+ model.filename,
66
+ options
67
+ )
68
+ else
69
+ signed_blob_id = model.blob.signed_id
70
+ variation_key = model.variation.key
71
+ filename = model.blob.filename
72
+
73
+ route_for(
74
+ :rails_blob_representation,
75
+ signed_blob_id,
76
+ variation_key,
77
+ filename,
78
+ options
79
+ )
80
+ end
81
+ end
82
+ end if ActiveStorage.draw_routes
@@ -1,13 +1,14 @@
1
1
  class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
2
2
  def change
3
3
  create_table :active_storage_blobs do |t|
4
- t.string :key, null: false
5
- t.string :filename, null: false
4
+ t.string :key, null: false
5
+ t.string :filename, null: false
6
6
  t.string :content_type
7
7
  t.text :metadata
8
- t.bigint :byte_size, null: false
9
- t.string :checksum, null: false
10
- t.datetime :created_at, null: false
8
+ t.string :service_name, null: false
9
+ t.bigint :byte_size, null: false
10
+ t.string :checksum, null: false
11
+ t.datetime :created_at, null: false
11
12
 
12
13
  t.index [ :key ], unique: true
13
14
  end
@@ -22,5 +23,13 @@ class CreateActiveStorageTables < ActiveRecord::Migration[5.2]
22
23
  t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
23
24
  t.foreign_key :active_storage_blobs, column: :blob_id
24
25
  end
26
+
27
+ create_table :active_storage_variant_records do |t|
28
+ t.belongs_to :blob, null: false, index: false
29
+ t.string :variation_digest, null: false
30
+
31
+ t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
32
+ t.foreign_key :active_storage_blobs, column: :blob_id
33
+ end
25
34
  end
26
35
  end
@@ -0,0 +1,17 @@
1
+ class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
2
+ def up
3
+ unless column_exists?(:active_storage_blobs, :service_name)
4
+ add_column :active_storage_blobs, :service_name, :string
5
+
6
+ if configured_service = ActiveStorage::Blob.service.name
7
+ ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
8
+ end
9
+
10
+ change_column :active_storage_blobs, :service_name, :string, null: false
11
+ end
12
+ end
13
+
14
+ def down
15
+ remove_column :active_storage_blobs, :service_name
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table :active_storage_variant_records do |t|
4
+ t.belongs_to :blob, null: false, index: false
5
+ t.string :variation_digest, null: false
6
+
7
+ t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
8
+ t.foreign_key :active_storage_blobs, column: :blob_id
9
+ end
10
+ end
11
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2017-2019 David Heinemeier Hansson, Basecamp
4
+ # Copyright (c) 2017-2020 David Heinemeier Hansson, Basecamp
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining
7
7
  # a copy of this software and associated documentation files (the
@@ -53,6 +53,7 @@ module ActiveStorage
53
53
  mattr_accessor :paths, default: {}
54
54
 
55
55
  mattr_accessor :variable_content_types, default: []
56
+ mattr_accessor :web_image_content_types, default: []
56
57
  mattr_accessor :binary_content_type, default: "application/octet-stream"
57
58
  mattr_accessor :content_types_to_serve_as_binary, default: []
58
59
  mattr_accessor :content_types_allowed_inline, default: []
@@ -60,14 +61,16 @@ module ActiveStorage
60
61
  mattr_accessor :service_urls_expire_in, default: 5.minutes
61
62
 
62
63
  mattr_accessor :routes_prefix, default: "/rails/active_storage"
64
+ mattr_accessor :draw_routes, default: true
65
+ mattr_accessor :resolve_model_to_route, default: :rails_storage_redirect
63
66
 
64
67
  mattr_accessor :replace_on_assign_to_many, default: false
68
+ mattr_accessor :track_variants, default: false
65
69
 
66
70
  module Transformers
67
71
  extend ActiveSupport::Autoload
68
72
 
69
73
  autoload :Transformer
70
74
  autoload :ImageProcessingTransformer
71
- autoload :MiniMagickTransformer
72
75
  end
73
76
  end
@@ -12,6 +12,12 @@ module ActiveStorage
12
12
  false
13
13
  end
14
14
 
15
+ # Implement this method in concrete subclasses. It will determine if blob analysis
16
+ # should be done in a job or performed inline. By default, analysis is enqueued in a job.
17
+ def self.analyze_later?
18
+ true
19
+ end
20
+
15
21
  def initialize(blob)
16
22
  @blob = blob
17
23
  end
@@ -43,6 +43,9 @@ module ActiveStorage
43
43
  rescue LoadError
44
44
  logger.info "Skipping image analysis because the mini_magick gem isn't installed"
45
45
  {}
46
+ rescue MiniMagick::Error => error
47
+ logger.error "Skipping image analysis due to an ImageMagick error: #{error.message}"
48
+ {}
46
49
  end
47
50
 
48
51
  def rotated_image?(image)
@@ -6,6 +6,10 @@ module ActiveStorage
6
6
  true
7
7
  end
8
8
 
9
+ def self.analyze_later?
10
+ false
11
+ end
12
+
9
13
  def metadata
10
14
  {}
11
15
  end
@@ -44,7 +44,8 @@ module ActiveStorage
44
44
  end
45
45
 
46
46
  def duration
47
- Float(video_stream["duration"]) if video_stream["duration"]
47
+ duration = video_stream["duration"] || container["duration"]
48
+ Float(duration) if duration
48
49
  end
49
50
 
50
51
  def angle
@@ -98,12 +99,22 @@ module ActiveStorage
98
99
  probe["streams"] || []
99
100
  end
100
101
 
102
+ def container
103
+ probe["format"] || {}
104
+ end
105
+
101
106
  def probe
102
- download_blob_to_tempfile { |file| probe_from(file) }
107
+ @probe ||= download_blob_to_tempfile { |file| probe_from(file) }
103
108
  end
104
109
 
105
110
  def probe_from(file)
106
- IO.popen([ ffprobe_path, "-print_format", "json", "-show_streams", "-v", "error", file.path ]) do |output|
111
+ IO.popen([ ffprobe_path,
112
+ "-print_format", "json",
113
+ "-show_streams",
114
+ "-show_format",
115
+ "-v", "error",
116
+ file.path
117
+ ]) do |output|
107
118
  JSON.parse(output.read)
108
119
  end
109
120
  rescue Errno::ENOENT
@@ -6,6 +6,7 @@ module ActiveStorage
6
6
 
7
7
  def initialize(name, record, attachables)
8
8
  @name, @record, @attachables = name, record, Array(attachables)
9
+ blobs.each(&:identify_without_saving)
9
10
  end
10
11
 
11
12
  def attachments
@@ -9,6 +9,7 @@ module ActiveStorage
9
9
 
10
10
  def initialize(name, record, attachable)
11
11
  @name, @record, @attachable = name, record, attachable
12
+ blob.identify_without_saving
12
13
  end
13
14
 
14
15
  def attachment
@@ -53,17 +54,29 @@ module ActiveStorage
53
54
  when ActiveStorage::Blob
54
55
  attachable
55
56
  when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
56
- ActiveStorage::Blob.build_after_unfurling \
57
+ ActiveStorage::Blob.build_after_unfurling(
57
58
  io: attachable.open,
58
59
  filename: attachable.original_filename,
59
- content_type: attachable.content_type
60
+ content_type: attachable.content_type,
61
+ record: record,
62
+ service_name: attachment_service_name
63
+ )
60
64
  when Hash
61
- ActiveStorage::Blob.build_after_unfurling(**attachable.symbolize_keys)
65
+ ActiveStorage::Blob.build_after_unfurling(
66
+ **attachable.reverse_merge(
67
+ record: record,
68
+ service_name: attachment_service_name
69
+ ).symbolize_keys
70
+ )
62
71
  when String
63
- ActiveStorage::Blob.find_signed(attachable)
72
+ ActiveStorage::Blob.find_signed!(attachable, record: record)
64
73
  else
65
74
  raise ArgumentError, "Could not find or build blob: expected attachable, got #{attachable.inspect}"
66
75
  end
67
76
  end
77
+
78
+ def attachment_service_name
79
+ record.attachment_reflections[name].options[:service_name]
80
+ end
68
81
  end
69
82
  end
@@ -29,15 +29,16 @@ module ActiveStorage
29
29
  # document.images.attach([ first_blob, second_blob ])
30
30
  def attach(*attachables)
31
31
  if record.persisted? && !record.changed?
32
- record.update(name => blobs + attachables.flatten)
32
+ record.public_send("#{name}=", blobs + attachables.flatten)
33
+ record.save
33
34
  else
34
35
  record.public_send("#{name}=", (change&.attachables || blobs) + attachables.flatten)
35
36
  end
36
37
  end
37
38
 
38
- # Returns true if any attachments has been made.
39
+ # Returns true if any attachments have been made.
39
40
  #
40
- # class Gallery < ActiveRecord::Base
41
+ # class Gallery < ApplicationRecord
41
42
  # has_many_attached :photos
42
43
  # end
43
44
  #
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/object/try"
4
+
3
5
  module ActiveStorage
4
6
  # Provides the class-level DSL for declaring an Active Record model's attachments.
5
7
  module Attached::Model
@@ -8,7 +10,7 @@ module ActiveStorage
8
10
  class_methods do
9
11
  # Specifies the relation between a single attachment and the model.
10
12
  #
11
- # class User < ActiveRecord::Base
13
+ # class User < ApplicationRecord
12
14
  # has_one_attached :avatar
13
15
  # end
14
16
  #
@@ -30,8 +32,19 @@ module ActiveStorage
30
32
  #
31
33
  # If the +:dependent+ option isn't set, the attachment will be purged
32
34
  # (i.e. destroyed) whenever the record is destroyed.
33
- def has_one_attached(name, dependent: :purge_later)
35
+ #
36
+ # If you need the attachment to use a service which differs from the globally configured one,
37
+ # pass the +:service+ option. For instance:
38
+ #
39
+ # class User < ActiveRecord::Base
40
+ # has_one_attached :avatar, service: :s3
41
+ # end
42
+ #
43
+ def has_one_attached(name, dependent: :purge_later, service: nil)
44
+ validate_service_configuration(name, service)
45
+
34
46
  generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
47
+ # frozen_string_literal: true
35
48
  def #{name}
36
49
  @active_storage_attached ||= {}
37
50
  @active_storage_attached[:#{name}] ||= ActiveStorage::Attached::One.new("#{name}", self)
@@ -56,16 +69,19 @@ module ActiveStorage
56
69
 
57
70
  after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
58
71
 
59
- ActiveRecord::Reflection.add_attachment_reflection(
60
- self,
72
+ reflection = ActiveRecord::Reflection.create(
73
+ :has_one_attached,
61
74
  name,
62
- ActiveRecord::Reflection.create(:has_one_attached, name, nil, { dependent: dependent }, self)
75
+ nil,
76
+ { dependent: dependent, service_name: service },
77
+ self
63
78
  )
79
+ ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)
64
80
  end
65
81
 
66
82
  # Specifies the relation between multiple attachments and the model.
67
83
  #
68
- # class Gallery < ActiveRecord::Base
84
+ # class Gallery < ApplicationRecord
69
85
  # has_many_attached :photos
70
86
  # end
71
87
  #
@@ -87,8 +103,19 @@ module ActiveStorage
87
103
  #
88
104
  # If the +:dependent+ option isn't set, all the attachments will be purged
89
105
  # (i.e. destroyed) whenever the record is destroyed.
90
- def has_many_attached(name, dependent: :purge_later)
106
+ #
107
+ # If you need the attachment to use a service which differs from the globally configured one,
108
+ # pass the +:service+ option. For instance:
109
+ #
110
+ # class Gallery < ActiveRecord::Base
111
+ # has_many_attached :photos, service: :s3
112
+ # end
113
+ #
114
+ def has_many_attached(name, dependent: :purge_later, service: nil)
115
+ validate_service_configuration(name, service)
116
+
91
117
  generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
118
+ # frozen_string_literal: true
92
119
  def #{name}
93
120
  @active_storage_attached ||= {}
94
121
  @active_storage_attached[:#{name}] ||= ActiveStorage::Attached::Many.new("#{name}", self)
@@ -130,12 +157,24 @@ module ActiveStorage
130
157
 
131
158
  after_commit(on: %i[ create update ]) { attachment_changes.delete(name.to_s).try(:upload) }
132
159
 
133
- ActiveRecord::Reflection.add_attachment_reflection(
134
- self,
160
+ reflection = ActiveRecord::Reflection.create(
161
+ :has_many_attached,
135
162
  name,
136
- ActiveRecord::Reflection.create(:has_many_attached, name, nil, { dependent: dependent }, self)
163
+ nil,
164
+ { dependent: dependent, service_name: service },
165
+ self
137
166
  )
167
+ ActiveRecord::Reflection.add_attachment_reflection(self, name, reflection)
138
168
  end
169
+
170
+ private
171
+ def validate_service_configuration(association_name, service)
172
+ if service.present?
173
+ ActiveStorage::Blob.services.fetch(service) do
174
+ raise ArgumentError, "Cannot configure service :#{service} for #{name}##{association_name}"
175
+ end
176
+ end
177
+ end
139
178
  end
140
179
 
141
180
  def attachment_changes #:nodoc:
@@ -3,7 +3,7 @@
3
3
  module ActiveStorage
4
4
  # Representation of a single attachment to a model.
5
5
  class Attached::One < Attached
6
- delegate_missing_to :attachment
6
+ delegate_missing_to :attachment, allow_nil: true
7
7
 
8
8
  # Returns the associated attachment record.
9
9
  #
@@ -29,7 +29,8 @@ module ActiveStorage
29
29
  # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
30
30
  def attach(attachable)
31
31
  if record.persisted? && !record.changed?
32
- record.update(name => attachable)
32
+ record.public_send("#{name}=", attachable)
33
+ record.save
33
34
  else
34
35
  record.public_send("#{name}=", attachable)
35
36
  end
@@ -37,7 +38,7 @@ module ActiveStorage
37
38
 
38
39
  # Returns +true+ if an attachment has been made.
39
40
  #
40
- # class User < ActiveRecord::Base
41
+ # class User < ApplicationRecord
41
42
  # has_one_attached :avatar
42
43
  # end
43
44
  #