activestorage 5.2.4.1 → 6.0.1

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -61
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +9 -6
  5. data/app/assets/javascripts/activestorage.js +4 -1
  6. data/app/controllers/active_storage/base_controller.rb +3 -5
  7. data/app/controllers/active_storage/blobs_controller.rb +1 -1
  8. data/app/controllers/active_storage/disk_controller.rb +5 -2
  9. data/app/controllers/active_storage/representations_controller.rb +1 -1
  10. data/app/controllers/concerns/active_storage/set_current.rb +15 -0
  11. data/app/javascript/activestorage/blob_record.js +6 -1
  12. data/app/jobs/active_storage/analyze_job.rb +5 -0
  13. data/app/jobs/active_storage/base_job.rb +0 -1
  14. data/app/jobs/active_storage/purge_job.rb +3 -0
  15. data/app/models/active_storage/attachment.rb +20 -9
  16. data/app/models/active_storage/blob.rb +84 -31
  17. data/app/models/active_storage/blob/representable.rb +5 -5
  18. data/app/models/active_storage/filename.rb +0 -6
  19. data/app/models/active_storage/preview.rb +3 -3
  20. data/app/models/active_storage/variant.rb +51 -52
  21. data/app/models/active_storage/variation.rb +24 -33
  22. data/config/routes.rb +13 -12
  23. data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
  24. data/lib/active_storage.rb +26 -6
  25. data/lib/active_storage/analyzer.rb +9 -4
  26. data/lib/active_storage/analyzer/image_analyzer.rb +11 -4
  27. data/lib/active_storage/analyzer/video_analyzer.rb +3 -5
  28. data/lib/active_storage/attached.rb +7 -22
  29. data/lib/active_storage/attached/changes.rb +16 -0
  30. data/lib/active_storage/attached/changes/create_many.rb +46 -0
  31. data/lib/active_storage/attached/changes/create_one.rb +69 -0
  32. data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
  33. data/lib/active_storage/attached/changes/delete_many.rb +27 -0
  34. data/lib/active_storage/attached/changes/delete_one.rb +19 -0
  35. data/lib/active_storage/attached/many.rb +16 -10
  36. data/lib/active_storage/attached/model.rb +147 -0
  37. data/lib/active_storage/attached/one.rb +16 -19
  38. data/lib/active_storage/downloader.rb +43 -0
  39. data/lib/active_storage/downloading.rb +8 -0
  40. data/lib/active_storage/engine.rb +43 -6
  41. data/lib/active_storage/errors.rb +22 -3
  42. data/lib/active_storage/gem_version.rb +4 -4
  43. data/lib/active_storage/previewer.rb +21 -11
  44. data/lib/active_storage/previewer/poppler_pdf_previewer.rb +2 -2
  45. data/lib/active_storage/previewer/video_previewer.rb +2 -3
  46. data/lib/active_storage/reflection.rb +64 -0
  47. data/lib/active_storage/service.rb +9 -6
  48. data/lib/active_storage/service/azure_storage_service.rb +30 -14
  49. data/lib/active_storage/service/configurator.rb +3 -1
  50. data/lib/active_storage/service/disk_service.rb +19 -11
  51. data/lib/active_storage/service/gcs_service.rb +49 -47
  52. data/lib/active_storage/service/s3_service.rb +10 -6
  53. data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
  54. data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
  55. data/lib/active_storage/transformers/transformer.rb +42 -0
  56. data/lib/tasks/activestorage.rake +7 -0
  57. metadata +41 -12
  58. data/app/models/active_storage/filename/parameters.rb +0 -36
  59. data/lib/active_storage/attached/macros.rb +0 -110
@@ -1,7 +1,26 @@
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
7
26
  end
@@ -7,10 +7,10 @@ module ActiveStorage
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 4
13
- PRE = "1"
10
+ MAJOR = 6
11
+ MINOR = 0
12
+ TINY = 1
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  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
@@ -28,9 +24,14 @@ module ActiveStorage
28
24
  end
29
25
 
30
26
  private
