activestorage 5.2.0.beta2 → 5.2.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 (43) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +25 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +15 -1
  5. data/app/assets/javascripts/activestorage.js +1 -1
  6. data/app/controllers/active_storage/blobs_controller.rb +4 -6
  7. data/app/controllers/active_storage/previews_controller.rb +4 -6
  8. data/app/controllers/active_storage/variants_controller.rb +4 -6
  9. data/app/controllers/concerns/active_storage/set_blob.rb +16 -0
  10. data/app/javascript/activestorage/blob_record.js +17 -3
  11. data/app/javascript/activestorage/helpers.js +10 -1
  12. data/app/models/active_storage/attachment.rb +5 -1
  13. data/app/models/active_storage/blob.rb +15 -134
  14. data/app/models/active_storage/blob/analyzable.rb +57 -0
  15. data/app/models/active_storage/blob/identifiable.rb +11 -0
  16. data/app/models/active_storage/blob/representable.rb +93 -0
  17. data/app/models/active_storage/filename.rb +1 -1
  18. data/app/models/active_storage/filename/parameters.rb +1 -1
  19. data/app/models/active_storage/identification.rb +38 -0
  20. data/app/models/active_storage/variant.rb +51 -5
  21. data/app/models/active_storage/variation.rb +19 -5
  22. data/config/routes.rb +7 -7
  23. data/lib/active_storage.rb +8 -1
  24. data/lib/active_storage/analyzer.rb +1 -1
  25. data/lib/active_storage/analyzer/image_analyzer.rb +1 -2
  26. data/lib/active_storage/analyzer/video_analyzer.rb +51 -10
  27. data/lib/active_storage/attached/macros.rb +4 -4
  28. data/lib/active_storage/downloading.rb +16 -4
  29. data/lib/active_storage/engine.rb +18 -1
  30. data/lib/active_storage/errors.rb +7 -0
  31. data/lib/active_storage/gem_version.rb +1 -1
  32. data/lib/active_storage/log_subscriber.rb +4 -0
  33. data/lib/active_storage/previewer.rb +21 -5
  34. data/lib/active_storage/previewer/pdf_previewer.rb +10 -1
  35. data/lib/active_storage/previewer/video_previewer.rb +5 -1
  36. data/lib/active_storage/service.rb +8 -3
  37. data/lib/active_storage/service/azure_storage_service.rb +24 -8
  38. data/lib/active_storage/service/disk_service.rb +26 -24
  39. data/lib/active_storage/service/gcs_service.rb +21 -7
  40. data/lib/active_storage/service/mirror_service.rb +5 -0
  41. data/lib/active_storage/service/s3_service.rb +13 -7
  42. data/lib/tasks/activestorage.rake +5 -1
  43. metadata +43 -9
@@ -38,13 +38,13 @@ module ActiveStorage
38
38
  end
39
39
  CODE
40
40
 
41
- has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record
41
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record
42
42
  has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
43
43
 
44
44
  scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
45
45
 
46
46
  if dependent == :purge_later
47
- before_destroy { public_send(name).purge_later }
47
+ after_destroy_commit { public_send(name).purge_later }
48
48
  end
49
49
  end
50
50
 
@@ -83,13 +83,13 @@ module ActiveStorage
83
83
  end
84
84
  CODE
85
85
 
86
- has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment"
86
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record
87
87
  has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
88
88
 
89
89
  scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
90
90
 
91
91
  if dependent == :purge_later
92
- before_destroy { public_send(name).purge_later }
92
+ after_destroy_commit { public_send(name).purge_later }
93
93
  end
94
94
  end
95
95
  end
@@ -1,25 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "tmpdir"
4
+
3
5
  module ActiveStorage
4
6
  module Downloading
5
7
  private
6
8
  # Opens a new tempfile in #tempdir and copies blob data into it. Yields the tempfile.
7
- def download_blob_to_tempfile # :doc:
8
- Tempfile.open("ActiveStorage", tempdir) do |file|
9
+ def download_blob_to_tempfile #:doc:
10
+ open_tempfile_for_blob do |file|
9
11
  download_blob_to file
10
12
  yield file
11
13
  end
12
14
  end
13
15
 
