diff options
Diffstat (limited to 'activestorage/lib/active_storage')
13 files changed, 162 insertions, 71 deletions
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 008f5a796b..a582ed0137 100644 --- a/activestorage/lib/active_storage/attached/one.rb +++ b/activestorage/lib/active_storage/attached/one.rb @@ -20,10 +20,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 @@ -63,23 +69,7 @@ module ActiveStorage end private - def replace(attachable) - blob_was = blob - blob = create_blob_from(attachable) - - unless blob == blob_was - transaction do - detach - write_attachment build_attachment(blob: blob) - end - - blob_was.purge_later - end - end - - def build_attachment_from(attachable) - build_attachment blob: create_blob_from(attachable) - end + delegate :transaction, to: :record def build_attachment(blob:) ActiveStorage::Attachment.new(record: record, name: name, blob: blob) diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 1e223f9f17..1385e2aa84 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,7 +15,7 @@ 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 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..5682087469 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -41,6 +41,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 diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index d17eea9046..75b66081c3 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,11 @@ 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_path( verified_key_with_expiration, 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 +104,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_path(verified_token_with_expiration) payload[:url] = generated_url @@ -121,11 +129,17 @@ module ActiveStorage path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) } end + def ensure_integrity_of(key, checksum) unless Digest::MD5.file(path_for(key)).base64digest == checksum delete key raise ActiveStorage::IntegrityError end end + + + def url_helpers + @url_helpers ||= Rails.application.routes.url_helpers + end end end diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index d163605754..369c33cbdb 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,16 @@ module ActiveStorage end end + def download_chunk(key, range) + instrument :download_chunk, key: key, range: range do + uri = URI(url(key, expires_in: 30.seconds, filename: ActiveStorage::Filename.new(""), content_type: "application/octet-stream", disposition: "inline")) + + 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 @@ -91,7 +104,7 @@ module ActiveStorage end def headers_for_direct_upload(key, checksum:, **) - { "Content-Type" => "", "Content-MD5" => 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..8ab7a44131 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -38,6 +38,12 @@ module ActiveStorage 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 |