activestorage 5.2.4.rc1 → 6.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of activestorage might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +103 -66
- 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.rb +63 -22
- data/app/models/active_storage/blob/representable.rb +5 -5
- 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 -32
- 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.rb +13 -2
- data/lib/active_storage/analyzer.rb +9 -4
- data/lib/active_storage/analyzer/video_analyzer.rb +2 -4
- data/lib/active_storage/attached.rb +7 -22
- data/lib/active_storage/attached/changes.rb +16 -0
- 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/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/downloader.rb +44 -0
- data/lib/active_storage/downloading.rb +8 -0
- data/lib/active_storage/engine.rb +35 -6
- data/lib/active_storage/errors.rb +22 -3
- data/lib/active_storage/gem_version.rb +4 -4
- data/lib/active_storage/previewer.rb +21 -11
- data/lib/active_storage/previewer/poppler_pdf_previewer.rb +1 -1
- data/lib/active_storage/previewer/video_previewer.rb +2 -3
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service.rb +5 -6
- data/lib/active_storage/service/azure_storage_service.rb +28 -12
- 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 +9 -5
- 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/tasks/activestorage.rake +7 -0
- metadata +24 -12
- data/app/models/active_storage/filename/parameters.rb +0 -36
- data/lib/active_storage/attached/macros.rb +0 -110
@@ -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:
|
@@ -18,11 +18,9 @@ module ActiveStorage
|
|
18
18
|
|
19
19
|
def upload(key, io, checksum: nil, **)
|
20
20
|
instrument :upload, key: key, checksum: checksum do
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
raise ActiveStorage::IntegrityError
|
25
|
-
end
|
21
|
+
object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
|
22
|
+
rescue Aws::S3::Errors::BadDigest
|
23
|
+
raise ActiveStorage::IntegrityError
|
26
24
|
end
|
27
25
|
end
|
28
26
|
|
@@ -34,6 +32,8 @@ module ActiveStorage
|
|
34
32
|
else
|
35
33
|
instrument :download, key: key do
|
36
34
|
object_for(key).get.body.string.force_encoding(Encoding::BINARY)
|
35
|
+
rescue Aws::S3::Errors::NoSuchKey
|
36
|
+
raise ActiveStorage::FileNotFoundError
|
37
37
|
end
|
38
38
|
end
|
39
39
|
end
|
@@ -41,6 +41,8 @@ module ActiveStorage
|
|
41
41
|
def download_chunk(key, range)
|
42
42
|
instrument :download_chunk, key: key, range: range do
|
43
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
|
44
46
|
end
|
45
47
|
end
|
46
48
|
|
@@ -103,6 +105,8 @@ module ActiveStorage
|
|
103
105
|
chunk_size = 5.megabytes
|
104
106
|
offset = 0
|
105
107
|
|
108
|
+
raise ActiveStorage::FileNotFoundError unless object.exists?
|
109
|
+
|
106
110
|
while offset < object.content_length
|
107
111
|
yield object.get(range: "bytes=#{offset}-#{offset + chunk_size - 1}").body.read.force_encoding(Encoding::BINARY)
|
108
112
|
offset += chunk_size
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "image_processing"
|
4
|
+
|
5
|
+
module ActiveStorage
|
6
|
+
module Transformers
|
7
|
+
class ImageProcessingTransformer < Transformer
|
8
|
+
private
|
9
|
+
def process(file, format:)
|
10
|
+
processor.
|
11
|
+
source(file).
|
12
|
+
loader(page: 0).
|
13
|
+
convert(format).
|
14
|
+
apply(operations).
|
15
|
+
call
|
16
|
+
end
|
17
|
+
|
18
|
+
def processor
|
19
|
+
ImageProcessing.const_get(ActiveStorage.variant_processor.to_s.camelize)
|
20
|
+
end
|
21
|
+
|
22
|
+
def operations
|
23
|
+
transformations.each_with_object([]) do |(name, argument), list|
|
24
|
+
if name.to_s == "combine_options"
|
25
|
+
ActiveSupport::Deprecation.warn <<~WARNING
|
26
|
+
Active Storage's ImageProcessing transformer doesn't support :combine_options,
|
27
|
+
as it always generates a single ImageMagick command. Passing :combine_options will
|
28
|
+
not be supported in Rails 6.1.
|
29
|
+
WARNING
|
30
|
+
|
31
|
+
list.concat argument.keep_if { |key, value| value.present? }.to_a
|
32
|
+
elsif argument.present?
|
33
|
+
list << [ name, argument ]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "mini_magick"
|
4
|
+
|
5
|
+
module ActiveStorage
|
6
|
+
module Transformers
|
7
|
+
class MiniMagickTransformer < Transformer
|
8
|
+
private
|
9
|
+
def process(file, format:)
|
10
|
+
image = MiniMagick::Image.new(file.path, file)
|
11
|
+
|
12
|
+
transformations.each do |name, argument_or_subtransformations|
|
13
|
+
image.mogrify do |command|
|
14
|
+
if name.to_s == "combine_options"
|
15
|
+
argument_or_subtransformations.each do |subtransformation_name, subtransformation_argument|
|
16
|
+
pass_transform_argument(command, subtransformation_name, subtransformation_argument)
|
17
|
+
end
|
18
|
+
else
|
19
|
+
pass_transform_argument(command, name, argument_or_subtransformations)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
image.format(format) if format
|
25
|
+
|
26
|
+
image.tempfile.tap(&:open)
|
27
|
+
end
|
28
|
+
|
29
|
+
def pass_transform_argument(command, method, argument)
|
30
|
+
if argument == true
|
31
|
+
command.public_send(method)
|
32
|
+
elsif argument.present?
|
33
|
+
command.public_send(method, argument)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveStorage
|
4
|
+
module Transformers
|
5
|
+
# A Transformer applies a set of transformations to an image.
|
6
|
+
#
|
7
|
+
# The following concrete subclasses are included in Active Storage:
|
8
|
+
#
|
9
|
+
# * ActiveStorage::Transformers::ImageProcessingTransformer:
|
10
|
+
# backed by ImageProcessing, a common interface for MiniMagick and ruby-vips
|
11
|
+
#
|
12
|
+
# * ActiveStorage::Transformers::MiniMagickTransformer:
|
13
|
+
# backed by MiniMagick, a wrapper around the ImageMagick CLI
|
14
|
+
class Transformer
|
15
|
+
attr_reader :transformations
|
16
|
+
|
17
|
+
def initialize(transformations)
|
18
|
+
@transformations = transformations
|
19
|
+
end
|
20
|
+
|
21
|
+
# Applies the transformations to the source image in +file+, producing a target image in the
|
22
|
+
# specified +format+. Yields an open Tempfile containing the target image. Closes and unlinks
|
23
|
+
# the output tempfile after yielding to the given block. Returns the result of the block.
|
24
|
+
def transform(file, format:)
|
25
|
+
output = process(file, format: format)
|
26
|
+
|
27
|
+
begin
|
28
|
+
yield output
|
29
|
+
ensure
|
30
|
+
output.close!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
# Returns an open Tempfile containing a transformed image in the given +format+.
|
36
|
+
# All subclasses implement this method.
|
37
|
+
def process(file, format:) #:doc:
|
38
|
+
raise NotImplementedError
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|