activestorage 5.2.4.rc1 → 6.0.0.rc2
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 +131 -60
- data/MIT-LICENSE +1 -1
- data/README.md +9 -6
- 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 +5 -2
- 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 +20 -9
- data/app/models/active_storage/blob.rb +66 -24
- 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 +24 -33
- data/config/routes.rb +13 -12
- data/db/update_migrate/20180723000244_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.rb +9 -0
- data/lib/active_storage.rb +26 -6
- data/lib/active_storage/analyzer.rb +9 -4
- data/lib/active_storage/analyzer/image_analyzer.rb +11 -4
- data/lib/active_storage/analyzer/video_analyzer.rb +3 -5
- 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 +69 -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 +147 -0
- data/lib/active_storage/attached/one.rb +16 -19
- data/lib/active_storage/downloader.rb +43 -0
- data/lib/active_storage/downloading.rb +8 -0
- data/lib/active_storage/engine.rb +43 -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 +2 -2
- data/lib/active_storage/previewer/video_previewer.rb +2 -3
- data/lib/active_storage/reflection.rb +64 -0
- data/lib/active_storage/service.rb +9 -6
- 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 +49 -47
- data/lib/active_storage/service/s3_service.rb +10 -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/tasks/activestorage.rake +7 -0
- metadata +39 -13
- 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
|
@@ -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.
|
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 #tmpdir.
|
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",
|
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
|
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
|
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}.
|
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
|
@@ -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
|
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")
|
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
|
-
|
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
|