27
+ # Downloads the blob to a tempfile on disk. Yields the tempfile.
28
+ def download_blob_to_tempfile(&block) #:doc:
29
+ blob.open tmpdir: tmpdir, &block
30
+ end
31
+
31
32
  # Executes a system command, capturing its binary output in a tempfile. Yields the tempfile.
32
33
  #
33
- # Use this method to shell out to a system library (e.g. mupdf or ffmpeg) for preview image
34
+ # Use this method to shell out to a system library (e.g. muPDF or FFmpeg) for preview image
34
35
  # generation. The resulting tempfile can be used as the +:io+ value in an attachable Hash:
35
36
  #
36
37
  # def preview
@@ -41,18 +42,19 @@ module ActiveStorage
41
42
  # end
42
43
  # end
43
44
  #
44
- # The output tempfile is opened in the directory returned by ActiveStorage::Downloading#tempdir.
45
+ # The output tempfile is opened in the directory returned by #tmpdir.
45
46
  def draw(*argv) #:doc:
46
- ActiveSupport::Notifications.instrument("preview.active_storage") do
47
- open_tempfile_for_drawing do |file|
47
+ open_tempfile do |file|
48
+ instrument :preview, key: blob.key do
48
49
  capture(*argv, to: file)
49
- yield file
50
50
  end
51
+
52
+ yield file
51
53
  end
52
54
  end
53
55
 
54
- def open_tempfile_for_drawing
55
- tempfile = Tempfile.open("ActiveStorage", tempdir)
56
+ def open_tempfile
57
+ tempfile = Tempfile.open("ActiveStorage-", tmpdir)
56
58
 
57
59
  begin
58
60
  yield tempfile
@@ -61,6 +63,10 @@ module ActiveStorage
61
63
  end
62
64
  end
63
65
 
66
+ def instrument(operation, payload = {}, &block)
67
+ ActiveSupport::Notifications.instrument "#{operation}.active_storage", payload, &block
68
+ end
69
+
64
70
  def capture(*argv, to:)
65
71
  to.binmode
66
72
  IO.popen(argv, err: File::NULL) { |out| IO.copy_stream(out, to) }
@@ -70,5 +76,9 @@ module ActiveStorage
70
76
  def logger #:doc:
71
77
  ActiveStorage.logger
72
78
  end
79
+
80
+ def tmpdir #:doc:
81
+ Dir.tmpdir
82
+ end
73
83
  end
74
84
  end
@@ -12,7 +12,7 @@ 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
@@ -28,7 +28,7 @@ module ActiveStorage
28
28
 
29
29
  private
30
30
  def draw_first_page_from(file, &block)
31
- # use 72 dpi to match thumbnail dimesions of the PDF
31
+ # use 72 dpi to match thumbnail dimensions of the PDF
32
32
  draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
33
33
  end
34
34
  end
@@ -9,15 +9,14 @@ module ActiveStorage
9
9
  def preview
10
10
  download_blob_to_tempfile do |input|
11
11
  draw_relevant_frame_from input do |output|
12
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
12
+ yield io: output, filename: "#{blob.filename.base}.jpg", content_type: "image/jpeg"
13
13
  end
14
14
  end
15
15
  end
16
16
 
17
17
  private
18
18
  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
19
+ draw ffmpeg_path, "-i", file.path, "-y", "-vframes", "1", "-f", "image2", "-", &block
21
20
  end
22
21
 
23
22
  def ffmpeg_path
@@ -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
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_storage/log_subscriber"
4
+ require "action_dispatch"
5
+ require "action_dispatch/http/content_disposition"
4
6
 
5
7
  module ActiveStorage
6
- class IntegrityError < StandardError; end
7
-
8
8
  # Abstract class serving as an interface for concrete services.
9
9
  #
10
10
  # The available services are:
@@ -41,8 +41,6 @@ module ActiveStorage
41
41
  extend ActiveSupport::Autoload
42
42
  autoload :Configurator
43
43
 
44
- class_attribute :url_expires_in, default: 5.minutes
45
-
46
44
  class << self