16
+ def open_tempfile_for_blob
17
+ tempfile = Tempfile.open([ "ActiveStorage", blob.filename.extension_with_delimiter ], tempdir)
18
+
19
+ begin
20
+ yield tempfile
21
+ ensure
22
+ tempfile.close!
23
+ end
24
+ end
25
+
14
26
  # Efficiently downloads blob data into the given file.
15
- def download_blob_to(file) # :doc:
27
+ def download_blob_to(file) #:doc:
16
28
  file.binmode
17
29
  blob.download { |chunk| file.write(chunk) }
18
30
  file.rewind
19
31
  end
20
32
 
21
33
  # Returns the directory in which tempfiles should be opened. Defaults to +Dir.tmpdir+.
22
- def tempdir # :doc:
34
+ def tempdir #:doc:
23
35
  Dir.tmpdir
24
36
  end
25
37
  end
@@ -15,7 +15,20 @@ module ActiveStorage
15
15
 
16
16
  config.active_storage = ActiveSupport::OrderedOptions.new
17
17
  config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
18
- config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
18
+ config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
19
+ config.active_storage.paths = ActiveSupport::OrderedOptions.new
20
+
21
+ config.active_storage.variable_content_types = %w( image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop )
22
+ config.active_storage.content_types_to_serve_as_binary = %w(
23
+ text/html
24
+ text/javascript
25
+ image/svg+xml
26
+ application/postscript
27
+ application/x-shockwave-flash
28
+ text/xml
29
+ application/xml
30
+ application/xhtml+xml
31
+ )
19
32
 
20
33
  config.eager_load_namespaces << ActiveStorage
21
34
 
@@ -25,6 +38,10 @@ module ActiveStorage
25
38
  ActiveStorage.queue = app.config.active_storage.queue
26
39
  ActiveStorage.previewers = app.config.active_storage.previewers || []
27
40
  ActiveStorage.analyzers = app.config.active_storage.analyzers || []
41
+ ActiveStorage.paths = app.config.active_storage.paths || {}
42
+
43
+ ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
44
+ ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
28
45
  end
29
46
  end
30
47
 
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveStorage
4
+ class InvariableError < StandardError; end
5
+ class UnpreviewableError < StandardError; end
6
+ class UnrepresentableError < StandardError; end
7
+ end
@@ -10,7 +10,7 @@ module ActiveStorage
10
10
  MAJOR = 5
11
11
  MINOR = 2
12
12
  TINY = 0
13
- PRE = "beta2"
13
+ PRE = "rc1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -18,6 +18,10 @@ module ActiveStorage
18
18
  info event, color("Deleted file from key: #{key_in(event)}", RED)
19
19
  end
20
20
 
21
+ def service_delete_prefixed(event)
22
+ info event, color("Deleted files by key prefix: #{event.payload[:prefix]}", RED)
23
+ end
24
+
21
25
  def service_exist(event)
22
26
  debug event, color("Checked if file exists at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE)
23
27
  end
@@ -42,17 +42,33 @@ module ActiveStorage
42
42
  # end
43
43
  #
44
44
  # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir.
45
- def draw(*argv) # :doc:
46
- Tempfile.open("ActiveStorage", tempdir) do |file|
47
- capture(*argv, to: file)
48
- yield file
45
+ def draw(*argv) #:doc:
46
+ ActiveSupport::Notifications.instrument("preview.active_storage") do
47
+ open_tempfile_for_drawing do |file|
48
+ capture(*argv, to: file)
49
+ yield file
50
+ end
51
+ end
52
+ end
53
+
54
+ def open_tempfile_for_drawing
55
+ tempfile = Tempfile.open("ActiveStorage", tempdir)
56
+
57
+ begin
58
+ yield tempfile
59
+ ensure
60
+ tempfile.close!
49
61
  end
50
62
  end
51
63
 
52
64
  def capture(*argv, to:)
53
65
  to.binmode
54
- IO.popen(argv) { |out| IO.copy_stream(out, to) }
66
+ IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) }
55
67
  to.rewind
56
68
  end
69
+
70
+ def logger #:doc:
71
+ ActiveStorage.logger
72
+ end
57
73
  end
58
74
  end
@@ -8,10 +8,19 @@ module ActiveStorage
8
8
 
