diff --git a/lib/active_storage/attached/macros.rb b/lib/active_storage/attached/macros.rb index 96493d1215..1e0f9a6b7e 100644 --- a/lib/active_storage/attached/macros.rb +++ b/lib/active_storage/attached/macros.rb @@ -1,7 +1,18 @@ module ActiveStorage::Attached::Macros + # Specifies the relation between a single attachment and the model. + # + # class User < ActiveRecord::Base + # has_one_attached :avatar + # end + # + # There is no column defined on the model side, Active Storage takes + # care of the mapping between your records and the attachment. + # + # If the +:dependent+ option isn't set, the attachment will be purged + # (i.e. destroyed) whenever the record is destroyed. def has_one_attached(name, dependent: :purge_later) define_method(name) do - instance_variable_get("@active_storage_attached_#{name}") || + instance_variable_get("@active_storage_attached_#{name}") || instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::One.new(name, self)) end @@ -10,9 +21,20 @@ module ActiveStorage::Attached::Macros end end + # Specifies the relation between multiple attachments and the model. + # + # class Gallery < ActiveRecord::Base + # has_many_attached :photos + # end + # + # There are no columns defined on the model side, Active Storage takes + # care of the mapping between your records and the attachments. + # + # If the +:dependent+ option isn't set, all the attachments will be purged + # (i.e. destroyed) whenever the record is destroyed. def has_many_attached(name, dependent: :purge_later) define_method(name) do - instance_variable_get("@active_storage_attached_#{name}") || + instance_variable_get("@active_storage_attached_#{name}") || instance_variable_set("@active_storage_attached_#{name}", ActiveStorage::Attached::Many.new(name, self)) end diff --git a/lib/active_storage/attached/many.rb b/lib/active_storage/attached/many.rb index f1535dfbc6..99d980196a 100644 --- a/lib/active_storage/attached/many.rb +++ b/lib/active_storage/attached/many.rb @@ -1,20 +1,36 @@ +# Representation of multiple attachments to a model. class ActiveStorage::Attached::Many < ActiveStorage::Attached delegate_missing_to :attachments + # Returns all the associated attachment records. + # + # You don't have to call this method to access the attachments' methods as + # they are all available at the model level. def attachments @attachments ||= ActiveStorage::Attachment.where(record_gid: record.to_gid.to_s, name: name) end + # Associates one or several attachments with the current record, saving + # them to the database. def attach(*attachables) @attachments = attachments | Array(attachables).flatten.collect do |attachable| ActiveStorage::Attachment.create!(record_gid: record.to_gid.to_s, name: name, blob: create_blob_from(attachable)) end end + # Checks the presence of attachments. + # + # class Gallery < ActiveRecord::Base + # has_many_attached :photos + # end + # + # Gallery.new.photos.attached? # => false def attached? attachments.any? end + # 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) @@ -22,6 +38,7 @@ class ActiveStorage::Attached::Many < ActiveStorage::Attached end end + # Purges each associated attachment through the queuing system. def purge_later if attached? attachments.each(&:purge_later) diff --git a/lib/active_storage/attached/one.rb b/lib/active_storage/attached/one.rb index d08d265992..80e4cb6234 100644 --- a/lib/active_storage/attached/one.rb +++ b/lib/active_storage/attached/one.rb @@ -1,18 +1,34 @@ +# Representation of a single attachment to a model. class ActiveStorage::Attached::One < ActiveStorage::Attached delegate_missing_to :attachment + # Returns the associated attachment record. + # + # You don't have to call this method to access the attachment's methods as + # they are all available at the model level. def attachment @attachment ||= ActiveStorage::Attachment.find_by(record_gid: record.to_gid.to_s, name: name) end + # Associates a given attachment with the current record, saving it to the + # database. def attach(attachable) @attachment = ActiveStorage::Attachment.create!(record_gid: record.to_gid.to_s, name: name, blob: create_blob_from(attachable)) end + # Checks the presence of the attachment. + # + # class User < ActiveRecord::Base + # has_one_attached :avatar + # end + # + # User.new.avatar.attached? # => false def attached? attachment.present? end + # Directly purges the attachment (i.e. destroys the blob and + # attachment and deletes the file on the service). def purge if attached? attachment.purge @@ -20,6 +36,7 @@ class ActiveStorage::Attached::One < ActiveStorage::Attached end end + # Purges the attachment through the queuing system. def purge_later if attached? attachment.purge_later diff --git a/lib/active_storage/disk_controller.rb b/lib/active_storage/disk_controller.rb index 3eba86c213..9d5b52d66f 100644 --- a/lib/active_storage/disk_controller.rb +++ b/lib/active_storage/disk_controller.rb @@ -4,11 +4,21 @@ require "active_storage/verified_key_with_expiration" require "active_support/core_ext/object/inclusion" +# This controller is a wrapper around local file downloading. It allows you to +# make abstraction of the URL generation logic and to serve files with expiry +# if you are using the +Disk+ service. +# +# By default, mounting the Active Storage engine inside your application will +# define a +/rails/blobs/:encoded_key+ route that will reference this controller's +# +show+ action and will be used to serve local files. +# +# A URL for an attachment can be generated through its +#url+ method, that +# will use the aforementioned route. class ActiveStorage::DiskController < ActionController::Base def show if key = decode_verified_key blob = ActiveStorage::Blob.find_by!(key: key) - + if stale?(etag: blob.checksum) send_data blob.download, filename: blob.filename, type: blob.content_type, disposition: disposition_param end diff --git a/lib/active_storage/migration.rb b/lib/active_storage/migration.rb index 433dd5026f..c56e7a1786 100644 --- a/lib/active_storage/migration.rb +++ b/lib/active_storage/migration.rb @@ -1,4 +1,4 @@ -class ActiveStorageCreateTables < ActiveRecord::Migration[5.1] +class ActiveStorageCreateTables < ActiveRecord::Migration[5.1] # :nodoc: def change create_table :active_storage_blobs do |t| t.string :key diff --git a/lib/active_storage/service.rb b/lib/active_storage/service.rb index 9aab654d80..f15958fda9 100644 --- a/lib/active_storage/service.rb +++ b/lib/active_storage/service.rb @@ -1,4 +1,34 @@ # 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 config/storage_services.yml 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 @@ -11,7 +41,6 @@ class ActiveStorage::Service end end - def upload(key, io, checksum: nil) raise NotImplementedError end