47
45
  # Configure an Active Storage service by name from a set of configurations,
48
46
  # typically loaded from a YAML file. The Active Storage engine uses this
@@ -84,6 +82,10 @@ module ActiveStorage
84
82
  raise NotImplementedError
85
83
  end
86
84
 
85
+ def open(*args, &block)
86
+ ActiveStorage::Downloader.new(self).open(*args, &block)
87
+ end
88
+
87
89
  # Delete the file at the +key+.
88
90
  def delete(key)
89
91
  raise NotImplementedError
@@ -100,7 +102,7 @@ module ActiveStorage
100
102
  end
101
103
 
102
104
  # Returns a signed, temporary URL for the file at the +key+. The URL will be valid for the amount
103
- # of seconds specified in +expires_in+. You most also provide the +disposition+ (+:inline+ or +:attachment+),
105
+ # of seconds specified in +expires_in+. You must also provide the +disposition+ (+:inline+ or +:attachment+),
104
106
  # +filename+, and +content_type+ that you wish the file to be served with on request.
105
107
  def url(key, expires_in:, disposition:, filename:, content_type:)
106
108
  raise NotImplementedError
@@ -132,7 +134,8 @@ module ActiveStorage
132
134
  end
133
135
 
134
136
  def content_disposition_with(type: "inline", filename:)
135
- (type.to_s.presence_in(%w( attachment inline )) || "inline") + "; #{filename.parameters}"
137
+ disposition = (type.to_s.presence_in(%w( attachment inline )) || "inline")
138
+ ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized)
136
139
  end
137
140
  end
138
141
  end
@@ -10,8 +10,8 @@ module ActiveStorage
10
10
  class Service::AzureStorageService < Service
11
11
  attr_reader :client, :blobs, :container, :signer
12
12
 
13
- def initialize(storage_account_name:, storage_access_key:, container:)
14
- @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
13
+ def initialize(storage_account_name:, storage_access_key:, container:, **options)
14
+ @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
15
15
  @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
16
16
  @blobs = client.blob_client
17
17
  @container = container
@@ -19,10 +19,8 @@ module ActiveStorage
19
19
 
20
20
  def upload(key, io, checksum: nil, **)
21
21
  instrument :upload, key: key, checksum: checksum do
22
- begin
22
+ handle_errors do
23
23
  blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum)
24
- rescue Azure::Core::Http::HTTPError
25
- raise ActiveStorage::IntegrityError
26
24
  end
27
25
  end
28
26
  end
@@ -34,26 +32,29 @@ module ActiveStorage
34
32
  end
35
33
  else
36
34
  instrument :download, key: key do
37
- _, io = blobs.get_blob(container, key)
38
- io.force_encoding(Encoding::BINARY)
35
+ handle_errors do
36
+ _, io = blobs.get_blob(container, key)
37
+ io.force_encoding(Encoding::BINARY)
38
+ end
39
39
  end
40
40
  end
41
41
  end
42
42
 
43
43
  def download_chunk(key, range)
44
44
  instrument :download_chunk, key: key, range: range do
45
- _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
46
- io.force_encoding(Encoding::BINARY)
45
+ handle_errors do
46
+ _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
47
+ io.force_encoding(Encoding::BINARY)
48
+ end
47
49
  end
48
50
  end
49
51
 
50
52
  def delete(key)
51
53
  instrument :delete, key: key do
52
- begin
53
- blobs.delete_blob(container, key)
54
- rescue Azure::Core::Http::HTTPError
55
- # Ignore files already deleted
56
- end
54
+ blobs.delete_blob(container, key)
55
+ rescue Azure::Core::Http::HTTPError => e
56
+ raise unless e.type == "BlobNotFound"
57
+ # Ignore files already deleted
57
58
  end
58
59
  end
59
60
 
@@ -139,11 +140,26 @@ module ActiveStorage
139
140
  chunk_size = 5.megabytes
140
141
  offset = 0
141
142
 
143
+ raise ActiveStorage::FileNotFoundError unless blob.present?
144
+
142
145
  while offset < blob.properties[:content_length]