9
9
  def preview
10
10
  download_blob_to_tempfile do |input|
11
- draw "mutool", "draw", "-F", "png", "-o", "-", input.path, "1" do |output|
11
+ draw_first_page_from input do |output|
12
12
  yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
13
13
  end
14
14
  end
15
15
  end
16
+
17
+ private
18
+ def draw_first_page_from(file, &block)
19
+ draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
20
+ end
21
+
22
+ def mutool_path
23
+ ActiveStorage.paths[:mutool] || "mutool"
24
+ end
16
25
  end
17
26
  end
@@ -16,8 +16,12 @@ module ActiveStorage
16
16
 
17
17
  private
18
18
  def draw_relevant_frame_from(file, &block)
19
- draw "ffmpeg", "-i", file.path, "-y", "-vcodec", "png",
19
+ draw ffmpeg_path, "-i", file.path, "-y", "-vcodec", "png",
20
20
  "-vf", "thumbnail", "-vframes", "1", "-f", "image2", "-", &block
21
21
  end
22
+
23
+ def ffmpeg_path
24
+ ActiveStorage.paths[:ffmpeg] || "ffmpeg"
25
+ end
22
26
  end
23
27
  end
@@ -78,6 +78,11 @@ module ActiveStorage
78
78
  raise NotImplementedError
79
79
  end
80
80
 
81
+ # Delete files at keys starting with the +prefix+.
82
+ def delete_prefixed(prefix)
83
+ raise NotImplementedError
84
+ end
85
+
81
86
  # Return +true+ if a file exists at the +key+.
82
87
  def exist?(key)
83
88
  raise NotImplementedError
@@ -92,7 +97,7 @@ module ActiveStorage
92
97
 
93
98
  # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+.
94
99
  # The URL will be valid for the amount of seconds specified in +expires_in+.
95
- # You most also provide the +content_type+, +content_length+, and +checksum+ of the file
100
+ # You must also provide the +content_type+, +content_length+, and +checksum+ of the file
96
101
  # that will be uploaded. All these attributes will be validated by the service upon upload.
97
102
  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
98
103
  raise NotImplementedError
@@ -104,10 +109,10 @@ module ActiveStorage
104
109
  end
105
110
 
106
111
  private
107
- def instrument(operation, key, payload = {}, &block)
112
+ def instrument(operation, payload = {}, &block)
108
113
  ActiveSupport::Notifications.instrument(
109
114
  "service_#{operation}.active_storage",
110
- payload.merge(key: key, service: service_name), &block)
115
+ payload.merge(service: service_name), &block)
111
116
  end
112
117
 
113
118
  def service_name
@@ -19,7 +19,7 @@ module ActiveStorage
19
19
  end
20
20
 
21
21
  def upload(key, io, checksum: nil)
22
- instrument :upload, key, checksum: checksum do
22
+ instrument :upload, key: key, checksum: checksum do
23
23
  begin
24
24
  blobs.create_block_blob(container, key, io, content_md5: checksum)
25
25
  rescue Azure::Core::Http::HTTPError
@@ -30,11 +30,11 @@ module ActiveStorage
30
30
 
31
31
  def download(key, &block)
32
32
  if block_given?
33
- instrument :streaming_download, key do
33
+ instrument :streaming_download, key: key do
34
34
  stream(key, &block)
35
35
  end
36
36
  else
37
- instrument :download, key do
37
+ instrument :download, key: key do
38
38
  _, io = blobs.get_blob(container, key)
39
39
  io.force_encoding(Encoding::BINARY)
40
40
  end
@@ -42,17 +42,33 @@ module ActiveStorage
42
42
  end
43
43
 
44
44
  def delete(key)
45
- instrument :delete, key do
45
+ instrument :delete, key: key do
46
46
  begin
47
47
  blobs.delete_blob(container, key)
48
48
  rescue Azure::Core::Http::HTTPError
49
- false
49
+ # Ignore files already deleted
50
+ end
51
+ end
52
+ end
53
+
54
+ def delete_prefixed(prefix)
55
+ instrument :delete_prefixed, prefix: prefix do
56
+ marker = nil
57
+
58
+ loop do
59
+ results = blobs.list_blobs(container, prefix: prefix, marker: marker)
60
+
61
+ results.each do |blob|
62
+ blobs.delete_blob(container, blob.name)
63
+ end
64
+
65
+ break unless marker = results.continuation_token.presence
50
66
  end
