diff options
Diffstat (limited to 'lib/active_storage')
-rw-r--r-- | lib/active_storage/attachment.rb | 30 | ||||
-rw-r--r-- | lib/active_storage/blob.rb | 95 | ||||
-rw-r--r-- | lib/active_storage/filename.rb | 31 | ||||
-rw-r--r-- | lib/active_storage/purge_job.rb | 10 | ||||
-rw-r--r-- | lib/active_storage/service.rb | 96 | ||||
-rw-r--r-- | lib/active_storage/service/configurator.rb | 28 | ||||
-rw-r--r-- | lib/active_storage/service/disk_service.rb | 89 | ||||
-rw-r--r-- | lib/active_storage/service/gcs_service.rb | 71 | ||||
-rw-r--r-- | lib/active_storage/service/mirror_service.rb | 40 | ||||
-rw-r--r-- | lib/active_storage/service/s3_service.rb | 89 | ||||
-rw-r--r-- | lib/active_storage/variant.rb | 35 | ||||
-rw-r--r-- | lib/active_storage/variation.rb | 41 | ||||
-rw-r--r-- | lib/active_storage/verified_key_with_expiration.rb | 24 |
13 files changed, 0 insertions, 679 deletions
diff --git a/lib/active_storage/attachment.rb b/lib/active_storage/attachment.rb deleted file mode 100644 index 20c619aa5a..0000000000 --- a/lib/active_storage/attachment.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "active_storage/blob" -require "global_id" -require "active_support/core_ext/module/delegation" - -# Schema: id, record_gid, blob_id, created_at -class ActiveStorage::Attachment < ActiveRecord::Base - self.table_name = "active_storage_attachments" - - belongs_to :blob, class_name: "ActiveStorage::Blob" - - delegate_missing_to :blob - - def record - @record ||= GlobalID::Locator.locate(record_gid) - end - - def record=(record) - @record = record - self.record_gid = record&.to_gid - end - - def purge - blob.purge - destroy - end - - def purge_later - ActiveStorage::PurgeJob.perform_later(self) - end -end diff --git a/lib/active_storage/blob.rb b/lib/active_storage/blob.rb deleted file mode 100644 index 6bd3941cd8..0000000000 --- a/lib/active_storage/blob.rb +++ /dev/null @@ -1,95 +0,0 @@ -require "active_storage/service" -require "active_storage/filename" -require "active_storage/purge_job" -require "active_storage/variant" - -# Schema: id, key, filename, content_type, metadata, byte_size, checksum, created_at -class ActiveStorage::Blob < ActiveRecord::Base - self.table_name = "active_storage_blobs" - - has_secure_token :key - store :metadata, coder: JSON - - class_attribute :service - - class << self - def build_after_upload(io:, filename:, content_type: nil, metadata: nil) - new.tap do |blob| - blob.filename = filename - blob.content_type = content_type - blob.metadata = metadata - - blob.upload io - end - end - - def create_after_upload!(io:, filename:, content_type: nil, metadata: nil) - build_after_upload(io: io, filename: filename, content_type: content_type, metadata: metadata).tap(&:save!) - end - - def create_before_direct_upload!(filename:, byte_size:, checksum:, content_type: nil, metadata: nil) - create! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type, metadata: metadata - end - end - - - def key - # We can't wait until the record is first saved to have a key for it - self[:key] ||= self.class.generate_unique_secure_token - end - - def filename - ActiveStorage::Filename.new(self[:filename]) - end - - def variant(transformations) - ActiveStorage::Variant.new(self, ActiveStorage::Variation.new(transformations)) - end - - - def url(expires_in: 5.minutes, disposition: :inline) - service.url key, expires_in: expires_in, disposition: disposition, filename: filename - end - - def url_for_direct_upload(expires_in: 5.minutes) - service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size - end - - - def upload(io) - self.checksum = compute_checksum_in_chunks(io) - self.byte_size = io.size - - service.upload(key, io, checksum: checksum) - end - - def download(&block) - service.download key, &block - end - - - def delete - service.delete key - end - - def purge - delete - destroy - end - - def purge_later - ActiveStorage::PurgeJob.perform_later(self) - end - - - private - def compute_checksum_in_chunks(io) - Digest::MD5.new.tap do |checksum| - while chunk = io.read(5.megabytes) - checksum << chunk - end - - io.rewind - end.base64digest - end -end diff --git a/lib/active_storage/filename.rb b/lib/active_storage/filename.rb deleted file mode 100644 index 71614b5113..0000000000 --- a/lib/active_storage/filename.rb +++ /dev/null @@ -1,31 +0,0 @@ -class ActiveStorage::Filename - include Comparable - - def initialize(filename) - @filename = filename - end - - def extname - File.extname(@filename) - end - - def extension - extname.from(1) - end - - def base - File.basename(@filename, extname) - end - - def sanitized - @filename.encode(Encoding::UTF_8, invalid: :replace, undef: :replace, replace: "�").strip.tr("\u{202E}%$|:;/\t\r\n\\", "-") - end - - def to_s - sanitized.to_s - end - - def <=>(other) - to_s.downcase <=> other.to_s.downcase - end -end diff --git a/lib/active_storage/purge_job.rb b/lib/active_storage/purge_job.rb deleted file mode 100644 index b59d3687f8..0000000000 --- a/lib/active_storage/purge_job.rb +++ /dev/null @@ -1,10 +0,0 @@ -require "active_job" - -class ActiveStorage::PurgeJob < ActiveJob::Base - # FIXME: Limit this to a custom ActiveStorage error - retry_on StandardError - - def perform(attachment_or_blob) - attachment_or_blob.purge - end -end diff --git a/lib/active_storage/service.rb b/lib/active_storage/service.rb deleted file mode 100644 index cba9cd9c83..0000000000 --- a/lib/active_storage/service.rb +++ /dev/null @@ -1,96 +0,0 @@ -require_relative "log_subscriber" - -# Abstract class serving as an interface for concrete services. -# -# The available services are: -# -# * +Disk+, to manage attachments saved directly on the hard drive. -# * +GCS+, to manage attachments through Google Cloud Storage. -# * +S3+, to manage attachments through Amazon S3. -# * +Mirror+, to be able to use several services to manage attachments. -# -# Inside a Rails application, you can set-up your services through the -# generated <tt>config/storage_services.yml</tt> file and reference one -# of the aforementioned constant under the +service+ key. For example: -# -# local: -# service: Disk -# root: <%= Rails.root.join("storage") %> -# -# You can checkout the service's constructor to know which keys are required. -# -# Then, in your application's configuration, you can specify the service to -# use like this: -# -# config.active_storage.service = :local -# -# If you are using Active Storage outside of a Ruby on Rails application, you -# can configure the service to use like this: -# -# ActiveStorage::Blob.service = ActiveStorage::Service.configure( -# :Disk, -# root: Pathname("/foo/bar/storage") -# ) -class ActiveStorage::Service - class ActiveStorage::IntegrityError < StandardError; end - - extend ActiveSupport::Autoload - autoload :Configurator - - class_attribute :logger - - class << self - # Configure an Active Storage service by name from a set of configurations, - # typically loaded from a YAML file. The Active Storage engine uses this - # to set the global Active Storage service when the app boots. - def configure(service_name, configurations) - Configurator.build(service_name, configurations) - end - - # Override in subclasses that stitch together multiple services and hence - # need to build additional services using the configurator. - # - # Passes the configurator and all of the service's config as keyword args. - # - # See MirrorService for an example. - def build(configurator:, service: nil, **service_config) #:nodoc: - new(**service_config) - end - end - - def upload(key, io, checksum: nil) - raise NotImplementedError - end - - def download(key) - raise NotImplementedError - end - - def delete(key) - raise NotImplementedError - end - - def exist?(key) - raise NotImplementedError - end - - def url(key, expires_in:, disposition:, filename:) - raise NotImplementedError - end - - def url_for_direct_upload(key, expires_in:, content_type:, content_length:) - raise NotImplementedError - end - - private - def instrument(operation, key, payload = {}, &block) - ActiveSupport::Notifications.instrument( - "service_#{operation}.active_storage", - payload.merge(key: key, service: service_name), &block) - end - - def service_name - # ActiveStorage::Service::DiskService => Disk - self.class.name.split("::").third.remove("Service") - end -end diff --git a/lib/active_storage/service/configurator.rb b/lib/active_storage/service/configurator.rb deleted file mode 100644 index 00ae24d251..0000000000 --- a/lib/active_storage/service/configurator.rb +++ /dev/null @@ -1,28 +0,0 @@ -class ActiveStorage::Service::Configurator #:nodoc: - attr_reader :configurations - - def self.build(service_name, configurations) - new(configurations).build(service_name) - end - - def initialize(configurations) - @configurations = configurations.deep_symbolize_keys - end - - def build(service_name) - config = config_for(service_name.to_sym) - resolve(config.fetch(:service)).build(**config, configurator: self) - end - - private - def config_for(name) - configurations.fetch name do - raise "Missing configuration for the #{name.inspect} Active Storage service. Configurations available for #{configurations.keys.inspect}" - end - end - - def resolve(class_name) - require "active_storage/service/#{class_name.to_s.downcase}_service" - ActiveStorage::Service.const_get(:"#{class_name}Service") - end -end diff --git a/lib/active_storage/service/disk_service.rb b/lib/active_storage/service/disk_service.rb deleted file mode 100644 index a2a27528c1..0000000000 --- a/lib/active_storage/service/disk_service.rb +++ /dev/null @@ -1,89 +0,0 @@ -require "fileutils" -require "pathname" -require "digest/md5" -require "active_support/core_ext/numeric/bytes" - -class ActiveStorage::Service::DiskService < ActiveStorage::Service - attr_reader :root - - def initialize(root:) - @root = root - end - - def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do - IO.copy_stream(io, make_path_for(key)) - ensure_integrity_of(key, checksum) if checksum - end - end - - def download(key) - if block_given? - instrument :streaming_download, key do - File.open(path_for(key), "rb") do |file| - while data = file.read(64.kilobytes) - yield data - end - end - end - else - instrument :download, key do - File.binread path_for(key) - end - end - end - - def delete(key) - instrument :delete, key do - begin - File.delete path_for(key) - rescue Errno::ENOENT - # Ignore files already deleted - end - end - end - - def exist?(key) - instrument :exist, key do |payload| - answer = File.exist? path_for(key) - payload[:exist] = answer - answer - end - end - - def url(key, expires_in:, disposition:, filename:) - instrument :url, key do |payload| - verified_key_with_expiration = ActiveStorage::VerifiedKeyWithExpiration.encode(key, expires_in: expires_in) - - generated_url = - if defined?(Rails) && defined?(Rails.application) - Rails.application.routes.url_helpers.rails_disk_blob_path(verified_key_with_expiration, disposition: disposition, filename: filename) - else - "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?disposition=#{disposition}" - end - - payload[:url] = generated_url - - generated_url - end - end - - private - def path_for(key) - File.join root, folder_for(key), key - end - - def folder_for(key) - [ key[0..1], key[2..3] ].join("/") - end - - def make_path_for(key) - 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 - raise ActiveStorage::IntegrityError - end - end -end diff --git a/lib/active_storage/service/gcs_service.rb b/lib/active_storage/service/gcs_service.rb deleted file mode 100644 index 7053a130c0..0000000000 --- a/lib/active_storage/service/gcs_service.rb +++ /dev/null @@ -1,71 +0,0 @@ -require "google/cloud/storage" -require "active_support/core_ext/object/to_query" - -class ActiveStorage::Service::GCSService < ActiveStorage::Service - attr_reader :client, :bucket - - def initialize(project:, keyfile:, bucket:) - @client = Google::Cloud::Storage.new(project: project, keyfile: keyfile) - @bucket = @client.bucket(bucket) - end - - def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do - begin - bucket.create_file(io, key, md5: checksum) - rescue Google::Cloud::InvalidArgumentError - raise ActiveStorage::IntegrityError - end - end - end - - # FIXME: Add streaming when given a block - def download(key) - instrument :download, key do - io = file_for(key).download - io.rewind - io.read - end - end - - def delete(key) - instrument :delete, key do - file_for(key)&.delete - end - end - - def exist?(key) - instrument :exist, key do |payload| - answer = file_for(key).present? - payload[:exist] = answer - answer - end - end - - def url(key, expires_in:, disposition:, filename:) - instrument :url, key do |payload| - query = { "response-content-disposition" => "#{disposition}; filename=\"#{filename}\"" } - generated_url = file_for(key).signed_url(expires: expires_in, query: query) - - payload[:url] = generated_url - - generated_url - end - end - - def url_for_direct_upload(key, expires_in:, content_type:, content_length:) - instrument :url, key do |payload| - generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, - content_type: content_type - - payload[:url] = generated_url - - generated_url - end - end - - private - def file_for(key) - bucket.file(key) - end -end diff --git a/lib/active_storage/service/mirror_service.rb b/lib/active_storage/service/mirror_service.rb deleted file mode 100644 index 54465cad05..0000000000 --- a/lib/active_storage/service/mirror_service.rb +++ /dev/null @@ -1,40 +0,0 @@ -require "active_support/core_ext/module/delegation" - -class ActiveStorage::Service::MirrorService < ActiveStorage::Service - attr_reader :primary, :mirrors - - delegate :download, :exist?, :url, to: :primary - - # Stitch together from named services. - def self.build(primary:, mirrors:, configurator:, **options) #:nodoc: - new \ - primary: configurator.build(primary), - mirrors: mirrors.collect { |name| configurator.build name } - end - - def initialize(primary:, mirrors:) - @primary, @mirrors = primary, mirrors - end - - def upload(key, io, checksum: nil) - each_service.collect do |service| - service.upload key, io.tap(&:rewind), checksum: checksum - end - end - - def delete(key) - perform_across_services :delete, key - end - - private - def each_service(&block) - [ primary, *mirrors ].each(&block) - end - - def perform_across_services(method, *args) - # FIXME: Convert to be threaded - each_service.collect do |service| - service.public_send method, *args - end - end -end diff --git a/lib/active_storage/service/s3_service.rb b/lib/active_storage/service/s3_service.rb deleted file mode 100644 index efffdec157..0000000000 --- a/lib/active_storage/service/s3_service.rb +++ /dev/null @@ -1,89 +0,0 @@ -require "aws-sdk" -require "active_support/core_ext/numeric/bytes" - -class ActiveStorage::Service::S3Service < ActiveStorage::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) - @bucket = @client.bucket(bucket) - - @upload_options = upload - end - - def upload(key, io, checksum: nil) - instrument :upload, key, checksum: checksum do - begin - object_for(key).put(upload_options.merge(body: io, content_md5: checksum)) - rescue Aws::S3::Errors::BadDigest - raise ActiveStorage::IntegrityError - end - end - end - - def download(key) - if block_given? - instrument :streaming_download, key do - stream(key, &block) - end - else - instrument :download, key do - object_for(key).get.body.read.force_encoding(Encoding::BINARY) - end - end - end - - def delete(key) - instrument :delete, key do - object_for(key).delete - end - end - - def exist?(key) - instrument :exist, key do |payload| - answer = object_for(key).exists? - payload[:exist] = answer - answer - end - end - - def url(key, expires_in:, disposition:, filename:) - instrument :url, key do |payload| - generated_url = object_for(key).presigned_url :get, expires_in: expires_in, - response_content_disposition: "#{disposition}; filename=\"#{filename}\"" - - payload[:url] = generated_url - - generated_url - end - end - - def url_for_direct_upload(key, expires_in:, content_type:, content_length:) - instrument :url, key do |payload| - generated_url = object_for(key).presigned_url :put, expires_in: expires_in, - content_type: content_type, content_length: content_length - - payload[:url] = generated_url - - generated_url - end - end - - private - def object_for(key) - bucket.object(key) - end - - # Reads the object for the given key in chunks, yielding each to the block. - def stream(key, options = {}, &block) - object = object_for(key) - - chunk_size = 5.megabytes - offset = 0 - - while offset < object.content_length - yield object.read(options.merge(range: "bytes=#{offset}-#{offset + chunk_size - 1}")) - offset += chunk_size - end - end -end diff --git a/lib/active_storage/variant.rb b/lib/active_storage/variant.rb deleted file mode 100644 index 435033f980..0000000000 --- a/lib/active_storage/variant.rb +++ /dev/null @@ -1,35 +0,0 @@ -require "active_storage/blob" -require "mini_magick" - -# Image blobs can have variants that are the result of a set of transformations applied to the original. -class ActiveStorage::Variant - attr_reader :blob, :variation - delegate :service, to: :blob - - def initialize(blob, variation) - @blob, @variation = blob, variation - end - - def processed - process unless service.exist?(key) - self - end - - def key - "variants/#{blob.key}/#{variation.key}" - end - - def url(expires_in: 5.minutes, disposition: :inline) - service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename - end - - - private - def process - service.upload key, transform(service.download(blob.key)) - end - - def transform(io) - File.open MiniMagick::Image.read(io).tap { |image| variation.transform(image) }.path - end -end diff --git a/lib/active_storage/variation.rb b/lib/active_storage/variation.rb deleted file mode 100644 index f7c81bb99a..0000000000 --- a/lib/active_storage/variation.rb +++ /dev/null @@ -1,41 +0,0 @@ -require "active_support/core_ext/object/inclusion" - -# A set of transformations that can be applied to a blob to create a variant. -class ActiveStorage::Variation - class_attribute :verifier - - attr_reader :transformations - - class << self - def decode(key) - new verifier.verify(key) - end - - def encode(transformations) - verifier.generate(transformations) - end - end - - def initialize(transformations) - @transformations = transformations - end - - def transform(image) - transformations.each do |(method, argument)| - if eligible_argument?(argument) - image.public_send(method, argument) - else - image.public_send(method) - end - end - end - - def key - self.class.encode(transformations) - end - - private - def eligible_argument?(argument) - argument.present? && argument != true - end -end diff --git a/lib/active_storage/verified_key_with_expiration.rb b/lib/active_storage/verified_key_with_expiration.rb deleted file mode 100644 index 4a46483db5..0000000000 --- a/lib/active_storage/verified_key_with_expiration.rb +++ /dev/null @@ -1,24 +0,0 @@ -class ActiveStorage::VerifiedKeyWithExpiration - class_attribute :verifier - - class << self - def encode(key, expires_in: nil) - verifier.generate([ key, expires_at(expires_in) ]) - end - - def decode(encoded_key) - key, expires_at = verifier.verified(encoded_key) - - key if key && fresh?(expires_at) - end - - private - def expires_at(expires_in) - expires_in ? Time.now.utc.advance(seconds: expires_in) : nil - end - - def fresh?(expires_at) - expires_at.nil? || Time.now.utc < expires_at - end - end -end |