143
146
  _, chunk = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1)
144
147
  yield chunk.force_encoding(Encoding::BINARY)
145
148
  offset += chunk_size
146
149
  end
147
150
  end
151
+
152
+ def handle_errors
153
+ yield
154
+ rescue Azure::Core::Http::HTTPError => e
155
+ case e.type
156
+ when "BlobNotFound"
157
+ raise ActiveStorage::FileNotFoundError
158
+ when "Md5Mismatch"
159
+ raise ActiveStorage::IntegrityError
160
+ else
161
+ raise
162
+ end
163
+ end
148
164
  end
149
165
  end
@@ -26,7 +26,9 @@ module ActiveStorage
26
26
 
27
27
  def resolve(class_name)
28
28
  require "active_storage/service/#{class_name.to_s.underscore}_service"
29
- ActiveStorage::Service.const_get(:"#{class_name}Service")
29
+ ActiveStorage::Service.const_get(:"#{class_name.camelize}Service")
30
+ rescue LoadError
31
+ raise "Missing service adapter for #{class_name.inspect}"
30
32
  end
31
33
  end
32
34
  end
@@ -22,18 +22,16 @@ module ActiveStorage
22
22
  end
23
23
  end
24
24
 
25
- def download(key)
25
+ def download(key, &block)
26
26
  if block_given?
27
27
  instrument :streaming_download, key: key do
28
- File.open(path_for(key), "rb") do |file|
29
- while data = file.read(5.megabytes)
30
- yield data
31
- end
32
- end
28
+ stream key, &block
33
29
  end
34
30
  else
35
31
  instrument :download, key: key do
36
32
  File.binread path_for(key)
33
+ rescue Errno::ENOENT
34
+ raise ActiveStorage::FileNotFoundError
37
35
  end
38
36
  end
39
37
  end
@@ -44,16 +42,16 @@ module ActiveStorage
44
42
  file.seek range.begin
45
43
  file.read range.size
46
44
  end
45
+ rescue Errno::ENOENT
46
+ raise ActiveStorage::FileNotFoundError
47
47
  end
48
48
  end
49
49
 
50
50
  def delete(key)
51
51
  instrument :delete, key: key do
52
- begin
53
- File.delete path_for(key)
54
- rescue Errno::ENOENT
55
- # Ignore files already deleted
56
- end
52
+ File.delete path_for(key)
53
+ rescue Errno::ENOENT
54
+ # Ignore files already deleted
57
55
  end
58
56
  end
59
57
 
@@ -132,6 +130,16 @@ module ActiveStorage
132
130
  end
133
131
 
134
132
  private
133
+ def stream(key)
134
+ File.open(path_for(key), "rb") do |file|
135
+ while data = file.read(5.megabytes)
136
+ yield data
137
+ end
138
+ end
139
+ rescue Errno::ENOENT
140
+ raise ActiveStorage::FileNotFoundError
141
+ end
142
+
135
143
  def folder_for(key)
136
144
  [ key[0..1], key[2..3] ].join("/")
137
145
  end
@@ -1,11 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- gem "google-cloud-storage", "~> 1.8"
4
-
3
+ gem "google-cloud-storage", "~> 1.11"
5
4
  require "google/cloud/storage"
6
- require "net/http"
7
-
8
- require "active_support/core_ext/object/to_query"
9
5
 
10
6
  module ActiveStorage
11
7
  # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
@@ -17,15 +13,27 @@ module ActiveStorage
17
13
 
18
14
  def upload(key, io, checksum: nil, content_type: nil, disposition: nil, filename: nil)
19
15
  instrument :upload, key: key, checksum: checksum do
