aboutsummaryrefslogtreecommitdiffstats
path: root/activestorage/lib/active_storage
diff options
context:
space:
mode:
Diffstat (limited to 'activestorage/lib/active_storage')
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb2
-rw-r--r--activestorage/lib/active_storage/attached/macros.rb18
-rw-r--r--activestorage/lib/active_storage/attached/many.rb18
-rw-r--r--activestorage/lib/active_storage/attached/one.rb27
-rw-r--r--activestorage/lib/active_storage/downloading.rb1
-rw-r--r--activestorage/lib/active_storage/engine.rb28
-rw-r--r--activestorage/lib/active_storage/previewer/mupdf_previewer.rb36
-rw-r--r--activestorage/lib/active_storage/previewer/pdf_previewer.rb26
-rw-r--r--activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb35
-rw-r--r--activestorage/lib/active_storage/service.rb5
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb29
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb31
-rw-r--r--activestorage/lib/active_storage/service/gcs_service.rb23
-rw-r--r--activestorage/lib/active_storage/service/mirror_service.rb2
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb12
15 files changed, 203 insertions, 90 deletions
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
index 656e362187..e31bdb0edb 100644
--- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/hash/compact"
-
module ActiveStorage
# Extracts the following from a video blob:
#
diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb
index c51efa9d6b..819f00cc06 100644
--- a/activestorage/lib/active_storage/attached/macros.rb
+++ b/activestorage/lib/active_storage/attached/macros.rb
@@ -38,13 +38,15 @@ module ActiveStorage
end
CODE
- has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record
+ has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false
has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob
scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) }
if dependent == :purge_later
after_destroy_commit { public_send(name).purge_later }
+ else
+ before_destroy { public_send(name).detach }
end
end
@@ -83,13 +85,25 @@ module ActiveStorage
end
CODE
- has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record
+ has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: false do
+ def purge
+ each(&:purge)
+ reset
+ end
+
+ def purge_later
+ each(&:purge_later)
+ reset
+ end
+ end
has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob
scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) }
if dependent == :purge_later
after_destroy_commit { public_send(name).purge_later }
+ else
+ before_destroy { public_send(name).detach }
end
end
end
diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb
index 6eace65b79..d61acb6fad 100644
--- a/activestorage/lib/active_storage/attached/many.rb
+++ b/activestorage/lib/active_storage/attached/many.rb
@@ -44,20 +44,16 @@ module ActiveStorage
attachments.destroy_all if attached?
end
+ ##
+ # :method: purge
+ #
# Directly purges each associated attachment (i.e. destroys the blobs and
# attachments and deletes the files on the service).
- def purge
- if attached?
- attachments.each(&:purge)
- attachments.reload
- end
- end
+
+ ##
+ # :method: purge_later
+ #
# Purges each associated attachment through the queuing system.
- def purge_later
- if attached?
- attachments.each(&:purge_later)
- end
- end
end
end
diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb
index 98477547c3..f992cb5f84 100644
--- a/activestorage/lib/active_storage/attached/one.rb
+++ b/activestorage/lib/active_storage/attached/one.rb
@@ -24,10 +24,16 @@ module ActiveStorage
# person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg")
# person.avatar.attach(avatar_blob) # ActiveStorage::Blob object
def attach(attachable)
- if attached? && dependent == :purge_later
- replace attachable
- else
- write_attachment build_attachment_from(attachable)
+ blob_was = blob if attached?
+ blob = create_blob_from(attachable)
+
+ unless blob == blob_was
+ transaction do
+ detach
+ write_attachment build_attachment(blob: blob)
+ end
+
+ blob_was.purge_later if blob_was && dependent == :purge_later
end
end
@@ -67,17 +73,10 @@ module ActiveStorage
end
private
- def replace(attachable)
- blob.tap do
- transaction do
- detach
- write_attachment build_attachment_from(attachable)
- end
- end.purge_later
- end
+ delegate :transaction, to: :record
- def build_attachment_from(attachable)
- ActiveStorage::Attachment.new(record: record, name: name, blob: create_blob_from(attachable))
+ def build_attachment(blob:)
+ ActiveStorage::Attachment.new(record: record, name: name, blob: blob)
end
def write_attachment(attachment)
diff --git a/activestorage/lib/active_storage/downloading.rb b/activestorage/lib/active_storage/downloading.rb
index f2a1fffdcb..7c3d20ade0 100644
--- a/activestorage/lib/active_storage/downloading.rb
+++ b/activestorage/lib/active_storage/downloading.rb
@@ -27,6 +27,7 @@ module ActiveStorage
def download_blob_to(file) #:doc:
file.binmode
blob.download { |chunk| file.write(chunk) }
+ file.flush
file.rewind
end
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index 8ba32490b1..99588cdd4b 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -3,7 +3,8 @@
require "rails"
require "active_storage"
-require "active_storage/previewer/pdf_previewer"
+require "active_storage/previewer/poppler_pdf_previewer"
+require "active_storage/previewer/mupdf_previewer"
require "active_storage/previewer/video_previewer"
require "active_storage/analyzer/image_analyzer"
@@ -14,11 +15,19 @@ module ActiveStorage
isolate_namespace ActiveStorage
config.active_storage = ActiveSupport::OrderedOptions.new
- config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
+ config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ]
config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
config.active_storage.paths = ActiveSupport::OrderedOptions.new
- config.active_storage.variable_content_types = %w( image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop )
+ config.active_storage.variable_content_types = %w(
+ image/png
+ image/gif
+ image/jpg
+ image/jpeg
+ image/vnd.adobe.photoshop
+ image/vnd.microsoft.icon
+ )
+
config.active_storage.content_types_to_serve_as_binary = %w(
text/html
text/javascript
@@ -34,11 +43,12 @@ module ActiveStorage
initializer "active_storage.configs" do
config.after_initialize do |app|
- ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
- ActiveStorage.queue = app.config.active_storage.queue
- ActiveStorage.previewers = app.config.active_storage.previewers || []
- ActiveStorage.analyzers = app.config.active_storage.analyzers || []
- ActiveStorage.paths = app.config.active_storage.paths || {}
+ ActiveStorage.logger = app.config.active_storage.logger || Rails.logger
+ ActiveStorage.queue = app.config.active_storage.queue
+ ActiveStorage.variant_processor = app.config.active_storage.variant_processor || :mini_magick
+ ActiveStorage.previewers = app.config.active_storage.previewers || []
+ ActiveStorage.analyzers = app.config.active_storage.analyzers || []
+ ActiveStorage.paths = app.config.active_storage.paths || {}
ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
@@ -60,7 +70,7 @@ module ActiveStorage
end
initializer "active_storage.services" do
- config.to_prepare do
+ ActiveSupport.on_load(:active_storage_blob) do
if config_choice = Rails.configuration.active_storage.service
configs = Rails.configuration.active_storage.service_configurations ||= begin
config_file = Pathname.new(Rails.root.join("config/storage.yml"))
diff --git a/activestorage/lib/active_storage/previewer/mupdf_previewer.rb b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb
new file mode 100644
index 0000000000..ae02a4889d
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::MuPDFPreviewer < Previewer
+ class << self
+ def accept?(blob)
+ blob.content_type == "application/pdf" && mutool_exists?
+ end
+
+ def mutool_path
+ ActiveStorage.paths[:mutool] || "mutool"
+ end
+
+ def mutool_exists?
+ return @mutool_exists unless @mutool_exists.nil?
+
+ system mutool_path, out: File::NULL, err: File::NULL
+
+ @mutool_exists = $?.exitstatus == 1
+ end
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ draw self.class.mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb
deleted file mode 100644
index 426ff51eb1..0000000000
--- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveStorage
- class Previewer::PDFPreviewer < Previewer
- def self.accept?(blob)
- blob.content_type == "application/pdf"
- end
-
- def preview
- download_blob_to_tempfile do |input|
- draw_first_page_from input do |output|
- yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
- end
- end
- end
-
- private
- def draw_first_page_from(file, &block)
- draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block
- end
-
- def mutool_path
- ActiveStorage.paths[:mutool] || "mutool"
- end
- end
-end
diff --git a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
new file mode 100644
index 0000000000..2a787362cf
--- /dev/null
+++ b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module ActiveStorage
+ class Previewer::PopplerPDFPreviewer < Previewer
+ class << self
+ def accept?(blob)
+ blob.content_type == "application/pdf" && pdftoppm_exists?
+ end
+
+ def pdftoppm_path
+ ActiveStorage.paths[:pdftoppm] || "pdftoppm"
+ end
+
+ def pdftoppm_exists?
+ return @pdftoppm_exists unless @pdftoppm_exists.nil?
+
+ @pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL)
+ end
+ end
+
+ def preview
+ download_blob_to_tempfile do |input|
+ draw_first_page_from input do |output|
+ yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png"
+ end
+ end
+ end
+
+ private
+ def draw_first_page_from(file, &block)
+ # use 72 dpi to match thumbnail dimesions of the PDF
+ draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
+ end
+ end
+end
diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb
index f2e1269f27..949969fc95 100644
--- a/activestorage/lib/active_storage/service.rb
+++ b/activestorage/lib/active_storage/service.rb
@@ -73,6 +73,11 @@ module ActiveStorage
raise NotImplementedError
end
+ # Return the partial content in the byte +range+ of the file at the +key+.
+ def download_chunk(key, range)
+ raise NotImplementedError
+ end
+
# Delete the file at the +key+.
def delete(key)
raise NotImplementedError
diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb
index 0a9eb7f23d..2867a4e441 100644
--- a/activestorage/lib/active_storage/service/azure_storage_service.rb
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -8,14 +8,13 @@ module ActiveStorage
# Wraps the Microsoft Azure Storage Blob Service as an Active Storage service.
# See ActiveStorage::Service for the generic API documentation that applies to all services.
class Service::AzureStorageService < Service
- attr_reader :client, :path, :blobs, :container, :signer
+ attr_reader :client, :blobs, :container, :signer
- def initialize(path:, storage_account_name:, storage_access_key:, container:)
+ def initialize(storage_account_name:, storage_access_key:, container:)
@client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
@signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
@blobs = client.blob_client
@container = container
- @path = path
end
def upload(key, io, checksum: nil)
@@ -41,6 +40,13 @@ module ActiveStorage
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end)
+ io.force_encoding(Encoding::BINARY)
+ end
+ end
+
def delete(key)
instrument :delete, key: key do
begin
@@ -77,9 +83,9 @@ module ActiveStorage
def url(key, expires_in:, filename:, disposition:, content_type:)
instrument :url, key: key do |payload|
- base_url = url_for(key)
generated_url = signer.signed_uri(
- URI(base_url), false,
+ uri_for(key), false,
+ service: "b",
permissions: "r",
expiry: format_expiry(expires_in),
content_disposition: content_disposition_with(type: disposition, filename: filename),
@@ -94,9 +100,12 @@ module ActiveStorage
def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
instrument :url, key: key do |payload|
- base_url = url_for(key)
- generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw",
- expiry: format_expiry(expires_in)).to_s
+ generated_url = signer.signed_uri(
+ uri_for(key), false,
+ service: "b",
+ permissions: "rw",
+ expiry: format_expiry(expires_in)
+ ).to_s
payload[:url] = generated_url
@@ -109,8 +118,8 @@ module ActiveStorage
end
private
- def url_for(key)
- "#{path}/#{container}/#{key}"
+ def uri_for(key)
+ blobs.generate_uri("#{container}/#{key}")
end
def blob_for(key)
diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb
index d17eea9046..5b652fe74e 100644
--- a/activestorage/lib/active_storage/service/disk_service.rb
+++ b/activestorage/lib/active_storage/service/disk_service.rb
@@ -9,10 +9,10 @@ module ActiveStorage
# Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API
# documentation that applies to all services.
class Service::DiskService < Service
- attr_reader :root, :host
+ attr_reader :root
- def initialize(root:, host: "http://localhost:3000")
- @root, @host = root, host
+ def initialize(root:)
+ @root = root
end
def upload(key, io, checksum: nil)
@@ -38,6 +38,15 @@ module ActiveStorage
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ File.open(path_for(key), "rb") do |file|
+ file.seek range.begin
+ file.read range.size
+ end
+ end
+ end
+
def delete(key)
instrument :delete, key: key do
begin
@@ -69,12 +78,12 @@ module ActiveStorage
verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key)
generated_url =
- Rails.application.routes.url_helpers.rails_disk_service_url(
+ url_helpers.rails_disk_service_url(
verified_key_with_expiration,
+ host: current_host,
filename: filename,
disposition: content_disposition_with(type: disposition, filename: filename),
- content_type: content_type,
- host: host
+ content_type: content_type
)
payload[:url] = generated_url
@@ -96,7 +105,7 @@ module ActiveStorage
purpose: :blob_token }
)
- generated_url = Rails.application.routes.url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: host)
+ generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host)
payload[:url] = generated_url
@@ -127,5 +136,13 @@ module ActiveStorage
raise ActiveStorage::IntegrityError
end
end
+
+ def url_helpers
+ @url_helpers ||= Rails.application.routes.url_helpers
+ end
+
+ def current_host
+ ActiveStorage::Current.host
+ end
end
end
diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb
index 6f6f4105fe..16a0765fc5 100644
--- a/activestorage/lib/active_storage/service/gcs_service.rb
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -3,7 +3,10 @@
gem "google-cloud-storage", "~> 1.8"
require "google/cloud/storage"
+require "net/http"
+
require "active_support/core_ext/object/to_query"
+require "active_storage/filename"
module ActiveStorage
# Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API
@@ -43,6 +46,17 @@ module ActiveStorage
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ file = file_for(key)
+ uri = URI(file.signed_url(expires: 30.seconds))
+
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
+ client.get(uri, "Range" => "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body
+ end
+ end
+ end
+
def delete(key)
instrument :delete, key: key do
begin
@@ -80,10 +94,9 @@ module ActiveStorage
end
end
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ def url_for_direct_upload(key, expires_in:, checksum:, **)
instrument :url, key: key do |payload|
- generated_url = bucket.signed_url key, method: "PUT", expires: expires_in,
- content_type: content_type, content_md5: checksum
+ generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
payload[:url] = generated_url
@@ -91,8 +104,8 @@ module ActiveStorage
end
end
- def headers_for_direct_upload(key, content_type:, checksum:, **)
- { "Content-Type" => content_type, "Content-MD5" => checksum }
+ def headers_for_direct_upload(key, checksum:, **)
+ { "Content-MD5" => checksum }
end
private
diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb
index 7eca8ce7f4..6002ef5a00 100644
--- a/activestorage/lib/active_storage/service/mirror_service.rb
+++ b/activestorage/lib/active_storage/service/mirror_service.rb
@@ -9,7 +9,7 @@ module ActiveStorage
class Service::MirrorService < Service
attr_reader :primary, :mirrors
- delegate :download, :exist?, :url, to: :primary
+ delegate :download, :download_chunk, :exist?, :url, to: :primary
# Stitch together from named services.
def self.build(primary:, mirrors:, configurator:, **options) #:nodoc:
diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb
index c95672f338..0286e7ff21 100644
--- a/activestorage/lib/active_storage/service/s3_service.rb
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -9,8 +9,8 @@ module ActiveStorage
class Service::S3Service < Service
attr_reader :client, :bucket, :upload_options
- def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options)
- @client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options)
+ def initialize(bucket:, upload: {}, **options)
+ @client = Aws::S3::Resource.new(**options)
@bucket = @client.bucket(bucket)
@upload_options = upload
@@ -33,11 +33,17 @@ module ActiveStorage
end
else
instrument :download, key: key do
- object_for(key).get.body.read.force_encoding(Encoding::BINARY)
+ object_for(key).get.body.string.force_encoding(Encoding::BINARY)
end
end
end
+ def download_chunk(key, range)
+ instrument :download_chunk, key: key, range: range do
+ object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY)
+ end
+ end
+
def delete(key)
instrument :delete, key: key do
object_for(key).delete