activestorage 6.1.7 → 7.1.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -276
- data/MIT-LICENSE +1 -1
- data/README.md +29 -15
- data/app/assets/javascripts/activestorage.esm.js +848 -0
- data/app/assets/javascripts/activestorage.js +263 -376
- data/app/controllers/active_storage/base_controller.rb +0 -9
- data/app/controllers/active_storage/blobs/proxy_controller.rb +16 -4
- data/app/controllers/active_storage/blobs/redirect_controller.rb +6 -4
- data/app/controllers/active_storage/disk_controller.rb +5 -2
- data/app/controllers/active_storage/representations/base_controller.rb +5 -1
- data/app/controllers/active_storage/representations/proxy_controller.rb +8 -3
- data/app/controllers/active_storage/representations/redirect_controller.rb +6 -4
- data/app/controllers/concerns/active_storage/disable_session.rb +12 -0
- data/app/controllers/concerns/active_storage/file_server.rb +4 -1
- data/app/controllers/concerns/active_storage/set_blob.rb +6 -2
- data/app/controllers/concerns/active_storage/set_current.rb +3 -3
- data/app/controllers/concerns/active_storage/streaming.rb +66 -0
- data/app/javascript/activestorage/blob_record.js +4 -1
- data/app/javascript/activestorage/direct_upload.js +3 -2
- data/app/javascript/activestorage/index.js +3 -1
- data/app/javascript/activestorage/ujs.js +1 -1
- data/app/jobs/active_storage/analyze_job.rb +1 -1
- data/app/jobs/active_storage/mirror_job.rb +1 -1
- data/app/jobs/active_storage/purge_job.rb +1 -1
- data/app/jobs/active_storage/transform_job.rb +12 -0
- data/app/models/active_storage/attachment.rb +111 -4
- data/app/models/active_storage/blob/analyzable.rb +4 -3
- data/app/models/active_storage/blob/identifiable.rb +1 -0
- data/app/models/active_storage/blob/representable.rb +14 -8
- data/app/models/active_storage/blob.rb +93 -57
- data/app/models/active_storage/current.rb +2 -2
- data/app/models/active_storage/filename.rb +2 -0
- data/app/models/active_storage/named_variant.rb +21 -0
- data/app/models/active_storage/preview.rb +11 -7
- data/app/models/active_storage/record.rb +1 -1
- data/app/models/active_storage/variant.rb +10 -12
- data/app/models/active_storage/variant_record.rb +2 -0
- data/app/models/active_storage/variant_with_record.rb +28 -12
- data/app/models/active_storage/variation.rb +7 -5
- data/config/routes.rb +12 -10
- data/db/migrate/20170806125915_create_active_storage_tables.rb +15 -6
- data/db/update_migrate/20211119233751_remove_not_null_on_active_storage_blobs_checksum.rb +7 -0
- data/lib/active_storage/analyzer/audio_analyzer.rb +77 -0
- data/lib/active_storage/analyzer/image_analyzer/image_magick.rb +41 -0
- data/lib/active_storage/analyzer/image_analyzer/vips.rb +51 -0
- data/lib/active_storage/analyzer/image_analyzer.rb +4 -30
- data/lib/active_storage/analyzer/video_analyzer.rb +41 -17
- data/lib/active_storage/analyzer.rb +10 -4
- data/lib/active_storage/attached/changes/create_many.rb +14 -5
- data/lib/active_storage/attached/changes/create_one.rb +46 -4
- data/lib/active_storage/attached/changes/create_one_of_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_many.rb +1 -1
- data/lib/active_storage/attached/changes/delete_one.rb +1 -1
- data/lib/active_storage/attached/changes/detach_many.rb +18 -0
- data/lib/active_storage/attached/changes/detach_one.rb +24 -0
- data/lib/active_storage/attached/changes/purge_many.rb +27 -0
- data/lib/active_storage/attached/changes/purge_one.rb +27 -0
- data/lib/active_storage/attached/changes.rb +7 -1
- data/lib/active_storage/attached/many.rb +32 -19
- data/lib/active_storage/attached/model.rb +80 -29
- data/lib/active_storage/attached/one.rb +37 -31
- data/lib/active_storage/attached.rb +2 -0
- data/lib/active_storage/deprecator.rb +7 -0
- data/lib/active_storage/downloader.rb +4 -4
- data/lib/active_storage/engine.rb +55 -7
- data/lib/active_storage/fixture_set.rb +75 -0
- data/lib/active_storage/gem_version.rb +3 -3
- data/lib/active_storage/log_subscriber.rb +12 -0
- data/lib/active_storage/previewer.rb +12 -5
- data/lib/active_storage/reflection.rb +12 -2
- data/lib/active_storage/service/azure_storage_service.rb +30 -6
- data/lib/active_storage/service/configurator.rb +1 -1
- data/lib/active_storage/service/disk_service.rb +26 -19
- data/lib/active_storage/service/gcs_service.rb +100 -11
- data/lib/active_storage/service/mirror_service.rb +12 -7
- data/lib/active_storage/service/registry.rb +1 -1
- data/lib/active_storage/service/s3_service.rb +39 -15
- data/lib/active_storage/service.rb +17 -7
- data/lib/active_storage/transformers/image_processing_transformer.rb +1 -1
- data/lib/active_storage/transformers/transformer.rb +3 -1
- data/lib/active_storage/version.rb +1 -1
- data/lib/active_storage.rb +22 -2
- metadata +30 -30
- data/app/controllers/concerns/active_storage/set_headers.rb +0 -12
@@ -7,13 +7,4 @@ class ActiveStorage::BaseController < ActionController::Base
|
|
7
7
|
protect_from_forgery with: :exception
|
8
8
|
|
9
9
|
self.etag_with_template_digest = false
|
10
|
-
|
11
|
-
private
|
12
|
-
def stream(blob)
|
13
|
-
blob.download do |chunk|
|
14
|
-
response.stream.write chunk
|
15
|
-
end
|
16
|
-
ensure
|
17
|
-
response.stream.close
|
18
|
-
end
|
19
10
|
end
|
@@ -1,14 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Proxy files through application. This avoids having a redirect and makes files easier to cache.
|
4
|
+
#
|
5
|
+
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
|
+
# generated URLs are hard to guess, but permanent by design. If your files
|
7
|
+
# require a higher level of protection consider implementing
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
4
9
|
class ActiveStorage::Blobs::ProxyController < ActiveStorage::BaseController
|
5
10
|
include ActiveStorage::SetBlob
|
6
|
-
include ActiveStorage::
|
11
|
+
include ActiveStorage::Streaming
|
12
|
+
include ActiveStorage::DisableSession
|
7
13
|
|
8
14
|
def show
|
9
|
-
|
10
|
-
|
11
|
-
|
15
|
+
if request.headers["Range"].present?
|
16
|
+
send_blob_byte_range_data @blob, request.headers["Range"]
|
17
|
+
else
|
18
|
+
http_cache_forever public: true do
|
19
|
+
response.headers["Accept-Ranges"] = "bytes"
|
20
|
+
response.headers["Content-Length"] = @blob.byte_size.to_s
|
21
|
+
|
22
|
+
send_blob_stream @blob, disposition: params[:disposition]
|
23
|
+
end
|
12
24
|
end
|
13
25
|
end
|
14
26
|
end
|
@@ -1,14 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Take a signed permanent reference for a blob and turn it into an expiring service URL for download.
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
4
|
+
#
|
5
|
+
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
|
+
# generated URLs are hard to guess, but permanent by design. If your files
|
7
|
+
# require a higher level of protection consider implementing
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
7
9
|
class ActiveStorage::Blobs::RedirectController < ActiveStorage::BaseController
|
8
10
|
include ActiveStorage::SetBlob
|
9
11
|
|
10
12
|
def show
|
11
13
|
expires_in ActiveStorage.service_urls_expire_in
|
12
|
-
redirect_to @blob.url(disposition: params[:disposition])
|
14
|
+
redirect_to @blob.url(disposition: params[:disposition]), allow_other_host: true
|
13
15
|
end
|
14
16
|
end
|
@@ -23,6 +23,7 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
23
23
|
if token = decode_verified_token
|
24
24
|
if acceptable_content?(token)
|
25
25
|
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
|
26
|
+
head :no_content
|
26
27
|
else
|
27
28
|
head :unprocessable_entity
|
28
29
|
end
|
@@ -41,11 +42,13 @@ class ActiveStorage::DiskController < ActiveStorage::BaseController
|
|
41
42
|
end
|
42
43
|
|
43
44
|
def decode_verified_key
|
44
|
-
ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
|
45
|
+
key = ActiveStorage.verifier.verified(params[:encoded_key], purpose: :blob_key)
|
46
|
+
key&.deep_symbolize_keys
|
45
47
|
end
|
46
48
|
|
47
49
|
def decode_verified_token
|
48
|
-
ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
|
50
|
+
token = ActiveStorage.verifier.verified(params[:encoded_token], purpose: :blob_token)
|
51
|
+
token&.deep_symbolize_keys
|
49
52
|
end
|
50
53
|
|
51
54
|
def acceptable_content?(token)
|
@@ -1,11 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController
|
3
|
+
class ActiveStorage::Representations::BaseController < ActiveStorage::BaseController # :nodoc:
|
4
4
|
include ActiveStorage::SetBlob
|
5
5
|
|
6
6
|
before_action :set_representation
|
7
7
|
|
8
8
|
private
|
9
|
+
def blob_scope
|
10
|
+
ActiveStorage::Blob.scope_for_strict_loading
|
11
|
+
end
|
12
|
+
|
9
13
|
def set_representation
|
10
14
|
@representation = @blob.representation(params[:variation_key]).processed
|
11
15
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
@@ -1,13 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Proxy files through application. This avoids having a redirect and makes files easier to cache.
|
4
|
+
#
|
5
|
+
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
|
+
# generated URLs are hard to guess, but permanent by design. If your files
|
7
|
+
# require a higher level of protection consider implementing
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
4
9
|
class ActiveStorage::Representations::ProxyController < ActiveStorage::Representations::BaseController
|
5
|
-
include ActiveStorage::
|
10
|
+
include ActiveStorage::Streaming
|
11
|
+
include ActiveStorage::DisableSession
|
6
12
|
|
7
13
|
def show
|
8
14
|
http_cache_forever public: true do
|
9
|
-
|
10
|
-
stream @representation
|
15
|
+
send_blob_stream @representation.image, disposition: params[:disposition]
|
11
16
|
end
|
12
17
|
end
|
13
18
|
end
|
@@ -1,12 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Take a signed permanent reference for a blob representation and turn it into an expiring service URL for download.
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
4
|
+
#
|
5
|
+
# WARNING: All Active Storage controllers are publicly accessible by default. The
|
6
|
+
# generated URLs are hard to guess, but permanent by design. If your files
|
7
|
+
# require a higher level of protection consider implementing
|
8
|
+
# {Authenticated Controllers}[https://guides.rubyonrails.org/active_storage_overview.html#authenticated-controllers].
|
7
9
|
class ActiveStorage::Representations::RedirectController < ActiveStorage::Representations::BaseController
|
8
10
|
def show
|
9
11
|
expires_in ActiveStorage.service_urls_expire_in
|
10
|
-
redirect_to @representation.url(disposition: params[:disposition])
|
12
|
+
redirect_to @representation.url(disposition: params[:disposition]), allow_other_host: true
|
11
13
|
end
|
12
14
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This concern disables the session in order to allow caching by default in some CDNs as CloudFlare.
|
4
|
+
module ActiveStorage::DisableSession
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_action do
|
9
|
+
request.session_options[:skip] = true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "active_support/core_ext/hash/except"
|
4
|
+
|
3
5
|
module ActiveStorage::FileServer # :nodoc:
|
4
6
|
private
|
5
7
|
def serve_file(path, content_type:, disposition:)
|
6
|
-
Rack::
|
8
|
+
::Rack::Files.new(nil).serving(request, path).tap do |(status, headers, body)|
|
7
9
|
self.status = status
|
8
10
|
self.response_body = body
|
9
11
|
|
@@ -11,6 +13,7 @@ module ActiveStorage::FileServer # :nodoc:
|
|
11
13
|
response.headers[name] = value
|
12
14
|
end
|
13
15
|
|
16
|
+
response.headers.except!("X-Cascade", "x-cascade") if status == 416
|
14
17
|
response.headers["Content-Type"] = content_type || DEFAULT_SEND_FILE_TYPE
|
15
18
|
response.headers["Content-Disposition"] = disposition || DEFAULT_SEND_FILE_DISPOSITION
|
16
19
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module ActiveStorage::SetBlob
|
3
|
+
module ActiveStorage::SetBlob # :nodoc:
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
6
|
included do
|
@@ -9,8 +9,12 @@ module ActiveStorage::SetBlob #:nodoc:
|
|
9
9
|
|
10
10
|
private
|
11
11
|
def set_blob
|
12
|
-
@blob =
|
12
|
+
@blob = blob_scope.find_signed!(params[:signed_blob_id] || params[:signed_id])
|
13
13
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
14
14
|
head :not_found
|
15
15
|
end
|
16
|
+
|
17
|
+
def blob_scope
|
18
|
+
ActiveStorage::Blob
|
19
|
+
end
|
16
20
|
end
|
@@ -1,15 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Sets the <tt>ActiveStorage::Current.
|
3
|
+
# Sets the <tt>ActiveStorage::Current.url_options</tt> attribute, which the disk service uses to generate URLs.
|
4
4
|
# Include this concern in custom controllers that call ActiveStorage::Blob#url,
|
5
5
|
# ActiveStorage::Variant#url, or ActiveStorage::Preview#url so the disk service can
|
6
|
-
# generate URLs using the same host, protocol, and
|
6
|
+
# generate URLs using the same host, protocol, and port as the current request.
|
7
7
|
module ActiveStorage::SetCurrent
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
10
|
included do
|
11
11
|
before_action do
|
12
|
-
ActiveStorage::Current.
|
12
|
+
ActiveStorage::Current.url_options = { protocol: request.protocol, host: request.host, port: request.port }
|
13
13
|
end
|
14
14
|
end
|
15
15
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module ActiveStorage::Streaming
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
DEFAULT_BLOB_STREAMING_DISPOSITION = "inline"
|
8
|
+
|
9
|
+
include ActionController::DataStreaming
|
10
|
+
include ActionController::Live
|
11
|
+
|
12
|
+
private
|
13
|
+
# Stream the blob in byte ranges specified through the header
|
14
|
+
def send_blob_byte_range_data(blob, range_header, disposition: nil)
|
15
|
+
ranges = Rack::Utils.get_byte_ranges(range_header, blob.byte_size)
|
16
|
+
|
17
|
+
return head(:range_not_satisfiable) if ranges.blank? || ranges.all?(&:blank?)
|
18
|
+
|
19
|
+
if ranges.length == 1
|
20
|
+
range = ranges.first
|
21
|
+
content_type = blob.content_type_for_serving
|
22
|
+
data = blob.download_chunk(range)
|
23
|
+
|
24
|
+
response.headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{blob.byte_size}"
|
25
|
+
else
|
26
|
+
boundary = SecureRandom.hex
|
27
|
+
content_type = "multipart/byteranges; boundary=#{boundary}"
|
28
|
+
data = +""
|
29
|
+
|
30
|
+
ranges.compact.each do |range|
|
31
|
+
chunk = blob.download_chunk(range)
|
32
|
+
|
33
|
+
data << "\r\n--#{boundary}\r\n"
|
34
|
+
data << "Content-Type: #{blob.content_type_for_serving}\r\n"
|
35
|
+
data << "Content-Range: bytes #{range.begin}-#{range.end}/#{blob.byte_size}\r\n\r\n"
|
36
|
+
data << chunk
|
37
|
+
end
|
38
|
+
|
39
|
+
data << "\r\n--#{boundary}--\r\n"
|
40
|
+
end
|
41
|
+
|
42
|
+
response.headers["Accept-Ranges"] = "bytes"
|
43
|
+
response.headers["Content-Length"] = data.length.to_s
|
44
|
+
|
45
|
+
send_data(
|
46
|
+
data,
|
47
|
+
disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
|
48
|
+
filename: blob.filename.sanitized,
|
49
|
+
status: :partial_content,
|
50
|
+
type: content_type
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Stream the blob from storage directly to the response. The disposition can be controlled by setting +disposition+.
|
55
|
+
# The content type and filename is set directly from the +blob+.
|
56
|
+
def send_blob_stream(blob, disposition: nil) # :doc:
|
57
|
+
send_stream(
|
58
|
+
filename: blob.filename.sanitized,
|
59
|
+
disposition: blob.forced_disposition_for_serving || disposition || DEFAULT_BLOB_STREAMING_DISPOSITION,
|
60
|
+
type: blob.content_type_for_serving) do |stream|
|
61
|
+
blob.download do |chunk|
|
62
|
+
stream.write chunk
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import { getMetaValue } from "./helpers"
|
2
2
|
|
3
3
|
export class BlobRecord {
|
4
|
-
constructor(file, checksum, url) {
|
4
|
+
constructor(file, checksum, url, customHeaders = {}) {
|
5
5
|
this.file = file
|
6
6
|
|
7
7
|
this.attributes = {
|
@@ -17,6 +17,9 @@ export class BlobRecord {
|
|
17
17
|
this.xhr.setRequestHeader("Content-Type", "application/json")
|
18
18
|
this.xhr.setRequestHeader("Accept", "application/json")
|
19
19
|
this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
|
20
|
+
Object.keys(customHeaders).forEach((headerKey) => {
|
21
|
+
this.xhr.setRequestHeader(headerKey, customHeaders[headerKey])
|
22
|
+
})
|
20
23
|
|
21
24
|
const csrfToken = getMetaValue("csrf-token")
|
22
25
|
if (csrfToken != undefined) {
|
@@ -5,11 +5,12 @@ import { BlobUpload } from "./blob_upload"
|
|
5
5
|
let id = 0
|
6
6
|
|
7
7
|
export class DirectUpload {
|
8
|
-
constructor(file, url, delegate) {
|
8
|
+
constructor(file, url, delegate, customHeaders = {}) {
|
9
9
|
this.id = ++id
|
10
10
|
this.file = file
|
11
11
|
this.url = url
|
12
12
|
this.delegate = delegate
|
13
|
+
this.customHeaders = customHeaders
|
13
14
|
}
|
14
15
|
|
15
16
|
create(callback) {
|
@@ -19,7 +20,7 @@ export class DirectUpload {
|
|
19
20
|
return
|
20
21
|
}
|
21
22
|
|
22
|
-
const blob = new BlobRecord(this.file, checksum, this.url)
|
23
|
+
const blob = new BlobRecord(this.file, checksum, this.url, this.customHeaders)
|
23
24
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
|
24
25
|
|
25
26
|
blob.create(error => {
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import { start } from "./ujs"
|
2
2
|
import { DirectUpload } from "./direct_upload"
|
3
|
-
|
3
|
+
import { DirectUploadController } from "./direct_upload_controller"
|
4
|
+
import { DirectUploadsController } from "./direct_uploads_controller"
|
5
|
+
export { start, DirectUpload, DirectUploadController, DirectUploadsController }
|
4
6
|
|
5
7
|
function autostart() {
|
6
8
|
if (window.ActiveStorage) {
|
@@ -9,7 +9,7 @@ export function start() {
|
|
9
9
|
if (!started) {
|
10
10
|
started = true
|
11
11
|
document.addEventListener("click", didClick, true)
|
12
|
-
document.addEventListener("submit", didSubmitForm)
|
12
|
+
document.addEventListener("submit", didSubmitForm, true)
|
13
13
|
document.addEventListener("ajax:before", didSubmitRemoteElement)
|
14
14
|
}
|
15
15
|
}
|
@@ -5,7 +5,7 @@ class ActiveStorage::AnalyzeJob < ActiveStorage::BaseJob
|
|
5
5
|
queue_as { ActiveStorage.queues[:analysis] }
|
6
6
|
|
7
7
|
discard_on ActiveRecord::RecordNotFound
|
8
|
-
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :
|
8
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
|
9
9
|
|
10
10
|
def perform(blob)
|
11
11
|
blob.analyze
|
@@ -7,7 +7,7 @@ class ActiveStorage::MirrorJob < ActiveStorage::BaseJob
|
|
7
7
|
queue_as { ActiveStorage.queues[:mirror] }
|
8
8
|
|
9
9
|
discard_on ActiveStorage::FileNotFoundError
|
10
|
-
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :
|
10
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
|
11
11
|
|
12
12
|
def perform(key, checksum:)
|
13
13
|
ActiveStorage::Blob.service.try(:mirror, key, checksum: checksum)
|
@@ -5,7 +5,7 @@ class ActiveStorage::PurgeJob < ActiveStorage::BaseJob
|
|
5
5
|
queue_as { ActiveStorage.queues[:purge] }
|
6
6
|
|
7
7
|
discard_on ActiveRecord::RecordNotFound
|
8
|
-
retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :
|
8
|
+
retry_on ActiveRecord::Deadlocked, attempts: 10, wait: :polynomially_longer
|
9
9
|
|
10
10
|
def perform(blob)
|
11
11
|
blob.purge
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class ActiveStorage::TransformJob < ActiveStorage::BaseJob
|
4
|
+
queue_as { ActiveStorage.queues[:transform] }
|
5
|
+
|
6
|
+
discard_on ActiveRecord::RecordNotFound
|
7
|
+
retry_on ActiveStorage::IntegrityError, attempts: 10, wait: :polynomially_longer
|
8
|
+
|
9
|
+
def perform(blob, transformations)
|
10
|
+
blob.variant(transformations).processed
|
11
|
+
end
|
12
|
+
end
|
@@ -2,28 +2,55 @@
|
|
2
2
|
|
3
3
|
require "active_support/core_ext/module/delegation"
|
4
4
|
|
5
|
+
# = Active Storage \Attachment
|
6
|
+
#
|
5
7
|
# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
|
6
8
|
# but it is possible to associate many different records with the same blob. A foreign-key constraint
|
7
9
|
# on the attachments table prevents blobs from being purged if they’re still attached to any records.
|
8
10
|
#
|
9
|
-
# Attachments also have access to all methods from
|
11
|
+
# Attachments also have access to all methods from ActiveStorage::Blob.
|
12
|
+
#
|
13
|
+
# If you wish to preload attachments or blobs, you can use these scopes:
|
14
|
+
#
|
15
|
+
# # preloads attachments, their corresponding blobs, and variant records (if using `ActiveStorage.track_variants`)
|
16
|
+
# User.all.with_attached_avatars
|
17
|
+
#
|
18
|
+
# # preloads blobs and variant records (if using `ActiveStorage.track_variants`)
|
19
|
+
# User.first.avatars.with_all_variant_records
|
10
20
|
class ActiveStorage::Attachment < ActiveStorage::Record
|
11
21
|
self.table_name = "active_storage_attachments"
|
12
22
|
|
23
|
+
##
|
24
|
+
# :method:
|
25
|
+
#
|
26
|
+
# Returns the associated record.
|
13
27
|
belongs_to :record, polymorphic: true, touch: true
|
28
|
+
|
29
|
+
##
|
30
|
+
# :method:
|
31
|
+
#
|
32
|
+
# Returns the associated ActiveStorage::Blob.
|
14
33
|
belongs_to :blob, class_name: "ActiveStorage::Blob", autosave: true
|
15
34
|
|
16
35
|
delegate_missing_to :blob
|
17
36
|
delegate :signed_id, to: :blob
|
18
37
|
|
19
|
-
after_create_commit :mirror_blob_later, :analyze_blob_later
|
38
|
+
after_create_commit :mirror_blob_later, :analyze_blob_later, :transform_variants_later
|
20
39
|
after_destroy_commit :purge_dependent_blob_later
|
21
40
|
|
41
|
+
##
|
42
|
+
# :singleton-method:
|
43
|
+
#
|
44
|
+
# Eager load all variant records on an attachment at once.
|
45
|
+
#
|
46
|
+
# User.first.avatars.with_all_variant_records
|
47
|
+
scope :with_all_variant_records, -> { includes(blob: { variant_records: { image_attachment: :blob } }) }
|
48
|
+
|
22
49
|
# Synchronously deletes the attachment and {purges the blob}[rdoc-ref:ActiveStorage::Blob#purge].
|
23
50
|
def purge
|
24
51
|
transaction do
|
25
52
|
delete
|
26
|
-
record&.
|
53
|
+
record.touch if record&.persisted?
|
27
54
|
end
|
28
55
|
blob&.purge
|
29
56
|
end
|
@@ -32,11 +59,68 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
32
59
|
def purge_later
|
33
60
|
transaction do
|
34
61
|
delete
|
35
|
-
record&.
|
62
|
+
record.touch if record&.persisted?
|
36
63
|
end
|
37
64
|
blob&.purge_later
|
38
65
|
end
|
39
66
|
|
67
|
+
# Returns an ActiveStorage::Variant or ActiveStorage::VariantWithRecord
|
68
|
+
# instance for the attachment with the set of +transformations+ provided.
|
69
|
+
# Example:
|
70
|
+
#
|
71
|
+
# avatar.variant(resize_to_limit: [100, 100]).processed.url
|
72
|
+
#
|
73
|
+
# or if you are using pre-defined variants:
|
74
|
+
#
|
75
|
+
# avatar.variant(:thumb).processed.url
|
76
|
+
#
|
77
|
+
# See ActiveStorage::Blob::Representable#variant for more information.
|
78
|
+
#
|
79
|
+
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
80
|
+
# unknown pre-defined variant of the attachment.
|
81
|
+
def variant(transformations)
|
82
|
+
transformations = transformations_by_name(transformations)
|
83
|
+
blob.variant(transformations)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Returns an ActiveStorage::Preview instance for the attachment with the set
|
87
|
+
# of +transformations+ provided.
|
88
|
+
# Example:
|
89
|
+
#
|
90
|
+
# video.preview(resize_to_limit: [100, 100]).processed.url
|
91
|
+
#
|
92
|
+
# or if you are using pre-defined variants:
|
93
|
+
#
|
94
|
+
# video.preview(:thumb).processed.url
|
95
|
+
#
|
96
|
+
# See ActiveStorage::Blob::Representable#preview for more information.
|
97
|
+
#
|
98
|
+
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
99
|
+
# unknown pre-defined variant of the attachment.
|
100
|
+
def preview(transformations)
|
101
|
+
transformations = transformations_by_name(transformations)
|
102
|
+
blob.preview(transformations)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns an ActiveStorage::Preview or an ActiveStorage::Variant for the
|
106
|
+
# attachment with set of +transformations+ provided.
|
107
|
+
# Example:
|
108
|
+
#
|
109
|
+
# avatar.representation(resize_to_limit: [100, 100]).processed.url
|
110
|
+
#
|
111
|
+
# or if you are using pre-defined variants:
|
112
|
+
#
|
113
|
+
# avatar.representation(:thumb).processed.url
|
114
|
+
#
|
115
|
+
# See ActiveStorage::Blob::Representable#representation for more information.
|
116
|
+
#
|
117
|
+
# Raises an +ArgumentError+ if +transformations+ is a +Symbol+ which is an
|
118
|
+
# unknown pre-defined variant of the attachment.
|
119
|
+
def representation(transformations)
|
120
|
+
transformations = transformations_by_name(transformations)
|
121
|
+
blob.representation(transformations)
|
122
|
+
end
|
123
|
+
|
40
124
|
private
|
41
125
|
def analyze_blob_later
|
42
126
|
blob.analyze_later unless blob.analyzed?
|
@@ -46,6 +130,12 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
46
130
|
blob.mirror_later
|
47
131
|
end
|
48
132
|
|
133
|
+
def transform_variants_later
|
134
|
+
named_variants.each do |_name, named_variant|
|
135
|
+
blob.preprocessed(named_variant.transformations) if named_variant.preprocessed?(record)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
49
139
|
def purge_dependent_blob_later
|
50
140
|
blob&.purge_later if dependent == :purge_later
|
51
141
|
end
|
@@ -53,6 +143,23 @@ class ActiveStorage::Attachment < ActiveStorage::Record
|
|
53
143
|
def dependent
|
54
144
|
record.attachment_reflections[name]&.options&.fetch(:dependent, nil)
|
55
145
|
end
|
146
|
+
|
147
|
+
def named_variants
|
148
|
+
record.attachment_reflections[name]&.named_variants
|
149
|
+
end
|
150
|
+
|
151
|
+
def transformations_by_name(transformations)
|
152
|
+
case transformations
|
153
|
+
when Symbol
|
154
|
+
variant_name = transformations
|
155
|
+
named_variants.fetch(variant_name) do
|
156
|
+
record_model_name = record.to_model.model_name.name
|
157
|
+
raise ArgumentError, "Cannot find variant :#{variant_name} for #{record_model_name}##{name}"
|
158
|
+
end.transformations
|
159
|
+
else
|
160
|
+
transformations
|
161
|
+
end
|
162
|
+
end
|
56
163
|
end
|
57
164
|
|
58
165
|
ActiveSupport.run_load_hooks :active_storage_attachment, ActiveStorage::Attachment
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "active_storage/analyzer/null_analyzer"
|
4
4
|
|
5
|
+
# = Active Storage \Blob \Analyzable
|
5
6
|
module ActiveStorage::Blob::Analyzable
|
6
7
|
# Extracts and stores metadata from the file associated with this blob using a relevant analyzer. Active Storage comes
|
7
8
|
# with built-in analyzers for images and videos. See ActiveStorage::Analyzer::ImageAnalyzer and
|
@@ -12,7 +13,7 @@ module ActiveStorage::Blob::Analyzable
|
|
12
13
|
# first analyzer for which +accept?+ returns true when given the blob. If no registered analyzer accepts the blob, no
|
13
14
|
# metadata is extracted from it.
|
14
15
|
#
|
15
|
-
# In a Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
|
16
|
+
# In a \Rails application, add or remove analyzers by manipulating +Rails.application.config.active_storage.analyzers+
|
16
17
|
# in an initializer:
|
17
18
|
#
|
18
19
|
# # Add a custom analyzer for Microsoft Office documents:
|
@@ -21,9 +22,9 @@ module ActiveStorage::Blob::Analyzable
|
|
21
22
|
# # Remove the built-in video analyzer:
|
22
23
|
# Rails.application.config.active_storage.analyzers.delete ActiveStorage::Analyzer::VideoAnalyzer
|
23
24
|
#
|
24
|
-
# Outside of a Rails application, manipulate +ActiveStorage.analyzers+ instead.
|
25
|
+
# Outside of a \Rails application, manipulate +ActiveStorage.analyzers+ instead.
|
25
26
|
#
|
26
|
-
# You won't ordinarily need to call this method from a Rails application. New blobs are automatically and asynchronously
|
27
|
+
# You won't ordinarily need to call this method from a \Rails application. New blobs are automatically and asynchronously
|
27
28
|
# analyzed via #analyze_later when they're attached for the first time.
|
28
29
|
def analyze
|
29
30
|
update! metadata: metadata.merge(extract_metadata_via_analyzer)
|