20
- begin
21
- # GCS's signed URLs don't include params such as response-content-type response-content_disposition
22
- # in the signature, which means an attacker can modify them and bypass our effort to force these to
23
- # binary and attachment when the file's content type requires it. The only way to force them is to
24
- # store them as object's metadata.
25
- content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
26
- bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition)
27
- rescue Google::Cloud::InvalidArgumentError
28
- raise ActiveStorage::IntegrityError
16
+ # GCS's signed URLs don't include params such as response-content-type response-content_disposition
17
+ # in the signature, which means an attacker can modify them and bypass our effort to force these to
18
+ # binary and attachment when the file's content type requires it. The only way to force them is to
19
+ # store them as object's metadata.
20
+ content_disposition = content_disposition_with(type: disposition, filename: filename) if disposition && filename
21
+ bucket.create_file(io, key, md5: checksum, content_type: content_type, content_disposition: content_disposition)
22
+ rescue Google::Cloud::InvalidArgumentError
23
+ raise ActiveStorage::IntegrityError
24
+ end
25
+ end
26
+
27
+ def download(key, &block)
28
+ if block_given?
29
+ instrument :streaming_download, key: key do
30
+ stream(key, &block)
31
+ end
32
+ else
33
+ instrument :download, key: key do
34
+ file_for(key).download.string
35
+ rescue Google::Cloud::NotFoundError
36
+ raise ActiveStorage::FileNotFoundError
29
37
  end
30
38
  end
31
39
  end
@@ -39,49 +47,28 @@ module ActiveStorage
39
47
  end
40
48
  end
41
49
 
42
- # FIXME: Download in chunks when given a block.
43
- def download(key)
44
- instrument :download, key: key do
45
- io = file_for(key).download
46
- io.rewind
47
-
48
- if block_given?
49
- yield io.string
50
- else
51
- io.string
52
- end
53
- end
54
- end
55
-
56
50
  def download_chunk(key, range)
57
51
  instrument :download_chunk, key: key, range: range do
58
- file = file_for(key)
59
- uri = URI(file.signed_url(expires: 30.seconds))
60
-
61
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
62
- client.get(uri, "Range" => "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body
63
- end
52
+ file_for(key).download(range: range).string
53
+ rescue Google::Cloud::NotFoundError
54
+ raise ActiveStorage::FileNotFoundError
64
55
  end
65
56
  end
66
57
 
67
58
  def delete(key)
68
59
  instrument :delete, key: key do
69
- begin
70
- file_for(key).delete
71
- rescue Google::Cloud::NotFoundError
72
- # Ignore files already deleted
73
- end
60
+ file_for(key).delete
61
+ rescue Google::Cloud::NotFoundError
62
+ # Ignore files already deleted
74
63
  end
75
64
  end
76
65
 
77
66
  def delete_prefixed(prefix)
78
67
  instrument :delete_prefixed, prefix: prefix do
79
68
  bucket.files(prefix: prefix).all do |file|
80
- begin
81
- file.delete
82
- rescue Google::Cloud::NotFoundError
83
- # Ignore concurrently-deleted files
84
- end
69
+ file.delete
70
+ rescue Google::Cloud::NotFoundError
71
+ # Ignore concurrently-deleted files
85
72
  end
86
73
  end
87
74
  end
@@ -124,12 +111,27 @@ module ActiveStorage
124
111
  private
125
112
  attr_reader :config
126
113
 
127
- def file_for(key)
128
- bucket.file(key, skip_lookup: true)
114
+ def file_for(key, skip_lookup: true)
115
+ bucket.file(key, skip_lookup: skip_lookup)
116
+ end
117
+
118
+ # Reads the file for the given key in chunks, yielding each to the block.
119
+ def stream(key)
120
+ file = file_for(key, skip_lookup: false)
121
+
122
+ chunk_size = 5.megabytes
123
+ offset = 0
124
+
125
+ raise ActiveStorage::FileNotFoundError unless file.present?
126
+
127
+ while offset < file.size
128
+ yield file.download(range: offset..(offset + chunk_size - 1)).string
129
+ offset += chunk_size
130
+ end
129
131
  end
130
132
 
131
133
  def bucket
132
- @bucket ||= client.bucket(config.fetch(:bucket))
134
+ @bucket ||= client.bucket(config.fetch(:bucket), skip_lookup: true)
133
135
  end
134
136
 
135
137
  def client