activestorage 5.2.8.1 → 6.0.0.beta1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -157
- data/MIT-LICENSE +1 -1
- data/README.md +6 -5
- data/app/assets/javascripts/activestorage.js +4 -1
- data/app/controllers/active_storage/base_controller.rb +3 -5
- data/app/controllers/active_storage/blobs_controller.rb +1 -1
- data/app/controllers/active_storage/disk_controller.rb +4 -1
- data/app/controllers/active_storage/representations_controller.rb +1 -1
- data/app/controllers/concerns/active_storage/set_current.rb +15 -0
- data/app/javascript/activestorage/blob_record.js +6 -1
- data/app/jobs/active_storage/analyze_job.rb +4 -0
- data/app/jobs/active_storage/base_job.rb +0 -1
- data/app/jobs/active_storage/purge_job.rb +3 -0
- data/app/models/active_storage/attachment.rb +18 -9
- data/app/models/active_storage/blob/representable.rb +5 -5
- data/app/models/active_storage/blob.rb +63 -22
- data/app/models/active_storage/filename.rb +0 -6
- data/app/models/active_storage/preview.rb +3 -3
- data/app/models/active_storage/variant.rb +51 -52
- data/app/models/active_storage/variation.rb +23 -92
- data/config/routes.rb +13 -12
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +7 -0
- data/lib/active_storage/analyzer/video_analyzer.rb +2 -4
- data/lib/active_storage/analyzer.rb +9 -4
- data/lib/active_storage/attached/changes/create_many.rb +46 -0
- data/lib/active_storage/attached/changes/create_one.rb +68 -0
- data/lib/active_storage/attached/changes/create_one_of_many.rb +10 -0
- data/lib/active_storage/attached/changes/delete_many.rb +23 -0
- data/lib/active_storage/attached/changes/delete_one.rb +19 -0
- data/lib/active_storage/attached/changes.rb +16 -0
- data/lib/active_storage/attached/many.rb +16 -10
- data/lib/active_storage/attached/model.rb +140 -0
- data/lib/active_storage/attached/one.rb +16 -19
- data/lib/active_storage/attached.rb +7 -22
- data/lib/active_storage/downloader.rb +44 -0
- data/lib/active_storage/downloading.rb +8 -0
- data/lib/active_storage/engine.rb +36 -21
- data/lib/active_storage/errors.rb +22 -3
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +3 -3
- data/lib/active_storage/previewer/video_previewer.rb +2 -3
- data/lib/active_storage/previewer.rb +21 -11
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service/azure_storage_service.rb +30 -14
- data/lib/active_storage/service/configurator.rb +3 -1
- data/lib/active_storage/service/disk_service.rb +20 -16
- data/lib/active_storage/service/gcs_service.rb +48 -46
- data/lib/active_storage/service/mirror_service.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +10 -9
- data/lib/active_storage/service.rb +5 -6
- data/lib/active_storage/transformers/image_processing_transformer.rb +39 -0
- data/lib/active_storage/transformers/mini_magick_transformer.rb +38 -0
- data/lib/active_storage/transformers/transformer.rb +42 -0
- data/lib/active_storage.rb +13 -292
- data/lib/tasks/activestorage.rake +7 -0
- metadata +31 -19
- data/app/models/active_storage/filename/parameters.rb +0 -36
- 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
|
5
|
-
class
|
6
|
-
|
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
|
@@ -12,7 +12,7 @@ module ActiveStorage
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def pdftoppm_exists?
|
15
|
-
return @pdftoppm_exists
|
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,8 +28,8 @@ module ActiveStorage
|
|
28
28
|
|
29
29
|
private
|
30
30
|
def draw_first_page_from(file, &block)
|
31
|
-
# use 72 dpi to match thumbnail
|
32
|
-
draw self.class.pdftoppm_path, "-singlefile", "-
|
31
|
+
# use 72 dpi to match thumbnail dimesions of the PDF
|
32
|
+
draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
|
33
33
|
end
|
34
34
|
end
|
35
35
|
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}.
|
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", "-
|
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
|
@@ -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 tempdir: tempdir, &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.
|
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
|
45
|
+
# The output tempfile is opened in the directory returned by #tempdir.
|
45
46
|
def draw(*argv) #:doc:
|
46
|
-
|
47
|
-
|
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
|
55
|
-
tempfile = Tempfile.open("ActiveStorage", tempdir)
|
56
|
+
def open_tempfile
|
57
|
+
tempfile = Tempfile.open("ActiveStorage-", tempdir)
|
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 tempdir #:doc:
|
81
|
+
Dir.tmpdir
|
82
|
+
end
|
73
83
|
end
|
74
84
|
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
|
@@ -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:)
|
14
|
+
@client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
|
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
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
|
@@ -86,12 +84,8 @@ module ActiveStorage
|
|
86
84
|
purpose: :blob_key }
|
87
85
|
)
|
88
86
|
|
89
|
-
current_uri = URI.parse(current_host)
|
90
|
-
|
91
87
|
generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration,
|
92
|
-
|
93
|
-
host: current_uri.host,
|
94
|
-
port: current_uri.port,
|
88
|
+
host: current_host,
|
95
89
|
disposition: content_disposition,
|
96
90
|
content_type: content_type,
|
97
91
|
filename: filename
|
@@ -132,6 +126,16 @@ module ActiveStorage
|
|
132
126
|
end
|
133
127
|
|
134
128
|
private
|
129
|
+
def stream(key)
|
130
|
+
File.open(path_for(key), "rb") do |file|
|
131
|
+
while data = file.read(5.megabytes)
|
132
|
+
yield data
|
133
|
+
end
|
134
|
+
end
|
135
|
+
rescue Errno::ENOENT
|
136
|
+
raise ActiveStorage::FileNotFoundError
|
137
|
+
end
|
138
|
+
|
135
139
|
def folder_for(key)
|
136
140
|
[ key[0..1], key[2..3] ].join("/")
|
137
141
|
end
|
@@ -1,11 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
gem "google-cloud-storage", "~> 1.
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
59
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
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,8 +111,23 @@ module ActiveStorage
|
|
124
111
|
private
|
125
112
|
attr_reader :config
|
126
113
|
|
127
|
-
def file_for(key)
|
128
|
-
bucket.file(key, skip_lookup:
|
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
|
@@ -9,7 +9,7 @@ module ActiveStorage
|
|
9
9
|
class Service::MirrorService < Service
|
10
10
|
attr_reader :primary, :mirrors
|
11
11
|
|
12
|
-
delegate :download, :download_chunk, :exist?, :url,
|
12
|
+
delegate :download, :download_chunk, :exist?, :url, to: :primary
|
13
13
|
|
14
14
|
# Stitch together from named services.
|
15
15
|
def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
gem "aws-sdk-s3", "~> 1.48"
|
4
|
-
|
5
3
|
require "aws-sdk-s3"
|
6
4
|
require "active_support/core_ext/numeric/bytes"
|
7
5
|
|
@@ -20,11 +18,9 @@ module ActiveStorage
|
|
20
18
|
|
21
19
|
def upload(key, io, checksum: nil, **)
|
22
20
|
instrument :upload, key: key, checksum: checksum do
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
raise ActiveStorage::IntegrityError
|
27
|
-
end
|
21
|
+
object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
|
22
|
+
rescue Aws::S3::Errors::BadDigest
|
23
|
+
raise ActiveStorage::IntegrityError
|
28
24
|
end
|
29
25
|
end
|
30
26
|
|
@@ -36,6 +32,8 @@ module ActiveStorage
|
|
36
32
|
else
|
37
33
|
instrument :download, key: key do
|
38
34
|
object_for(key).get.body.string.force_encoding(Encoding::BINARY)
|
35
|
+
rescue Aws::S3::Errors::NoSuchKey
|
36
|
+
raise ActiveStorage::FileNotFoundError
|
39
37
|
end
|
40
38
|
end
|
41
39
|
end
|
@@ -43,6 +41,8 @@ module ActiveStorage
|
|
43
41
|
def download_chunk(key, range)
|
44
42
|
instrument :download_chunk, key: key, range: range do
|
45
43
|
object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY)
|
44
|
+
rescue Aws::S3::Errors::NoSuchKey
|
45
|
+
raise ActiveStorage::FileNotFoundError
|
46
46
|
end
|
47
47
|
end
|
48
48
|
|
@@ -81,8 +81,7 @@ module ActiveStorage
|
|
81
81
|
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
|
82
82
|
instrument :url, key: key do |payload|
|
83
83
|
generated_url = object_for(key).presigned_url :put, expires_in: expires_in.to_i,
|
84
|
-
content_type: content_type, content_length: content_length, content_md5: checksum
|
85
|
-
whitelist_headers: ['content-length']
|
84
|
+
content_type: content_type, content_length: content_length, content_md5: checksum
|
86
85
|
|
87
86
|
payload[:url] = generated_url
|
88
87
|
|
@@ -106,6 +105,8 @@ module ActiveStorage
|
|
106
105
|
chunk_size = 5.megabytes
|
107
106
|
offset = 0
|
108
107
|
|
108
|
+
raise ActiveStorage::FileNotFoundError unless object.exists?
|
109
|
+
|
109
110
|
while offset < object.content_length
|
110
111
|
yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
|
111
112
|
offset += chunk_size
|
@@ -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
|
@@ -100,7 +98,7 @@ module ActiveStorage
|
|
100
98
|
end
|
101
99
|
|
102
100
|
# 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
|
101
|
+
# of seconds specified in +expires_in+. You must also provide the +disposition+ (+:inline+ or +:attachment+),
|
104
102
|
# +filename+, and +content_type+ that you wish the file to be served with on request.
|
105
103
|
def url(key, expires_in:, disposition:, filename:, content_type:)
|
106
104
|
raise NotImplementedError
|
@@ -132,7 +130,8 @@ module ActiveStorage
|
|
132
130
|
end
|
133
131
|
|
134
132
|
def content_disposition_with(type: "inline", filename:)
|
135
|
-
(type.to_s.presence_in(%w( attachment inline )) || "inline")
|
133
|
+
disposition = (type.to_s.presence_in(%w( attachment inline )) || "inline")
|
134
|
+
ActionDispatch::Http::ContentDisposition.format(disposition: disposition, filename: filename.sanitized)
|
136
135
|
end
|
137
136
|
end
|
138
137
|
end
|