aboutsummaryrefslogtreecommitdiffstats
path: root/lib/active_storage
diff options
context:
space:
mode:
Diffstat (limited to 'lib/active_storage')
-rw-r--r--lib/active_storage/attachment.rb30
-rw-r--r--lib/active_storage/blob.rb95
-rw-r--r--lib/active_storage/filename.rb31
-rw-r--r--lib/active_storage/purge_job.rb10
-rw-r--r--lib/active_storage/service.rb96
-rw-r--r--lib/active_storage/service/configurator.rb28
-rw-r--r--lib/active_storage/service/disk_service.rb89
-rw-r--r--lib/active_storage/service/gcs_service.rb71
-rw-r--r--lib/active_storage/service/mirror_service.rb40
-rw-r--r--lib/active_storage/service/s3_service.rb89
-rw-r--r--lib/active_storage/variant.rb35
-rw-r--r--lib/active_storage/variation.rb41
-rw-r--r--lib/active_storage/verified_key_with_expiration.rb24
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