51
67
  end
52
68
  end
53
69
 
54
70
  def exist?(key)
55
- instrument :exist, key do |payload|
71
+ instrument :exist, key: key do |payload|
56
72
  answer = blob_for(key).present?
57
73
  payload[:exist] = answer
58
74
  answer
@@ -60,7 +76,7 @@ module ActiveStorage
60
76
  end
61
77
 
62
78
  def url(key, expires_in:, filename:, disposition:, content_type:)
63
- instrument :url, key do |payload|
79
+ instrument :url, key: key do |payload|
64
80
  base_url = url_for(key)
65
81
  generated_url = signer.signed_uri(
66
82
  URI(base_url), false,
@@ -77,7 +93,7 @@ module ActiveStorage
77
93
  end
78
94
 
79
95
  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
80
- instrument :url, key do |payload|
96
+ instrument :url, key: key do |payload|
81
97
  base_url = url_for(key)
82
98
  generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw",
83
99
  expiry: format_expiry(expires_in)).to_s
@@ -9,14 +9,14 @@ module ActiveStorage
9
9
  # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API
10
10
  # documentation that applies to all services.
11
11
  class Service::DiskService < Service
12
- attr_reader :root
12
+ attr_reader :root, :host
13
13
 
14
- def initialize(root:)
15
- @root = root
14
+ def initialize(root:, host: "http://localhost:3000")
15
+ @root, @host = root, host
16
16
  end
17
17
 
18
18
  def upload(key, io, checksum: nil)
19
- instrument :upload, key, checksum: checksum do
19
+ instrument :upload, key: key, checksum: checksum do
20
20
  IO.copy_stream(io, make_path_for(key))
21
21
  ensure_integrity_of(key, checksum) if checksum
22
22
  end
@@ -24,7 +24,7 @@ module ActiveStorage
24
24
 
25
25
  def download(key)
26
26
  if block_given?
27
- instrument :streaming_download, key do
27
+ instrument :streaming_download, key: key do
28
28
  File.open(path_for(key), "rb") do |file|
29
29
  while data = file.read(64.kilobytes)
30
30
  yield data
@@ -32,14 +32,14 @@ module ActiveStorage
32
32
  end
33
33
  end
34
34
  else
35
- instrument :download, key do
35
+ instrument :download, key: key do
36
36
  File.binread path_for(key)
37
37
  end
38
38
  end
39
39
  end
40
40
 
41
41
  def delete(key)
42
- instrument :delete, key do
42
+ instrument :delete, key: key do
43
43
  begin
44
44
  File.delete path_for(key)
45
45
  rescue Errno::ENOENT
@@ -48,8 +48,16 @@ module ActiveStorage
48
48
  end
49
49
  end
50
50
 
51
+ def delete_prefixed(prefix)
52
+ instrument :delete_prefixed, prefix: prefix do
53
+ Dir.glob(path_for("#{prefix}*")).each do |path|
54
+ FileUtils.rm_rf(path)
55
+ end
56
+ end
57
+ end
58
+
51
59
  def exist?(key)
52
- instrument :exist, key do |payload|
60
+ instrument :exist, key: key do |payload|
53
61
  answer = File.exist? path_for(key)
54
62
  payload[:exist] = answer
55
63
  answer
@@ -57,18 +65,17 @@ module ActiveStorage
57
65
  end
58
66
 
59
67
  def url(key, expires_in:, filename:, disposition:, content_type:)
60
- instrument :url, key do |payload|
68
+ instrument :url, key: key do |payload|
61
69
  verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
62
70
 
63
71
  generated_url =
64
- if defined?(Rails.application)
65
- Rails.application.routes.url_helpers.rails_disk_service_path \
66
- verified_key_with_expiration,
67
- filename: filename, disposition: content_disposition_with(type: disposition, filename: filename), content_type: content_type
68
- else
69
- "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?content_type=#{content_type}" \
70
- "&disposition=#{content_disposition_with(type: disposition, filename: filename)}"
71
- end
72
+ Rails.application.routes.url_helpers.rails_disk_service_url(
73
+ verified_key_with_expiration,
74
+ filename: filename,
75
+ disposition: content_disposition_with(type: disposition, filename: filename),
76
+ content_type: content_type,
77
+ host: host
78
+ )
72
79
 
73
80
  payload[:url] = generated_url
74
81
 
@@ -77,7 +84,7 @@ module ActiveStorage
77
84
  end
78
85
 
79
86
  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
80
- instrument :url, key do |payload|
87
+ instrument :url, key: key do |payload|
81
88
  verified_token_with_expiration = ActiveStorage.verifier.generate(
82
89
  {
83
90
  key: key,
@@ -89,12 +96,7 @@ module ActiveStorage
89
96
  purpose: :blob_token }
90
97
  )
91
98
 
92
- generated_url =
93
- if defined?(Rails.application)
94
- Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration
95
- else
96
- "/rails/active_storage/disk/#{verified_token_with_expiration}"
97
- end
99
+ generated_url = Rails.application.routes.url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: host)
98
100
 
99
101
  payload[:url] = generated_url
100
102
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ gem "google-cloud-storage", "~> 1.8"
4
+
3
5
  require "google/cloud/storage"
4
6
  require "active_support/core_ext/object/to_query"
5
7
 
@@ -12,9 +14,15 @@ module ActiveStorage
12
14
  end
13
15
 
14
16
  def upload(key, io, checksum: nil)
15
- instrument :upload, key, checksum: checksum do
17
+ instrument :upload, key: key, checksum: checksum do
16
18
  begin
17
- bucket.create_file(io, key, md5: checksum)
19
+ # The official GCS client library doesn't allow us to create a file with no Content-Type metadata.
20
+ # We need the file we create to have no Content-Type so we can control it via the response-content-type
21
+ # param in signed URLs. Workaround: let the GCS client create the file with an inferred
22
+ # Content-Type (usually "application/octet-stream") then clear it.
23
+ bucket.create_file(io, key, md5: checksum).update do |file|
24
+ file.content_type = nil
25
+ end
18
26
  rescue Google::Cloud::InvalidArgumentError
19
27
  raise ActiveStorage::IntegrityError
20
28
  end
@@ -23,7 +31,7 @@ module ActiveStorage
23
31
 
24
32
  # FIXME: Download in chunks when given a block.
25
33
  def download(key)
26
- instrument :download, key do
34
+ instrument :download, key: key do
27
35
  io = file_for(key).download
28
36
  io.rewind
29
37
 
@@ -36,7 +44,7 @@ module ActiveStorage
36
44
  end
37
45
 
38
46
  def delete(key)
39
- instrument :delete, key do
47
+ instrument :delete, key: key do
40
48
  begin
41
49
  file_for(key).delete
42
50
  rescue Google::Cloud::NotFoundError
@@ -45,8 +53,14 @@ module ActiveStorage
45
53
  end
46
54
  end
47
55
 
56
+ def delete_prefixed(prefix)
57
+ instrument :delete_prefixed, prefix: prefix do
58
+ bucket.files(prefix: prefix).all(&:delete)
59
+ end
60
+ end
61
+
48
62
  def exist?(key)
49
- instrument :exist, key do |payload|
63
+ instrument :exist, key: key do |payload|
50
64
  answer = file_for(key).exists?
51
65
  payload[:exist] = answer
52
66
  answer
@@ -54,7 +68,7 @@ module ActiveStorage
54
68
  end
55
69
 
56
70
  def url(key, expires_in:, filename:, content_type:, disposition:)
57
- instrument :url, key do |payload|
71
+ instrument :url, key: key do |payload|
58
72
  generated_url = file_for(key).signed_url expires: expires_in, query: {
59
73
  "response-content-disposition" => content_disposition_with(type: disposition, filename: filename),
60
74
  "response-content-type" => content_type
@@ -67,7 +81,7 @@ module ActiveStorage
67
81
  end
68
82
 
69
83
  def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
70
- instrument :url, key do |payload|
84
+ instrument :url, key: key do |payload|
71
85
  generated_url = bucket.signed_url key, method: "PUT", expires: expires_in,
72
86
  content_type: content_type, content_md5: checksum
73
87