diff options
101 files changed, 1284 insertions, 998 deletions
diff --git a/.travis.yml b/.travis.yml index d1b384720a..46b9c45133 100644 --- a/.travis.yml +++ b/.travis.yml @@ -45,7 +45,7 @@ env: matrix: - "GEM=railties" - "GEM=ap,ac" - - "GEM=am,amo,as,av,aj" + - "GEM=am,amo,as,av,aj,ast" - "GEM=as PRESERVE_TIMEZONES=1" - "GEM=ar:mysql2" - "GEM=ar:sqlite3" @@ -1,9 +1,6 @@ source "https://rubygems.org" -git_source(:github) do |repo_name| - repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/") - "https://github.com/#{repo_name}.git" -end +git_source(:github) { |repo| "https://github.com/#{repo}.git" } gemspec diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index fbe9863af7..560ee89846 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,4 +1,4 @@ -* Hash long stream identifiers when using Postgres adapter. +* Hash long stream identifiers when using PostgreSQL adapter. PostgreSQL has a limit on identifiers length (63 chars, [docs](https://www.postgresql.org/docs/current/static/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS)). Provided fix minifies identifiers longer than 63 chars by hashing them with SHA1. @@ -7,7 +7,7 @@ *Vladimir Dementyev* -* ActionCable's `redis` adapter allows for other common redis-rb options (`host`, `port`, `db`, `password`) in cable.yml. +* Action Cable's `redis` adapter allows for other common redis-rb options (`host`, `port`, `db`, `password`) in cable.yml. Previously, it accepts only a [redis:// url](https://www.iana.org/assignments/uri-schemes/prov/redis) as an option. While we can add all of these options to the `url` itself, it is not explicitly documented. This alternative setup @@ -16,7 +16,7 @@ *Marc Rendl Ignacio* -* ActionCable socket errors are now logged to the console +* Action Cable socket errors are now logged to the console Previously any socket errors were ignored and this made it hard to diagnose socket issues (e.g. as discussed in #28362). diff --git a/actioncable/README.md b/actioncable/README.md index 6946dbefb0..9667403673 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -53,7 +53,7 @@ module ApplicationCable private def find_verified_user - if verified_user = User.find_by(id: cookies.signed[:user_id]) + if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user else reject_unauthorized_connection diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index 8dbafe5105..84053db9fd 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -26,7 +26,7 @@ module ActionCable # # private # def find_verified_user - # User.find_by_identity(cookies.signed[:identity_id]) || + # User.find_by_identity(cookies.encrypted[:identity_id]) || # reject_unauthorized_connection # end # end diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb index 230433a110..7f90cb3876 100644 --- a/actioncable/test/connection/multiple_identifiers_test.rb +++ b/actioncable/test/connection/multiple_identifiers_test.rb @@ -36,8 +36,4 @@ class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase @connection.process @connection.send :handle_open end - - def close_connection - @connection.send :handle_close - end end diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb index 6387fb792c..4cb58e7fd0 100644 --- a/actioncable/test/connection/string_identifier_test.rb +++ b/actioncable/test/connection/string_identifier_test.rb @@ -38,8 +38,4 @@ class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase @connection.process @connection.send :on_open end - - def close_connection - @connection.send :on_close - end end diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 6add4ec89c..a54eb52dcb 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -590,10 +590,6 @@ module ActionMailer attr_internal :message - # Instantiate a new mailer object. If +method_name+ is not +nil+, the mailer - # will be initialized according to the named method. If not, the mailer will - # remain uninitialized (useful when you only need to invoke the "receive" - # method, for instance). def initialize super() @_mail_was_called = false diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 291e019530..938b24a6b9 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,11 @@ +* Deprecate `ActionDispatch::TestResponse` response aliases + + `#success?`, `#missing?` & `#error?` are not supported by the actual + `ActionDispatch::Response` object and can produce false-positives. Instead, + use the response helpers provided by `Rack::Response`. + + *Trevor Wistaff* + * Protect from forgery by default Rather than protecting from forgery in the generated `ApplicationController`, @@ -14,11 +22,11 @@ * `driven_by` now registers poltergeist and capybara-webkit - If driver poltergeist or capybara-webkit is set for System Tests, + If poltergeist or capybara-webkit are set as drivers is set for System Tests, `driven_by` will register the driver and set additional options passed via - `:options` param. + the `:options` parameter. - Refer to drivers documentation to learn what options can be passed. + Refer to the respective driver's documentation to see what options can be passed. *Mario Chavez* diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb index 0b7e9ca945..b23ea7479c 100644 --- a/actionpack/lib/action_dispatch/testing/test_response.rb +++ b/actionpack/lib/action_dispatch/testing/test_response.rb @@ -20,13 +20,31 @@ module ActionDispatch end # Was the response successful? - alias_method :success?, :successful? + def success? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The success? predicate is deprecated and will be removed in Rails 6.0. + Please use successful? as provided by Rack::Response::Helpers. + MSG + successful? + end # Was the URL not found? - alias_method :missing?, :not_found? + def missing? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The missing? predicate is deprecated and will be removed in Rails 6.0. + Please use not_found? as provided by Rack::Response::Helpers. + MSG + not_found? + end # Was there a server-side error? - alias_method :error?, :server_error? + def error? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The error? predicate is deprecated and will be removed in Rails 6.0. + Please use server_error? as provided by Rack::Response::Helpers. + MSG + server_error? + end def parsed_body @parsed_body ||= @response_parser.call(body) diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 67d6dea86f..a14083c6a2 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -296,13 +296,8 @@ class ResponseTest < ActiveSupport::TestCase end test "read content type with default charset utf-8" do - original = ActionDispatch::Response.default_charset - begin - resp = ActionDispatch::Response.new(200, "Content-Type" => "text/xml") - assert_equal("utf-8", resp.charset) - ensure - ActionDispatch::Response.default_charset = original - end + resp = ActionDispatch::Response.new(200, "Content-Type" => "text/xml") + assert_equal("utf-8", resp.charset) end test "read content type with charset utf-16" do diff --git a/actionpack/test/dispatch/test_response_test.rb b/actionpack/test/dispatch/test_response_test.rb index 2629a61057..f0b8f7785d 100644 --- a/actionpack/test/dispatch/test_response_test.rb +++ b/actionpack/test/dispatch/test_response_test.rb @@ -27,4 +27,11 @@ class TestResponseTest < ActiveSupport::TestCase response = ActionDispatch::TestResponse.create(200, { "Content-Type" => "application/json" }, '{ "foo": "fighters" }') assert_equal({ "foo" => "fighters" }, response.parsed_body) end + + test "response status aliases deprecated" do + response = ActionDispatch::TestResponse.create + assert_deprecated { response.success? } + assert_deprecated { response.missing? } + assert_deprecated { response.error? } + end end diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 66e31978c6..82b0fcbf2e 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -200,7 +200,7 @@ module ActionView end # Returns an HTML image tag for the +source+. The +source+ can be a full - # path or a file. + # path, a file or an Active Storage attachment. # # ==== Options # @@ -217,6 +217,8 @@ module ActionView # # ==== Examples # + # Assets (images that are part of your app): + # # image_tag("icon") # # => <img alt="Icon" src="/assets/icon" /> # image_tag("icon.png") @@ -235,12 +237,21 @@ module ActionView # # => <img src="/assets/icon.png" srcset="/assets/icon_2x.png 2x, /assets/icon_4x.png 4x"> # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw") # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw"> + # + # Active Storage (images that are uploaded by the users of your app): + # + # image_tag(user.avatar) + # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" alt="Tiger" /> + # image_tag(user.avatar.variant(resize: "100x100")) + # # => <img src="/rails/active_storage/variants/.../tiger.jpg" alt="Tiger" /> + # image_tag(user.avatar.variant(resize: "100x100"), size: '100') + # # => <img width="100" height="100" src="/rails/active_storage/variants/.../tiger.jpg" alt="Tiger" /> def image_tag(source, options = {}) options = options.symbolize_keys check_for_image_tag_errors(options) skip_pipeline = options.delete(:skip_pipeline) - src = options[:src] = path_to_image(source, skip_pipeline: skip_pipeline) + src = options[:src] = resolve_image_source(source, skip_pipeline) unless src.start_with?("cid:") || src.start_with?("data:") || src.blank? options[:alt] = options.fetch(:alt) { image_alt(src) } @@ -364,6 +375,16 @@ module ActionView end end + def resolve_image_source(source, skip_pipeline) + if source.is_a?(Symbol) || source.is_a?(String) + path_to_image(source, skip_pipeline: skip_pipeline) + else + polymorphic_url(source) + end + rescue NoMethodError => e + raise ArgumentError, "Can't resolve image into URL: #{e}" + end + def extract_dimensions(size) size = size.to_s if /\A\d+x\d+\z/.match?(size) diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 8b6322f8ad..082ea85aba 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1569,7 +1569,7 @@ module ActionView # In the above block, a +FormBuilder+ object is yielded as the # +person_form+ variable. This allows you to generate the +text_field+ # and +check_box+ fields by specifying their eponymous methods, which - # modify the underlying template and associates the +@person+ model object + # modify the underlying template and associates the <tt>@person</tt> model object # with the form. # # The +FormBuilder+ object can be thought of as serving as a proxy for the diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index f548fc24ed..f2edcb750c 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -355,7 +355,7 @@ module ActionView # finds the options and details and extracts them. The method also contains # logic that handles the type of object passed in as the partial. # - # If +options[:partial]+ is a string, then the +@path+ instance variable is + # If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is # set to that string. Otherwise, the +options[:partial]+ object must # respond to +to_partial_path+ in order to setup the path. def setup(context, options, block) diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 4d312d3b6d..5b8e50cc5e 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -565,9 +565,6 @@ class AssetTagHelperTest < ActionView::TestCase def blank?; true; end def to_s; "no-image-yet.png"; end end - def test_image_tag_with_blank_placeholder - assert_equal '<img alt="" src="/images/no-image-yet.png" />', image_tag(PlaceholderImage.new, alt: "") - end def test_image_path_with_blank_placeholder assert_equal "/images/no-image-yet.png", image_path(PlaceholderImage.new) end diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb index d1473bd792..fe396998a3 100644 --- a/activemodel/lib/active_model/type/integer.rb +++ b/activemodel/lib/active_model/type/integer.rb @@ -6,7 +6,7 @@ module ActiveModel include Helpers::Numeric # Column storage size in bytes. - # 4 bytes means a MySQL int or Postgres integer as opposed to smallint etc. + # 4 bytes means an integer as opposed to smallint etc. DEFAULT_LIMIT = 4 def initialize(*) diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 8181d67816..c1c511a65c 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -9,7 +9,7 @@ *Sean Griffin* -* ApplicationRecord is no longer generated when generating models. If you +* `ApplicationRecord` is no longer generated when generating models. If you need to generate it, it can be created with `rails g application_record`. *Lisa Ugray* @@ -83,7 +83,7 @@ * Fix transactions to apply state to child transactions - Previously if you had a nested transaction and the outer transaction was rolledback the record from the + Previously, if you had a nested transaction and the outer transaction was rolledback, the record from the inner transaction would still be marked as persisted. This change fixes that by applying the state of the parent transaction to the child transaction when the @@ -149,7 +149,7 @@ *Kir Shatrov* -* Prevent making bind param if casted value is nil. +* Prevent creation of bind param if casted value is nil. *Ryuta Kamizono* @@ -210,7 +210,7 @@ *bogdanvlviv* -* When calling the dynamic fixture accessor method with no arguments it now returns all fixtures of this type. +* When calling the dynamic fixture accessor method with no arguments, it now returns all fixtures of this type. Previously this method always returned an empty array. *Kevin McPhillips* diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 840f71bef2..a61c0336db 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -349,6 +349,7 @@ module ActiveRecord # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X # create_other!(attributes={}) | X | | X + # reload_other | X | X | X # # === Collection associations (one-to-many / many-to-many) # | | | has_many @@ -378,6 +379,7 @@ module ActiveRecord # others.exists? | X | X | X # others.distinct | X | X | X # others.reset | X | X | X + # others.reload | X | X | X # # === Overriding generated methods # @@ -1190,8 +1192,8 @@ module ActiveRecord # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. # # [collection] - # Returns an array of all the associated objects. - # An empty array is returned if none are found. + # Returns a Relation of all the associated objects. + # An empty Relation is returned if none are found. # [collection<<(object, ...)] # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. # Note that this operation instantly fires update SQL without waiting for the save or update call on the @@ -1248,6 +1250,9 @@ module ActiveRecord # [collection.create!(attributes = {})] # Does the same as <tt>collection.create</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. + # [collection.reload] + # Returns a Relation of all of the associated objects, forcing a database read. + # An empty Relation is returned if none are found. # # === Example # @@ -1267,6 +1272,7 @@ module ActiveRecord # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) + # * <tt>Firm#clients.reload</tt> # The declaration can also include an +options+ hash to specialize the behavior of the association. # # === Scopes @@ -1426,6 +1432,8 @@ module ActiveRecord # [create_association!(attributes = {})] # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. + # [reload_association] + # Returns the associated object, forcing a database read. # # === Example # @@ -1435,6 +1443,7 @@ module ActiveRecord # * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>) # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) + # * <tt>Account#reload_beneficiary</tt> # # === Scopes # @@ -1555,6 +1564,8 @@ module ActiveRecord # [create_association!(attributes = {})] # Does the same as <tt>create_association</tt>, but raises ActiveRecord::RecordInvalid # if the record is invalid. + # [reload_association] + # Returns the associated object, forcing a database read. # # === Example # @@ -1564,6 +1575,7 @@ module ActiveRecord # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) + # * <tt>Post#reload_author</tt> # The declaration can also include an +options+ hash to specialize the behavior of the association. # # === Scopes @@ -1704,8 +1716,8 @@ module ActiveRecord # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. # # [collection] - # Returns an array of all the associated objects. - # An empty array is returned if none are found. + # Returns a Relation of all the associated objects. + # An empty Relation is returned if none are found. # [collection<<(object, ...)] # Adds one or more objects to the collection by creating associations in the join table # (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method). @@ -1743,6 +1755,9 @@ module ActiveRecord # Returns a new object of the collection type that has been instantiated # with +attributes+, linked to this object through the join table, and that has already been # saved (if it passed the validation). + # [collection.reload] + # Returns a Relation of all of the associated objects, forcing a database read. + # An empty Relation is returned if none are found. # # === Example # @@ -1761,6 +1776,7 @@ module ActiveRecord # * <tt>Developer#projects.exists?(...)</tt> # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) + # * <tt>Developer#projects.reload</tt> # The declaration may include an +options+ hash to specialize the behavior of the association. # # === Scopes diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 9b0b50977d..3d79e540b8 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -68,11 +68,11 @@ module ActiveRecord foreign_key = join_keys.foreign_key value = transform_value(owner[foreign_key]) - scope = scope.where(table.name => { key => value }) + scope = apply_scope(scope, table, key, value) if reflection.type polymorphic_type = transform_value(owner.class.base_class.name) - scope = scope.where(table.name => { reflection.type => polymorphic_type }) + scope = apply_scope(scope, table, reflection.type, polymorphic_type) end scope @@ -91,10 +91,10 @@ module ActiveRecord if reflection.type value = transform_value(next_reflection.klass.base_class.name) - scope = scope.where(table.name => { reflection.type => value }) + scope = apply_scope(scope, table, reflection.type, value) end - scope = scope.joins(join(foreign_table, constraint)) + scope.joins!(join(foreign_table, constraint)) end class ReflectionProxy < SimpleDelegator # :nodoc: @@ -165,6 +165,14 @@ module ActiveRecord scope end + def apply_scope(scope, table, key, value) + if scope.table == table + scope.where!(key => value) + else + scope.where!(table.name => { key => value }) + end + end + def eval_scope(reflection, table, scope, owner) reflection.build_scope(table).instance_exec(owner, &scope) end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 0001b804a8..1a424896fe 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -59,7 +59,9 @@ module ActiveRecord r.send(reflection.association_primary_key) end.values_at(*ids).compact if records.size != ids.size - klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, reflection.association_primary_key) + found_ids = records.map { |record| record.send(reflection.association_primary_key) } + not_found_ids = ids - found_ids + klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, reflection.association_primary_key, not_found_ids) else replace(records) end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 5efe051125..48d33e6744 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -5,7 +5,7 @@ require_relative "../attribute_mutation_tracker" module ActiveRecord module AttributeMethods - module Dirty # :nodoc: + module Dirty extend ActiveSupport::Concern include ActiveModel::Dirty @@ -47,7 +47,7 @@ module ActiveRecord clear_mutation_trackers end - def changes_applied + def changes_applied # :nodoc: @mutations_before_last_save = mutation_tracker @mutations_from_database = AttributeMutationTracker.new(@attributes) @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new @@ -55,27 +55,27 @@ module ActiveRecord clear_mutation_trackers end - def clear_changes_information + def clear_changes_information # :nodoc: @mutations_before_last_save = nil @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments clear_mutation_trackers end - def write_attribute_without_type_cast(attr_name, *) + def write_attribute_without_type_cast(attr_name, *) # :nodoc: result = super clear_attribute_change(attr_name) result end - def clear_attribute_changes(attr_names) + def clear_attribute_changes(attr_names) # :nodoc: super attr_names.each do |attr_name| clear_attribute_change(attr_name) end end - def changed_attributes + def changed_attributes # :nodoc: # This should only be set by methods which will call changed_attributes # multiple times when it is known that the computed value cannot change. if defined?(@cached_changed_attributes) @@ -85,17 +85,17 @@ module ActiveRecord end end - def changes + def changes # :nodoc: cache_changed_attributes do super end end - def previous_changes + def previous_changes # :nodoc: mutations_before_last_save.changes end - def attribute_changed_in_place?(attr_name) + def attribute_changed_in_place?(attr_name) # :nodoc: mutation_tracker.changed_in_place?(attr_name) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index c11b7b012f..0759f4d2b3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -270,7 +270,7 @@ module ActiveRecord # Connections must be leased while holding the main pool mutex. This is # an internal subclass that also +.leases+ returned connections while # still in queue's critical section (queue synchronizes with the same - # +@lock+ as the main pool) so that a returned connection is already + # <tt>@lock</tt> as the main pool) so that a returned connection is already # leased and there is no need to re-enter synchronized block. class ConnectionLeasingQueue < Queue # :nodoc: include BiasableQueue @@ -326,8 +326,6 @@ module ActiveRecord @spec = spec @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5 - @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f)) - @reaper.run # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 @@ -340,7 +338,7 @@ module ActiveRecord # then that +thread+ does indeed own that +conn+. However, an absence of a such # mapping does not mean that the +thread+ doesn't own the said connection. In # that case +conn.owner+ attr should be consulted. - # Access and modification of +@thread_cached_conns+ does not require + # Access and modification of <tt>@thread_cached_conns</tt> does not require # synchronization. @thread_cached_conns = Concurrent::Map.new(initial_capacity: @size) @@ -357,6 +355,9 @@ module ActiveRecord @available = ConnectionLeasingQueue.new self @lock_thread = false + + @reaper = Reaper.new(self, spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f) + @reaper.run end def lock_thread=(lock_thread) @@ -736,10 +737,10 @@ module ActiveRecord # Implementation detail: the connection returned by +acquire_connection+ # will already be "+connection.lease+ -ed" to the current thread. def acquire_connection(checkout_timeout) - # NOTE: we rely on +@available.poll+ and +try_to_checkout_new_connection+ to + # NOTE: we rely on <tt>@available.poll</tt> and +try_to_checkout_new_connection+ to # +conn.lease+ the returned connection (and to do this in a +synchronized+ # section). This is not the cleanest implementation, as ideally we would - # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to +@available.poll+ + # <tt>synchronize { conn.lease }</tt> in this method, but by leaving it to <tt>@available.poll</tt> # and +try_to_checkout_new_connection+ we can piggyback on +synchronize+ sections # of the said methods and avoid an additional +synchronize+ overhead. if conn = @available.poll || try_to_checkout_new_connection @@ -763,7 +764,7 @@ module ActiveRecord end end - # If the pool is not at a +@size+ limit, establish new connection. Connecting + # If the pool is not at a <tt>@size</tt> limit, establish new connection. Connecting # to the DB is done outside main synchronized section. #-- # Implementation constraint: a newly established connection returned by this diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index ecf5201d12..41d93c4322 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "concurrent/map" + module ActiveRecord module ConnectionAdapters # :nodoc: module QueryCache diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index e21f93856e..3c9b25e411 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -780,7 +780,7 @@ module ActiveRecord def rename_index(table_name, old_name, new_name) validate_index_length!(table_name, new_name) - # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) + # this is a naive implementation; some DBs may support this more efficiently (PostgreSQL, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } return unless old_index_def add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique) diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 933589d4b1..e790760292 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -169,7 +169,7 @@ module ActiveRecord class NoDatabaseError < StatementInvalid end - # Raised when Postgres returns 'cached plan must not change result type' and + # Raised when PostgreSQL returns 'cached plan must not change result type' and # we cannot retry gracefully (e.g. inside a transaction) class PreparedStatementCacheExpired < StatementInvalid end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index d6595c9355..784292f3f9 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -39,7 +39,7 @@ module ActiveRecord end end - # Since 5.1 Postgres adapter uses bigserial type for primary + # Since 5.1 PostgreSQL adapter uses bigserial type for primary # keys by default and MySQL uses bigint. This compat layer makes old migrations utilize # serial/int type instead -- the way it used to work before 5.1. unless options.key?(:id) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 626c50470e..5da9573052 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -330,7 +330,7 @@ module ActiveRecord # of results obtained should be provided in the +result_size+ argument and # the expected number of results should be provided in the +expected_size+ # argument. - def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key) # :nodoc: + def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil, key = primary_key, not_found_ids = nil) # :nodoc: conditions = arel.where_sql(@klass) conditions = " [#{conditions}]" if conditions name = @klass.name @@ -344,8 +344,8 @@ module ActiveRecord raise RecordNotFound.new(error, name, key, ids) else error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup - error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" - + error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})." + error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids raise RecordNotFound.new(error, name, primary_key, ids) end end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index a2e74efc2b..5681ccdd23 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -22,7 +22,7 @@ module ActiveRecord configuration.merge("encoding" => encoding) establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if /database .* already exists/.match?(error.message) + if error.cause.is_a?(PG::DuplicateDatabase) raise DatabaseAlreadyExists else raise @@ -136,7 +136,7 @@ module ActiveRecord ensure tempfile.close end - FileUtils.mv(tempfile.path, filename) + FileUtils.cp(tempfile.path, filename) end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index f91f0cdf12..b2f5e39e09 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -472,7 +472,7 @@ module ActiveRecord # if it's associated with a transaction, then the state of the Active Record # object will be updated to reflect the current state of the transaction. # - # The +@transaction_state+ variable stores the states of the associated + # The <tt>@transaction_state</tt> variable stores the states of the associated # transaction. This relies on the fact that a transaction can only be in # one rollback or commit (otherwise a list of states would be required). # Each Active Record object inside of a transaction carries that transaction's diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 5797ffef38..4c2723addc 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -817,7 +817,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase sarah = Person.create!(first_name: "Sarah", primary_contact_id: people(:susan).id, gender: "F", number1_fan_id: 1) john = Person.create!(first_name: "John", primary_contact_id: sarah.id, gender: "M", number1_fan_id: 1) assert_equal sarah.agents, [john] - assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents + assert_equal people(:susan).agents.flat_map(&:agents).sort, people(:susan).agents_of_agents.sort end def test_associate_existing_with_nonstandard_primary_key_on_belongs_to @@ -891,7 +891,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase company = companies(:rails_core) ids = [Developer.first.id, -9999] e = assert_raises(ActiveRecord::RecordNotFound) { company.developer_ids = ids } - assert_match(/Couldn't find all Developers with 'id'/, e.message) + msg = "Couldn't find all Developers with 'id': (1, -9999) (found 1 results, but was looking for 2). Couldn't find Developer with id -9999." + assert_equal(msg, e.message) end def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set_with_changed_primary_key @@ -905,7 +906,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase author = authors(:david) ids = [categories(:general).name, "Unknown"] e = assert_raises(ActiveRecord::RecordNotFound) { author.essay_category_ids = ids } - assert_equal "Couldn't find all Categories with 'name': (General, Unknown) (found 1 results, but was looking for 2)", e.message + msg = "Couldn't find all Categories with 'name': (General, Unknown) (found 1 results, but was looking for 2). Couldn't find Category with name Unknown." + assert_equal msg, e.message end def test_build_a_model_from_hm_through_association_with_where_clause diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 2f68bc5141..9d3d5353ff 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -404,7 +404,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_one - assert_equal Tagging.find(1, 2).sort_by(&:id), authors(:david).taggings_2 + assert_equal Tagging.find(1, 2).sort_by(&:id), authors(:david).taggings_2.sort_by(&:id) end def test_has_many_through_polymorphic_has_many diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index fb3d691d51..a602f83d8c 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -466,11 +466,10 @@ class DirtyTest < ActiveRecord::TestCase def test_save_should_not_save_serialized_attribute_with_partial_writes_if_not_present with_partial_writes(Topic) do - Topic.create!(author_name: "Bill", content: { a: "a" }) - topic = Topic.select("id, author_name").first + topic = Topic.create!(author_name: "Bill", content: { a: "a" }) + topic = Topic.select("id, author_name").find(topic.id) topic.update_columns author_name: "John" - topic = Topic.first - assert_not_nil topic.content + assert_not_nil topic.reload.content end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index a0d87dfb90..55496147c1 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1153,7 +1153,7 @@ class FinderTest < ActiveRecord::TestCase e = assert_raises(ActiveRecord::RecordNotFound) do model.find "Hello", "World!" end - assert_equal "Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2)", e.message + assert_equal "Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2).", e.message end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 05420a4240..47749c07d2 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -192,6 +192,7 @@ ActiveRecord::Schema.define do t.string :resource_type t.integer :developer_id t.datetime :deleted_at + t.integer :comments end create_table :companies, force: true do |t| diff --git a/activestorage/README.md b/activestorage/README.md index 06594a3ecb..d4ffe0c484 100644 --- a/activestorage/README.md +++ b/activestorage/README.md @@ -2,7 +2,7 @@ Active Storage makes it simple to upload and reference files in cloud services, like Amazon S3, Google Cloud Storage or Microsoft Azure Storage and attach those files to Active Records. It also provides a disk service for testing or local deployments, but the focus is on cloud storage. -Files can uploaded from the server to the cloud or directly from the client to the cloud. +Files can be uploaded from the server to the cloud or directly from the client to the cloud. Image files can further more be transformed using on-demand variants for quality, aspect ratio, size, or any other MiniMagick supported transformation. @@ -80,17 +80,9 @@ Variation of image attachment: ```erb <%# Hitting the variant URL will lazy transform the original blob and then redirect to its new service location %> -<%= image_tag url_for(user.avatar.variant(resize: "100x100")) %> +<%= image_tag user.avatar.variant(resize: "100x100") %> ``` -## Installation - -1. Run `rails activestorage:install` to create needed directories, migrations, and configuration. -2. Optional: Add `gem "aws-sdk", "~> 2"` to your Gemfile if you want to use AWS S3. -3. Optional: Add `gem "google-cloud-storage", "~> 1.3"` to your Gemfile if you want to use Google Cloud Storage. -4. Optional: Add `gem "azure-storage"` to your Gemfile if you want to use Microsoft Azure. -5. Optional: Add `gem "mini_magick"` to your Gemfile if you want to use variants. - ## Direct uploads Active Storage, with its included JavaScript library, supports uploading directly from the client to the cloud. @@ -119,7 +111,7 @@ Active Storage, with its included JavaScript library, supports uploading directl | Event name | Event target | Event data (`event.detail`) | Description | | --- | --- | --- | --- | -| `direct-uploads:start` | `<form>` | None | A form containing files for direct upload fields was submit. | +| `direct-uploads:start` | `<form>` | None | A form containing files for direct upload fields was submitted. | | `direct-upload:initialize` | `<input>` | `{id, file}` | Dispatched for every file after form submission. | | `direct-upload:start` | `<input>` | `{id, file}` | A direct upload is starting. | | `direct-upload:before-blob-request` | `<input>` | `{id, file, xhr}` | Before making a request to your application for direct upload metadata. | diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb index 05af29f8b2..cff88bd488 100644 --- a/activestorage/app/controllers/active_storage/blobs_controller.rb +++ b/activestorage/app/controllers/active_storage/blobs_controller.rb @@ -5,6 +5,7 @@ class ActiveStorage::BlobsController < ActionController::Base def show if blob = find_signed_blob + expires_in 5.minutes # service_url defaults to 5 minutes redirect_to blob.service_url(disposition: disposition_param) else head :not_found diff --git a/activestorage/app/controllers/active_storage/variants_controller.rb b/activestorage/app/controllers/active_storage/variants_controller.rb index 994c57aafd..b72b0ff7f5 100644 --- a/activestorage/app/controllers/active_storage/variants_controller.rb +++ b/activestorage/app/controllers/active_storage/variants_controller.rb @@ -5,6 +5,7 @@ class ActiveStorage::VariantsController < ActionController::Base def show if blob = find_signed_blob + expires_in 5.minutes # service_url defaults to 5 minutes redirect_to ActiveStorage::Variant.new(blob, decoded_variation).processed.service_url(disposition: disposition_param) else head :not_found diff --git a/activestorage/app/jobs/active_storage/purge_job.rb b/activestorage/app/jobs/active_storage/purge_job.rb index 815f908e6c..b504ee0df4 100644 --- a/activestorage/app/jobs/active_storage/purge_job.rb +++ b/activestorage/app/jobs/active_storage/purge_job.rb @@ -1,4 +1,4 @@ -# Provides delayed purging of attachments or blobs using their `#purge_later` method. +# Provides delayed purging of attachments or blobs using their +#purge_later+ method. class ActiveStorage::PurgeJob < ActiveJob::Base # FIXME: Limit this to a custom ActiveStorage error retry_on StandardError diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index e94fc69bba..07b5733ff8 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -21,7 +21,7 @@ class ActiveStorage::Attachment < ActiveRecord::Base # Purging an attachment means purging the blob, which means talking to the service, which means # talking over the internet. Whenever you're doing that, it's a good idea to put that work in a job, - # so it doesn't hold up other operations. That's what #purge_later provides. + # so it doesn't hold up other operations. That's what +#purge_later+ provides. def purge_later ActiveStorage::PurgeJob.perform_later(self) end diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb index debc62bd41..113a7f774d 100644 --- a/activestorage/app/models/active_storage/blob.rb +++ b/activestorage/app/models/active_storage/blob.rb @@ -29,7 +29,7 @@ class ActiveStorage::Blob < ActiveRecord::Base find ActiveStorage.verifier.verify(id, purpose: :blob_id) end - # Returns a new, unsaved blob instance after the `io` has been uploaded to the service. + # Returns a new, unsaved blob instance after the +io+ has been uploaded to the service. def build_after_upload(io:, filename:, content_type: nil, metadata: nil) new.tap do |blob| blob.filename = filename @@ -40,8 +40,8 @@ class ActiveStorage::Blob < ActiveRecord::Base end end - # Returns a saved blob instance after the `io` has been uploaded to the service. Note, the blob is first built, - # then the `io` is uploaded, then the blob is saved. This is doing to avoid opening a transaction and talking to + # Returns a saved blob instance after the +io+ has been uploaded to the service. Note, the blob is first built, + # then the +io+ is uploaded, then the blob is saved. This is doing to avoid opening a transaction and talking to # the service during that (which is a bad idea and leads to deadlocks). 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!) @@ -72,7 +72,7 @@ class ActiveStorage::Blob < ActiveRecord::Base self[:key] ||= self.class.generate_unique_secure_token end - # Returns a `ActiveStorage::Filename` instance of the filename that can be queried for basename, extension, and + # Returns a ActiveStorage::Filename instance of the filename that can be queried for basename, extension, and # a sanitized version of the filename that's safe to use in URLs. def filename ActiveStorage::Filename.new(self[:filename]) @@ -80,25 +80,25 @@ class ActiveStorage::Blob < ActiveRecord::Base # Returns true if the content_type of this blob is in the image range, like image/png. def image? - content_type =~ /^image/ + content_type.start_with?("image") end # Returns true if the content_type of this blob is in the audio range, like audio/mpeg. def audio? - content_type =~ /^audio/ + content_type.start_with?("audio") end # Returns true if the content_type of this blob is in the video range, like video/mp4. def video? - content_type =~ /^video/ + content_type.start_with?("video") end # Returns true if the content_type of this blob is in the text range, like text/plain. def text? - content_type =~ /^text/ + content_type.start_with?("text") end - # Returns a `ActiveStorage::Variant` instance with the set of `transformations` passed in. This is only relevant + # Returns a ActiveStorage::Variant instance with the set of +transformations+ passed in. This is only relevant # for image files, and it allows any image to be transformed for size, colors, and the like. Example: # # avatar.variant(resize: "100x100").processed.service_url @@ -111,7 +111,7 @@ class ActiveStorage::Blob < ActiveRecord::Base # # <%= image_tag url_for(Current.user.avatar.variant(resize: "100x100")) %> # - # This will create a URL for that specific blob with that specific variant, which the `ActiveStorage::VariantsController` + # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController # can then produce on-demand. def variant(transformations) ActiveStorage::Variant.new(self, ActiveStorage::Variation.new(transformations)) @@ -119,9 +119,9 @@ class ActiveStorage::Blob < ActiveRecord::Base # Returns the URL of the blob on the service. This URL is intended to be short-lived for security and not used directly - # with users. Instead, the `service_url` should only be exposed as a redirect from a stable, possibly authenticated URL. - # Hiding the `service_url` behind a redirect also gives you the power to change services without updating all URLs. And - # it allows permanent URLs that redirect to the `service_url` to be cached in the view. + # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL. + # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And + # it allows permanent URLs that redirect to the +service_url+ to be cached in the view. def service_url(expires_in: 5.minutes, disposition: :inline) service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type end @@ -132,21 +132,21 @@ class ActiveStorage::Blob < ActiveRecord::Base service.url_for_direct_upload key, expires_in: expires_in, content_type: content_type, content_length: byte_size, checksum: checksum end - # Returns a Hash of headers for `service_url_for_direct_upload` requests. + # Returns a Hash of headers for +service_url_for_direct_upload+ requests. def service_headers_for_direct_upload service.headers_for_direct_upload key, filename: filename, content_type: content_type, content_length: byte_size, checksum: checksum end - # Uploads the `io` to the service on the `key` for this blob. Blobs are intended to be immutable, so you shouldn't be + # Uploads the +io+ to the service on the +key+ for this blob. Blobs are intended to be immutable, so you shouldn't be # using this method after a file has already been uploaded to fit with a blob. If you want to create a derivative blob, # you should instead simply create a new blob based on the old one. # # Prior to uploading, we compute the checksum, which is sent to the service for transit integrity validation. If the - # checksum does not match what the service receives, an exception will be raised. We also measure the size of the `io` - # and store that in `byte_size` on the blob record. + # checksum does not match what the service receives, an exception will be raised. We also measure the size of the +io+ + # and store that in +byte_size+ on the blob record. # - # Normally, you do not have to call this method directly at all. Use the factory class methods of `build_after_upload` - # and `create_after_upload!`. + # Normally, you do not have to call this method directly at all. Use the factory class methods of +build_after_upload+ + # and +create_after_upload!+. def upload(io) self.checksum = compute_checksum_in_chunks(io) self.byte_size = io.size @@ -162,7 +162,7 @@ class ActiveStorage::Blob < ActiveRecord::Base # Deletes the file on the service that's associated with this blob. This should only be done if the blob is going to be - # deleted as well or you will essentially have a dead reference. It's recommended to use the `#purge` and `#purge_later` + # deleted as well or you will essentially have a dead reference. It's recommended to use the +#purge+ and +#purge_later+ # methods in most circumstances. def delete service.delete key @@ -170,13 +170,13 @@ class ActiveStorage::Blob < ActiveRecord::Base # Deletes the file on the service and then destroys the blob record. This is the recommended way to dispose of unwanted # blobs. Note, though, that deleting the file off the service will initiate a HTTP connection to the service, which may - # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use `#purge_later` instead. + # be slow or prevented, so you should not use this method inside a transaction or in callbacks. Use +#purge_later+ instead. def purge delete destroy end - # Enqueues a `ActiveStorage::PurgeJob` job that'll call `#purge`. This is the recommended way to purge blobs when the call + # Enqueues a ActiveStorage::PurgeJob job that'll call +#purge+. This is the recommended way to purge blobs when the call # needs to be made from a transaction, a callback, or any other real-time scenario. def purge_later ActiveStorage::PurgeJob.perform_later(self) diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index e3f22bcb25..b9b93b4c1b 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -9,17 +9,17 @@ # into memory. The larger the image, the more memory is used. Because of this process, you also want to be # considerate about when the variant is actually processed. You shouldn't be processing variants inline in a # template, for example. Delay the processing to an on-demand controller, like the one provided in -# `ActiveStorage::VariantsController`. +# ActiveStorage::VariantsController. # # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided # by Active Storage like so: # # <%= image_tag url_for(Current.user.avatar.variant(resize: "100x100")) %> # -# This will create a URL for that specific blob with that specific variant, which the `ActiveStorage::VariantsController` +# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController # can then produce on-demand. # -# When you do want to actually produce the variant needed, call `#processed`. This will check that the variant +# When you do want to actually produce the variant needed, call +#processed+. This will check that the variant # has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform # the transformations, upload the variant to the service, and return itself again. Example: # @@ -52,12 +52,12 @@ class ActiveStorage::Variant end # Returns the URL of the variant on the service. This URL is intended to be short-lived for security and not used directly - # with users. Instead, the `service_url` should only be exposed as a redirect from a stable, possibly authenticated URL. - # Hiding the `service_url` behind a redirect also gives you the power to change services without updating all URLs. And - # it allows permanent URLs that redirec to the `service_url` to be cached in the view. + # with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL. + # Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And + # it allows permanent URLs that redirect to the +service_url+ to be cached in the view. # # Use `url_for(variant)` (or the implied form, like `link_to variant` or `redirect_to variant`) to get the stable URL - # for a variant that points to the `ActiveStorage::VariantsController`, which in turn will use this `#service_call` method + # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +#service_call+ method # for its redirection. def service_url(expires_in: 5.minutes, disposition: :inline) service.url key, expires_in: expires_in, disposition: disposition, filename: blob.filename, content_type: blob.content_type diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb index e784506b4c..24168c064c 100644 --- a/activestorage/app/models/active_storage/variation.rb +++ b/activestorage/app/models/active_storage/variation.rb @@ -13,12 +13,12 @@ class ActiveStorage::Variation attr_reader :transformations class << self - # Returns a variation instance with the transformations that were encoded by `#encode`. + # Returns a variation instance with the transformations that were encoded by +#encode+. def decode(key) new ActiveStorage.verifier.verify(key, purpose: :variation) end - # Returns a signed key for the `transformations`, which can be used to refer to a specific + # Returns a signed key for the +transformations+, which can be used to refer to a specific # variation in a URL or combined key (like `ActiveStorage::Variant#key`). def encode(transformations) ActiveStorage.verifier.generate(transformations, purpose: :variation) @@ -30,7 +30,7 @@ class ActiveStorage::Variation end # Accepts an open MiniMagick image instance, like what's return by `MiniMagick::Image.read(io)`, - # and performs the `transformations` against it. The transformed image instance is then returned. + # and performs the +transformations+ against it. The transformed image instance is then returned. def transform(image) transformations.each do |(method, argument)| if eligible_argument?(argument) @@ -41,7 +41,7 @@ class ActiveStorage::Variation end end - # Returns a signed key for all the `transformations` that this variation was instantiated with. + # Returns a signed key for all the +transformations+ that this variation was instantiated with. def key self.class.encode(transformations) end diff --git a/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb new file mode 100644 index 0000000000..2c7e3c5bc6 --- /dev/null +++ b/activestorage/db/migrate/20170806125915_create_active_storage_tables.rb @@ -0,0 +1,27 @@ +class CreateActiveStorageTables < ActiveRecord::Migration[5.1] + def change + create_table :active_storage_blobs do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.integer :byte_size, null: false + t.string :checksum, null: false + t.datetime :created_at, null: false + + t.index [ :key ], unique: true + end + + create_table :active_storage_attachments do |t| + t.string :name, null: false + t.string :record_type, null: false + t.integer :record_id, null: false + t.integer :blob_id, null: false + + t.datetime :created_at, null: false + + t.index :blob_id + t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true + end + end +end diff --git a/activestorage/lib/active_storage/attached.rb b/activestorage/lib/active_storage/attached.rb index 2dbf841864..f11b62e744 100644 --- a/activestorage/lib/active_storage/attached.rb +++ b/activestorage/lib/active_storage/attached.rb @@ -2,33 +2,35 @@ require "action_dispatch" require "action_dispatch/http/upload" require "active_support/core_ext/module/delegation" -# Abstract baseclass for the concrete `ActiveStorage::Attached::One` and `ActiveStorage::Attached::Many` +module ActiveStorage +# Abstract baseclass for the concrete ActiveStorage::Attached::One and ActiveStorage::Attached::Many # classes that both provide proxy access to the blob association for a record. -class ActiveStorage::Attached - attr_reader :name, :record + class Attached + attr_reader :name, :record - def initialize(name, record) - @name, @record = name, record - end + def initialize(name, record) + @name, @record = name, record + end - private - def create_blob_from(attachable) - case attachable - when ActiveStorage::Blob - attachable - when ActionDispatch::Http::UploadedFile - ActiveStorage::Blob.create_after_upload! \ - io: attachable.open, - filename: attachable.original_filename, - content_type: attachable.content_type - when Hash - ActiveStorage::Blob.create_after_upload!(attachable) - when String - ActiveStorage::Blob.find_signed(attachable) - else - nil + private + def create_blob_from(attachable) + case attachable + when ActiveStorage::Blob + attachable + when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile + ActiveStorage::Blob.create_after_upload! \ + io: attachable.open, + filename: attachable.original_filename, + content_type: attachable.content_type + when Hash + ActiveStorage::Blob.create_after_upload!(attachable) + when String + ActiveStorage::Blob.find_signed(attachable) + else + nil + end end - end + end end require "active_storage/attached/one" diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb index 89297e5bdf..eb877f10c0 100644 --- a/activestorage/lib/active_storage/attached/macros.rb +++ b/activestorage/lib/active_storage/attached/macros.rb @@ -1,76 +1,80 @@ -# Provides the class-level DSL for declaring that an Active Record model has attached blobs. -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. - # - # Under the covers, this relationship is implemented as a `has_one` association to a - # `ActiveStorage::Attachment` record and a `has_one-through` association to a - # `ActiveStorage::Blob` record. These associations are available as `avatar_attachment` - # and `avatar_blob`. But you shouldn't need to work with these associations directly in - # most circumstances. - # - # The system has been designed to having you go through the `ActiveStorage::Attached::One` - # proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`. - # - # 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_set("@active_storage_attached_#{name}", ActiveStorage::Attached::One.new(name, self)) - end +module ActiveStorage + # Provides the class-level DSL for declaring that an Active Record model has attached blobs. + module 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. + # + # Under the covers, this relationship is implemented as a +has_one+ association to a + # ActiveStorage::Attachment record and a +has_one-through+ association to a + # ActiveStorage::Blob record. These associations are available as +avatar_attachment+ + # and +avatar_blob+. But you shouldn't need to work with these associations directly in + # most circumstances. + # + # The system has been designed to having you go through the ActiveStorage::Attached::One + # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+. + # + # 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) + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name} + @active_storage_attached_#{name} ||= ActiveStorage::Attached::One.new("#{name}", self) + end + CODE - has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record - has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob + has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record + has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob - if dependent == :purge_later - before_destroy { public_send(name).purge_later } + if dependent == :purge_later + before_destroy { public_send(name).purge_later } + end 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. - # - # To avoid N+1 queries, you can include the attached blobs in your query like so: - # - # Gallery.where(user: Current.user).with_attached_photos - # - # Under the covers, this relationship is implemented as a `has_many` association to a - # `ActiveStorage::Attachment` record and a `has_many-through` association to a - # `ActiveStorage::Blob` record. These associations are available as `photos_attachments` - # and `photos_blobs`. But you shouldn't need to work with these associations directly in - # most circumstances. - # - # The system has been designed to having you go through the `ActiveStorage::Attached::Many` - # proxy that provides the dynamic proxy to the associations and factory methods, like `#attach`. - # - # 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_set("@active_storage_attached_#{name}", ActiveStorage::Attached::Many.new(name, self)) - 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. + # + # To avoid N+1 queries, you can include the attached blobs in your query like so: + # + # Gallery.where(user: Current.user).with_attached_photos + # + # Under the covers, this relationship is implemented as a +has_many+ association to a + # ActiveStorage::Attachment record and a +has_many-through+ association to a + # ActiveStorage::Blob record. These associations are available as +photos_attachments+ + # and +photos_blobs+. But you shouldn't need to work with these associations directly in + # most circumstances. + # + # The system has been designed to having you go through the ActiveStorage::Attached::Many + # proxy that provides the dynamic proxy to the associations and factory methods, like +#attach+. + # + # 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) + class_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{name} + @active_storage_attached_#{name} ||= ActiveStorage::Attached::Many.new("#{name}", self) + end + CODE - has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment" - has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob + has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment" + has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob - scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } + scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } - if dependent == :purge_later - before_destroy { public_send(name).purge_later } + if dependent == :purge_later + before_destroy { public_send(name).purge_later } + end end end end diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb index 035cd9c091..cc3e70ffe2 100644 --- a/activestorage/lib/active_storage/attached/many.rb +++ b/activestorage/lib/active_storage/attached/many.rb @@ -1,51 +1,53 @@ -# Decorated proxy object representing of multiple attachments to a model. -class ActiveStorage::Attached::Many < ActiveStorage::Attached - delegate_missing_to :attachments +module ActiveStorage + # Decorated proxy object representing of multiple attachments to a model. + class Attached::Many < Attached + delegate_missing_to :attachments - # Returns all the associated attachment records. - # - # All methods called on this proxy object that aren't listed here will automatically be delegated to `attachments`. - def attachments - record.public_send("#{name}_attachments") - end + # Returns all the associated attachment records. + # + # All methods called on this proxy object that aren't listed here will automatically be delegated to +attachments+. + def attachments + record.public_send("#{name}_attachments") + end - # Associates one or several attachments with the current record, saving them to the database. - # Examples: - # - # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects - # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload - # document.images.attach(io: File.open("~/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg") - # document.images.attach([ first_blob, second_blob ]) - def attach(*attachables) - attachables.flatten.collect do |attachable| - attachments.create!(name: name, blob: create_blob_from(attachable)) + # Associates one or several attachments with the current record, saving them to the database. + # Examples: + # + # document.images.attach(params[:images]) # Array of ActionDispatch::Http::UploadedFile objects + # document.images.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload + # document.images.attach(io: File.open("~/racecar.jpg"), filename: "racecar.jpg", content_type: "image/jpg") + # document.images.attach([ first_blob, second_blob ]) + def attach(*attachables) + attachables.flatten.collect do |attachable| + attachments.create!(name: name, blob: create_blob_from(attachable)) + end end - end - # Returns true if any attachments has been made. - # - # class Gallery < ActiveRecord::Base - # has_many_attached :photos - # end - # - # Gallery.new.photos.attached? # => false - def attached? - attachments.any? - end + # Returns true if any attachments has been made. + # + # 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) - attachments.reload + # 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 - end - # Purges each associated attachment through the queuing system. - def purge_later - if attached? - attachments.each(&: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 0c522e856e..6b34b30f1c 100644 --- a/activestorage/lib/active_storage/attached/one.rb +++ b/activestorage/lib/active_storage/attached/one.rb @@ -1,56 +1,58 @@ -# Representation of a single attachment to a model. -class ActiveStorage::Attached::One < ActiveStorage::Attached - delegate_missing_to :attachment +module ActiveStorage + # Representation of a single attachment to a model. + class Attached::One < 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 - record.public_send("#{name}_attachment") - end - - # Associates a given attachment with the current record, saving it to the database. - # Examples: - # - # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object - # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload - # person.avatar.attach(io: File.open("~/face.jpg"), filename: "face.jpg", content_type: "image/jpg") - # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object - def attach(attachable) - write_attachment \ - ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable)) - end + # 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 + record.public_send("#{name}_attachment") + end - # Returns true if an attachment has been made. - # - # class User < ActiveRecord::Base - # has_one_attached :avatar - # end - # - # User.new.avatar.attached? # => false - def attached? - attachment.present? - end + # Associates a given attachment with the current record, saving it to the database. + # Examples: + # + # person.avatar.attach(params[:avatar]) # ActionDispatch::Http::UploadedFile object + # person.avatar.attach(params[:signed_blob_id]) # Signed reference to blob from direct upload + # person.avatar.attach(io: File.open("~/face.jpg"), filename: "face.jpg", content_type: "image/jpg") + # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object + def attach(attachable) + write_attachment \ + ActiveStorage::Attachment.create!(record: record, name: name, blob: create_blob_from(attachable)) + 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 - write_attachment nil + # Returns true if an attachment has been made. + # + # class User < ActiveRecord::Base + # has_one_attached :avatar + # end + # + # User.new.avatar.attached? # => false + def attached? + attachment.present? end - end - # Purges the attachment through the queuing system. - def purge_later - if attached? - attachment.purge_later + # Directly purges the attachment (i.e. destroys the blob and + # attachment and deletes the file on the service). + def purge + if attached? + attachment.purge + write_attachment nil + end end - end - private - def write_attachment(attachment) - record.public_send("#{name}_attachment=", attachment) + # Purges the attachment through the queuing system. + def purge_later + if attached? + attachment.purge_later + end end + + private + def write_attachment(attachment) + record.public_send("#{name}_attachment=", attachment) + end + end end diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 1d345920fa..a5562b32d3 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -5,6 +5,8 @@ require "active_storage" module ActiveStorage class Engine < Rails::Engine # :nodoc: + isolate_namespace ActiveStorage + config.active_storage = ActiveSupport::OrderedOptions.new config.eager_load_namespaces << ActiveStorage @@ -27,29 +29,28 @@ module ActiveStorage initializer "active_storage.verifier" do config.after_initialize do |app| - if app.config.secret_key_base.present? + if app.secrets.secret_key_base.present? ActiveStorage.verifier = app.message_verifier("ActiveStorage") end end end initializer "active_storage.services" do - config.after_initialize do |app| - if config_choice = app.config.active_storage.service - config_file = Pathname.new(Rails.root.join("config/storage.yml")) - raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist? + config.to_prepare 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")) + raise("Couldn't find Active Storage configuration in #{config_file}") unless config_file.exist? - require "yaml" - require "erb" + require "yaml" + require "erb" - configs = - begin - YAML.load(ERB.new(config_file.read).result) || {} - rescue Psych::SyntaxError => e - raise "YAML syntax error occurred while parsing #{config_file}. " \ - "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ - "Error: #{e.message}" - end + YAML.load(ERB.new(config_file.read).result) || {} + rescue Psych::SyntaxError => e + raise "YAML syntax error occurred while parsing #{config_file}. " \ + "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \ + "Error: #{e.message}" + end ActiveStorage::Blob.service = begin diff --git a/activestorage/lib/active_storage/gem_version.rb b/activestorage/lib/active_storage/gem_version.rb index cde112a006..db79c669bf 100644 --- a/activestorage/lib/active_storage/gem_version.rb +++ b/activestorage/lib/active_storage/gem_version.rb @@ -5,8 +5,8 @@ module ActiveStorage end module VERSION - MAJOR = 0 - MINOR = 1 + MAJOR = 5 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/activestorage/lib/active_storage/log_subscriber.rb b/activestorage/lib/active_storage/log_subscriber.rb index 4ac34a3b25..5c1b8d23ef 100644 --- a/activestorage/lib/active_storage/log_subscriber.rb +++ b/activestorage/lib/active_storage/log_subscriber.rb @@ -1,48 +1,50 @@ require "active_support/log_subscriber" -class ActiveStorage::LogSubscriber < ActiveSupport::LogSubscriber - def service_upload(event) - message = "Uploaded file to key: #{key_in(event)}" - message << " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum] - info event, color(message, GREEN) - end - - def service_download(event) - info event, color("Downloaded file from key: #{key_in(event)}", BLUE) - end - - def service_delete(event) - info event, color("Deleted file from key: #{key_in(event)}", RED) - end - - def service_exist(event) - debug event, color("Checked if file exist at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE) - end - - def service_url(event) - debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE) - end +module ActiveStorage + class LogSubscriber < ActiveSupport::LogSubscriber + def service_upload(event) + message = "Uploaded file to key: #{key_in(event)}" + message << " (checksum: #{event.payload[:checksum]})" if event.payload[:checksum] + info event, color(message, GREEN) + end - def logger - ActiveStorage::Service.logger - end + def service_download(event) + info event, color("Downloaded file from key: #{key_in(event)}", BLUE) + end - private - def info(event, colored_message) - super log_prefix_for_service(event) + colored_message + def service_delete(event) + info event, color("Deleted file from key: #{key_in(event)}", RED) end - def debug(event, colored_message) - super log_prefix_for_service(event) + colored_message + def service_exist(event) + debug event, color("Checked if file exist at key: #{key_in(event)} (#{event.payload[:exist] ? "yes" : "no"})", BLUE) end - def log_prefix_for_service(event) - color " #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN + def service_url(event) + debug event, color("Generated URL for file at key: #{key_in(event)} (#{event.payload[:url]})", BLUE) end - def key_in(event) - event.payload[:key] + def logger + ActiveStorage::Service.logger end + + private + def info(event, colored_message) + super log_prefix_for_service(event) + colored_message + end + + def debug(event, colored_message) + super log_prefix_for_service(event) + colored_message + end + + def log_prefix_for_service(event) + color " #{event.payload[:service]} Storage (#{event.duration.round(1)}ms) ", CYAN + end + + def key_in(event) + event.payload[:key] + end + end end ActiveStorage::LogSubscriber.attach_to :active_storage diff --git a/activestorage/lib/active_storage/migration.rb b/activestorage/lib/active_storage/migration.rb deleted file mode 100644 index 2e35e163cd..0000000000 --- a/activestorage/lib/active_storage/migration.rb +++ /dev/null @@ -1,27 +0,0 @@ -class ActiveStorageCreateTables < ActiveRecord::Migration[5.1] - def change - create_table :active_storage_blobs do |t| - t.string :key - t.string :filename - t.string :content_type - t.text :metadata - t.integer :byte_size - t.string :checksum - t.datetime :created_at - - t.index [ :key ], unique: true - end - - create_table :active_storage_attachments do |t| - t.string :name - t.string :record_type - t.integer :record_id - t.integer :blob_id - - t.datetime :created_at - - t.index :blob_id - t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true - end - end -end diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb index 4223295ed8..4dd6eb6045 100644 --- a/activestorage/lib/active_storage/service.rb +++ b/activestorage/lib/active_storage/service.rb @@ -1,114 +1,115 @@ require "active_storage/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. -# * +AzureStorage+, to manage attachments through Microsoft Azure Storage. -# * +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.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 +module ActiveStorage + class IntegrityError < StandardError; end + # 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. + # * +AzureStorage+, to manage attachments through Microsoft Azure Storage. + # * +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.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 Service + extend ActiveSupport::Autoload + autoload :Configurator - extend ActiveSupport::Autoload - autoload :Configurator + class_attribute :logger - 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 - 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) + # 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 - # 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) + # Upload the +io+ to the +key+ specified. If a +checksum+ is provided, the service will + # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError. + def upload(key, io, checksum: nil) + raise NotImplementedError end - end - - # Upload the `io` to the `key` specified. If a `checksum` is provided, the service will - # ensure a match when the upload has completed or raise an `ActiveStorage::IntegrityError`. - def upload(key, io, checksum: nil) - raise NotImplementedError - end - - # Return the content of the file at the `key`. - def download(key) - raise NotImplementedError - end - # Delete the file at the `key`. - def delete(key) - raise NotImplementedError - end - - # Return true if a file exists at the `key`. - def exist?(key) - raise NotImplementedError - end + # Return the content of the file at the +key+. + def download(key) + raise NotImplementedError + end - # Returns a signed, temporary URL for the file at the `key`. The URL will be valid for the amount - # of seconds specified in `expires_in`. You most also provide the `disposition` (`:inline` or `:attachment`), - # `filename`, and `content_type` that you wish the file to be served with on request. - def url(key, expires_in:, disposition:, filename:, content_type:) - raise NotImplementedError - end + # Delete the file at the +key+. + def delete(key) + raise NotImplementedError + end - # Returns a signed, temporary URL that a direct upload file can be PUT to on the `key`. - # The URL will be valid for the amount of seconds specified in `expires_in`. - # You most also provide the `content_type`, `content_length`, and `checksum` of the file - # that will be uploaded. All these attributes will be validated by the service upon upload. - def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - raise NotImplementedError - end + # Return true if a file exists at the +key+. + def exist?(key) + raise NotImplementedError + end - # Returns a Hash of headers for `url_for_direct_upload` requests. - def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:) - {} - end + # Returns a signed, temporary URL for the file at the +key+. The URL will be valid for the amount + # of seconds specified in +expires_in+. You most also provide the +disposition+ (+:inline+ or +:attachment+), + # +filename+, and +content_type+ that you wish the file to be served with on request. + def url(key, expires_in:, disposition:, filename:, content_type:) + 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) + # Returns a signed, temporary URL that a direct upload file can be PUT to on the +key+. + # The URL will be valid for the amount of seconds specified in +expires_in+. + # You most also provide the +content_type+, +content_length+, and +checksum+ of the file + # that will be uploaded. All these attributes will be validated by the service upon upload. + def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) + raise NotImplementedError end - def service_name - # ActiveStorage::Service::DiskService => Disk - self.class.name.split("::").third.remove("Service") + # Returns a Hash of headers for +url_for_direct_upload+ requests. + def headers_for_direct_upload(key, filename:, content_type:, content_length:, checksum:) + {} 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 end diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index 527dc57eeb..d1c62f7db1 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -2,114 +2,117 @@ require "active_support/core_ext/numeric/bytes" require "azure/storage" require "azure/storage/core/auth/shared_access_signature" -# Wraps the Microsoft Azure Storage Blob Service as a Active Storage service. -# See `ActiveStorage::Service` for the generic API documentation that applies to all services. -class ActiveStorage::Service::AzureStorageService < ActiveStorage::Service - attr_reader :client, :path, :blobs, :container, :signer - - def initialize(path:, 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 +module ActiveStorage + # Wraps the Microsoft Azure Storage Blob Service as a 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 + + def initialize(path:, 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) - instrument :upload, key, checksum: checksum do - begin - blobs.create_block_blob(container, key, io, content_md5: checksum) - rescue Azure::Core::Http::HTTPError => e - raise ActiveStorage::IntegrityError + def upload(key, io, checksum: nil) + instrument :upload, key, checksum: checksum do + begin + blobs.create_block_blob(container, key, io, content_md5: checksum) + rescue Azure::Core::Http::HTTPError + raise ActiveStorage::IntegrityError + end end end - end - def download(key) - if block_given? - instrument :streaming_download, key do - stream(key, &block) - end - else - instrument :download, key do - _, io = blobs.get_blob(container, key) - io.force_encoding(Encoding::BINARY) + def download(key) + if block_given? + instrument :streaming_download, key do + stream(key, &block) + end + else + instrument :download, key do + _, io = blobs.get_blob(container, key) + io.force_encoding(Encoding::BINARY) + end end end - end - def delete(key) - instrument :delete, key do - begin - blobs.delete_blob(container, key) - rescue Azure::Core::Http::HTTPError - false + def delete(key) + instrument :delete, key do + begin + blobs.delete_blob(container, key) + rescue Azure::Core::Http::HTTPError + false + end end end - end - def exist?(key) - instrument :exist, key do |payload| - answer = blob_for(key).present? - payload[:exist] = answer - answer + def exist?(key) + instrument :exist, key do |payload| + answer = blob_for(key).present? + payload[:exist] = answer + answer + end end - end - def url(key, expires_in:, disposition:, filename:) - instrument :url, key do |payload| - base_url = url_for(key) - generated_url = signer.signed_uri(URI(base_url), false, permissions: "r", - expiry: format_expiry(expires_in), content_disposition: "#{disposition}; filename=\"#{filename}\"").to_s + def url(key, expires_in:, disposition:, filename:, content_type:) + instrument :url, key do |payload| + base_url = url_for(key) + generated_url = signer.signed_uri(URI(base_url), false, permissions: "r", + expiry: format_expiry(expires_in), content_type: content_type, + content_disposition: "#{disposition}; filename=\"#{filename}\"").to_s - payload[:url] = generated_url + payload[:url] = generated_url - generated_url + generated_url + end end - end - def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, 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 + def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) + instrument :url, 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 - payload[:url] = generated_url + payload[:url] = generated_url - generated_url + generated_url + end end - end - - def headers_for_direct_upload(key, content_type:, checksum:, **) - { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" } - end - private - def url_for(key) - "#{path}/#{container}/#{key}" + def headers_for_direct_upload(key, content_type:, checksum:, **) + { "Content-Type" => content_type, "Content-MD5" => checksum, "x-ms-blob-type" => "BlockBlob" } end - def blob_for(key) - blobs.get_blob_properties(container, key) - rescue Azure::Core::Http::HTTPError - false - end + private + def url_for(key) + "#{path}/#{container}/#{key}" + end - def format_expiry(expires_in) - expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil - end + def blob_for(key) + blobs.get_blob_properties(container, key) + rescue Azure::Core::Http::HTTPError + false + end + + def format_expiry(expires_in) + expires_in ? Time.now.utc.advance(seconds: expires_in).iso8601 : nil + end - # Reads the object for the given key in chunks, yielding each to the block. - def stream(key, options = {}, &block) - blob = blob_for(key) + # Reads the object for the given key in chunks, yielding each to the block. + def stream(key, options = {}, &block) + blob = blob_for(key) - chunk_size = 5.megabytes - offset = 0 + chunk_size = 5.megabytes + offset = 0 - while offset < blob.properties[:content_length] - _, io = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1) - yield io - offset += chunk_size + while offset < blob.properties[:content_length] + _, io = blobs.get_blob(container, key, start_range: offset, end_range: offset + chunk_size - 1) + yield io + offset += chunk_size + end end - end + end end diff --git a/activestorage/lib/active_storage/service/configurator.rb b/activestorage/lib/active_storage/service/configurator.rb index a0afdaa912..5d6475a8ae 100644 --- a/activestorage/lib/active_storage/service/configurator.rb +++ b/activestorage/lib/active_storage/service/configurator.rb @@ -1,28 +1,30 @@ -class ActiveStorage::Service::Configurator #:nodoc: - attr_reader :configurations +module ActiveStorage + class Service::Configurator #:nodoc: + attr_reader :configurations - def self.build(service_name, configurations) - new(configurations).build(service_name) - end + def self.build(service_name, configurations) + new(configurations).build(service_name) + end - def initialize(configurations) - @configurations = configurations.deep_symbolize_keys - 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 + 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}" + 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 - end - def resolve(class_name) - require "active_storage/service/#{class_name.to_s.underscore}_service" - ActiveStorage::Service.const_get(:"#{class_name}Service") - end + def resolve(class_name) + require "active_storage/service/#{class_name.to_s.underscore}_service" + ActiveStorage::Service.const_get(:"#{class_name}Service") + end + end end diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index 35b0909297..9498761cc5 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -3,122 +3,124 @@ require "pathname" require "digest/md5" require "active_support/core_ext/numeric/bytes" -# Wraps a local disk path as a Active Storage service. See `ActiveStorage::Service` for the generic API -# documentation that applies to all services. -class ActiveStorage::Service::DiskService < ActiveStorage::Service - attr_reader :root - - def initialize(root:) - @root = root - end +module ActiveStorage + # Wraps a local disk path as a Active Storage service. See ActiveStorage::Service for the generic API + # documentation that applies to all services. + class Service::DiskService < 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 + 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 - 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 + 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 - end - else - instrument :download, key do - File.binread path_for(key) + else + instrument :download, key do + File.binread path_for(key) + end end end - end - def delete(key) - instrument :delete, key do - begin - File.delete path_for(key) - rescue Errno::ENOENT - # Ignore files already deleted + def delete(key) + instrument :delete, key do + begin + File.delete path_for(key) + rescue Errno::ENOENT + # Ignore files already deleted + end end end - end - def exist?(key) - instrument :exist, key do |payload| - answer = File.exist? path_for(key) - payload[:exist] = answer - answer + def exist?(key) + instrument :exist, key do |payload| + answer = File.exist? path_for(key) + payload[:exist] = answer + answer + end end - end - def url(key, expires_in:, disposition:, filename:, content_type:) - instrument :url, key do |payload| - verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) - - generated_url = - if defined?(Rails.application) - Rails.application.routes.url_helpers.rails_disk_service_path \ - verified_key_with_expiration, - disposition: disposition, filename: filename, content_type: content_type - else - "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?disposition=#{disposition}&content_type=#{content_type}" - end + def url(key, expires_in:, disposition:, filename:, content_type:) + instrument :url, key do |payload| + verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) + + generated_url = + if defined?(Rails.application) + Rails.application.routes.url_helpers.rails_disk_service_path \ + verified_key_with_expiration, + disposition: disposition, filename: filename, content_type: content_type + else + "/rails/active_storage/disk/#{verified_key_with_expiration}/#{filename}?disposition=#{disposition}&content_type=#{content_type}" + end - payload[:url] = generated_url + payload[:url] = generated_url - generated_url + generated_url + end end - end - def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| - verified_token_with_expiration = ActiveStorage.verifier.generate( - { - key: key, - content_type: content_type, - content_length: content_length, - checksum: checksum - }, - expires_in: expires_in, - purpose: :blob_token - ) - - generated_url = - if defined?(Rails.application) - Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration - else - "/rails/active_storage/disk/#{verified_token_with_expiration}" - end + def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) + instrument :url, key do |payload| + verified_token_with_expiration = ActiveStorage.verifier.generate( + { + key: key, + content_type: content_type, + content_length: content_length, + checksum: checksum + }, + expires_in: expires_in, + purpose: :blob_token + ) + + generated_url = + if defined?(Rails.application) + Rails.application.routes.url_helpers.update_rails_disk_service_path verified_token_with_expiration + else + "/rails/active_storage/disk/#{verified_token_with_expiration}" + end - payload[:url] = generated_url + payload[:url] = generated_url - generated_url + generated_url + end end - end - def headers_for_direct_upload(key, content_type:, **) - { "Content-Type" => content_type } - end - - private - def path_for(key) - File.join root, folder_for(key), key + def headers_for_direct_upload(key, content_type:, **) + { "Content-Type" => content_type } end - def folder_for(key) - [ key[0..1], key[2..3] ].join("/") - end + private + def path_for(key) + File.join root, folder_for(key), key + end - def make_path_for(key) - path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) } - end + def folder_for(key) + [ key[0..1], key[2..3] ].join("/") + end - def ensure_integrity_of(key, checksum) - unless Digest::MD5.file(path_for(key)).base64digest == checksum - delete key - raise ActiveStorage::IntegrityError + def make_path_for(key) + path_for(key).tap { |path| FileUtils.mkdir_p File.dirname(path) } end - end + + def ensure_integrity_of(key, checksum) + unless Digest::MD5.file(path_for(key)).base64digest == checksum + delete key + raise ActiveStorage::IntegrityError + end + end + end end diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index 73629f7486..0707f26a74 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -1,79 +1,81 @@ require "google/cloud/storage" require "active_support/core_ext/object/to_query" -# Wraps the Google Cloud Storage as a Active Storage service. See `ActiveStorage::Service` for the generic API -# documentation that applies to all services. -class ActiveStorage::Service::GCSService < ActiveStorage::Service - attr_reader :client, :bucket +module ActiveStorage + # Wraps the Google Cloud Storage as a Active Storage service. See ActiveStorage::Service for the generic API + # documentation that applies to all services. + class Service::GCSService < 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 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 + 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 - end - # FIXME: Add streaming when given a block - def download(key) - instrument :download, key do - io = file_for(key).download - io.rewind - io.read + # 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 - end - def delete(key) - instrument :delete, key do - file_for(key).try(:delete) + def delete(key) + instrument :delete, key do + file_for(key).try(:delete) + end end - end - def exist?(key) - instrument :exist, key do |payload| - answer = file_for(key).present? - payload[:exist] = answer - answer + def exist?(key) + instrument :exist, key do |payload| + answer = file_for(key).present? + payload[:exist] = answer + answer + end end - end - def url(key, expires_in:, disposition:, filename:, content_type:) - instrument :url, key do |payload| - generated_url = file_for(key).signed_url expires: expires_in, query: { - "response-content-disposition" => "#{disposition}; filename=\"#{filename}\"", - "response-content-type" => content_type - } + def url(key, expires_in:, disposition:, filename:, content_type:) + instrument :url, key do |payload| + generated_url = file_for(key).signed_url expires: expires_in, query: { + "response-content-disposition" => "#{disposition}; filename=\"#{filename}\"", + "response-content-type" => content_type + } - payload[:url] = generated_url + payload[:url] = generated_url - generated_url + generated_url + end end - end - def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - instrument :url, key do |payload| - generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, - content_type: content_type, content_md5: checksum + def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) + instrument :url, key do |payload| + generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, + content_type: content_type, content_md5: checksum - payload[:url] = generated_url + payload[:url] = generated_url - generated_url + generated_url + end end - end - def headers_for_direct_upload(key, content_type:, checksum:, **) - { "Content-Type" => content_type, "Content-MD5" => checksum } - end - - private - def file_for(key) - bucket.file(key) + def headers_for_direct_upload(key, content_type:, checksum:, **) + { "Content-Type" => content_type, "Content-MD5" => checksum } end + + private + def file_for(key) + bucket.file(key) + end + end end diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb index 7c407f2730..8491df4911 100644 --- a/activestorage/lib/active_storage/service/mirror_service.rb +++ b/activestorage/lib/active_storage/service/mirror_service.rb @@ -1,46 +1,48 @@ require "active_support/core_ext/module/delegation" -# Wraps a set of mirror services and provides a single `ActiveStorage::Service` object that will all -# have the files uploaded to them. A `primary` service is designated to answer calls to `download`, `exists?`, -# and `url`. -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 - - # Upload the `io` to the `key` specified to all services. If a `checksum` is provided, all services will - # ensure a match when the upload has completed or raise an `ActiveStorage::IntegrityError`. - def upload(key, io, checksum: nil) - each_service.collect do |service| - service.upload key, io.tap(&:rewind), checksum: checksum +module ActiveStorage + # Wraps a set of mirror services and provides a single ActiveStorage::Service object that will all + # have the files uploaded to them. A +primary+ service is designated to answer calls to +download+, +exists?+, + # and +url+. + class Service::MirrorService < 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 - end - - # Delete the file at the `key` on all services. - def delete(key) - perform_across_services :delete, key - end - private - def each_service(&block) - [ primary, *mirrors ].each(&block) + def initialize(primary:, mirrors:) + @primary, @mirrors = primary, mirrors end - def perform_across_services(method, *args) - # FIXME: Convert to be threaded + # Upload the +io+ to the +key+ specified to all services. If a +checksum+ is provided, all services will + # ensure a match when the upload has completed or raise an ActiveStorage::IntegrityError. + def upload(key, io, checksum: nil) each_service.collect do |service| - service.public_send method, *args + service.upload key, io.tap(&:rewind), checksum: checksum end end + + # Delete the file at the +key+ on all services. + 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 end diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb index ca461c2994..b25eb409ef 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -1,96 +1,98 @@ require "aws-sdk" require "active_support/core_ext/numeric/bytes" -# Wraps the Amazon Simple Storage Service (S3) as a Active Storage service. -# See `ActiveStorage::Service` for the generic API documentation that applies to all services. -class ActiveStorage::Service::S3Service < ActiveStorage::Service - attr_reader :client, :bucket, :upload_options +module ActiveStorage + # Wraps the Amazon Simple Storage Service (S3) as a Active Storage service. + # See ActiveStorage::Service for the generic API documentation that applies to all services. + 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) - @bucket = @client.bucket(bucket) + 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 + @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 + 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 - 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) + 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 - end - def delete(key) - instrument :delete, key do - object_for(key).delete + def delete(key) + instrument :delete, key do + object_for(key).delete + end end - end - def exist?(key) - instrument :exist, key do |payload| - answer = object_for(key).exists? - payload[:exist] = answer - answer + def exist?(key) + instrument :exist, key do |payload| + answer = object_for(key).exists? + payload[:exist] = answer + answer + end end - end - def url(key, expires_in:, disposition:, filename:, content_type:) - instrument :url, key do |payload| - generated_url = object_for(key).presigned_url :get, expires_in: expires_in, - response_content_disposition: "#{disposition}; filename=\"#{filename}\"", - response_content_type: content_type + def url(key, expires_in:, disposition:, filename:, content_type:) + instrument :url, key do |payload| + generated_url = object_for(key).presigned_url :get, expires_in: expires_in, + response_content_disposition: "#{disposition}; filename=\"#{filename}\"", + response_content_type: content_type - payload[:url] = generated_url + payload[:url] = generated_url - generated_url + generated_url + end end - end - def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) - 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, content_md5: checksum + def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) + 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, content_md5: checksum - payload[:url] = generated_url + payload[:url] = generated_url - generated_url + generated_url + end end - end - - def headers_for_direct_upload(key, content_type:, checksum:, **) - { "Content-Type" => content_type, "Content-MD5" => checksum } - end - private - def object_for(key) - bucket.object(key) + def headers_for_direct_upload(key, content_type:, checksum:, **) + { "Content-Type" => content_type, "Content-MD5" => checksum } end - # Reads the object for the given key in chunks, yielding each to the block. - def stream(key, options = {}, &block) - object = object_for(key) + 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 + 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 + while offset < object.content_length + yield object.read(options.merge(range: "bytes=#{offset}-#{offset + chunk_size - 1}")) + offset += chunk_size + end end - end + end end diff --git a/activestorage/lib/tasks/activestorage.rake b/activestorage/lib/tasks/activestorage.rake index 1d386e67df..7a573be596 100644 --- a/activestorage/lib/tasks/activestorage.rake +++ b/activestorage/lib/tasks/activestorage.rake @@ -1,13 +1,6 @@ -require "fileutils" - namespace :activestorage do desc "Copy over the migration needed to the application" - task :install do - migration_file_path = "db/migrate/#{Time.now.utc.strftime("%Y%m%d%H%M%S")}_active_storage_create_tables.rb" - FileUtils.mkdir_p Rails.root.join("db/migrate") - FileUtils.cp File.expand_path("../../active_storage/migration.rb", __FILE__), Rails.root.join(migration_file_path) - puts "Copied migration to #{migration_file_path}" - - puts "Now run rails db:migrate to create the tables for Active Storage" + task install: :environment do + Rake::Task["active_storage:install:migrations"].invoke end end diff --git a/activestorage/package.json b/activestorage/package.json index 56d307f9d6..b481295a8d 100644 --- a/activestorage/package.json +++ b/activestorage/package.json @@ -1,6 +1,6 @@ { "name": "activestorage", - "version": "0.1.1", + "version": "5.2.0.alpha", "description": "Attach cloud and local files in Rails applications", "main": "app/assets/javascripts/activestorage.js", "files": [ diff --git a/activestorage/test/controllers/blobs_controller_test.rb b/activestorage/test/controllers/blobs_controller_test.rb new file mode 100644 index 0000000000..5ec353889c --- /dev/null +++ b/activestorage/test/controllers/blobs_controller_test.rb @@ -0,0 +1,15 @@ +require "test_helper" +require "database/setup" + +class ActiveStorage::BlobsControllerTest < ActionDispatch::IntegrationTest + setup do + @blob = create_image_blob filename: "racecar.jpg" + end + + test "showing blob utilizes browser caching" do + get rails_blob_url(@blob) + + assert_redirected_to(/racecar.jpg/) + assert_equal "max-age=300, private", @response.headers["Cache-Control"] + end +end diff --git a/activestorage/test/controllers/direct_uploads_controller_test.rb b/activestorage/test/controllers/direct_uploads_controller_test.rb index e056b629bb..4f335c7759 100644 --- a/activestorage/test/controllers/direct_uploads_controller_test.rb +++ b/activestorage/test/controllers/direct_uploads_controller_test.rb @@ -25,7 +25,7 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr assert_equal checksum, details["checksum"] assert_equal "text/plain", details["content_type"] assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], details["direct_upload"]["url"] - assert_match /s3\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"] + assert_match(/s3\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"]) assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"]) end end @@ -115,7 +115,7 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati assert_equal 6, details["byte_size"] assert_equal checksum, details["checksum"] assert_equal "text/plain", details["content_type"] - assert_match /rails\/active_storage\/disk/, details["direct_upload"]["url"] + assert_match(/rails\/active_storage\/disk/, details["direct_upload"]["url"]) assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"]) end end diff --git a/activestorage/test/controllers/variants_controller_test.rb b/activestorage/test/controllers/variants_controller_test.rb index fa8d15977d..b3ff83e95e 100644 --- a/activestorage/test/controllers/variants_controller_test.rb +++ b/activestorage/test/controllers/variants_controller_test.rb @@ -12,7 +12,7 @@ class ActiveStorage::VariantsControllerTest < ActionDispatch::IntegrationTest signed_blob_id: @blob.signed_id, variation_key: ActiveStorage::Variation.encode(resize: "100x100")) - assert_redirected_to /racecar.jpg\?.*disposition=inline/ + assert_redirected_to(/racecar.jpg\?.*disposition=inline/) image = read_image_variant(@blob.variant(resize: "100x100")) assert_equal 100, image.width diff --git a/activestorage/test/database/setup.rb b/activestorage/test/database/setup.rb index b12038ee68..610b927cc3 100644 --- a/activestorage/test/database/setup.rb +++ b/activestorage/test/database/setup.rb @@ -1,6 +1,5 @@ -require "active_storage/migration" require_relative "create_users_migration" ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") -ActiveStorageCreateTables.migrate(:up) +ActiveRecord::Migrator.migrate File.expand_path("../../../db/migrate", __FILE__) ActiveStorageCreateUsers.migrate(:up) diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb index 82256e1f44..34cd62dcde 100644 --- a/activestorage/test/models/attachments_test.rb +++ b/activestorage/test/models/attachments_test.rb @@ -1,13 +1,6 @@ require "test_helper" require "database/setup" -# ActiveRecord::Base.logger = Logger.new(STDOUT) - -class User < ActiveRecord::Base - has_one_attached :avatar - has_many_attached :highlights -end - class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase include ActiveJob::TestHelper @@ -25,11 +18,17 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase assert_equal "funky.jpg", @user.avatar.filename.to_s end - test "attach new blob" do + test "attach new blob from a Hash" do @user.avatar.attach io: StringIO.new("STUFF"), filename: "town.jpg", content_type: "image/jpg" assert_equal "town.jpg", @user.avatar.filename.to_s end + test "attach new blob from an UploadedFile" do + file = file_fixture "racecar.jpg" + @user.avatar.attach Rack::Test::UploadedFile.new file + assert_equal "racecar.jpg", @user.avatar.filename.to_s + end + test "access underlying associations of new blob" do @user.avatar.attach create_blob(filename: "funky.jpg") assert_equal @user, @user.avatar_attachment.record diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb index 7d52f005a7..04ebaccb6a 100644 --- a/activestorage/test/models/variant_test.rb +++ b/activestorage/test/models/variant_test.rb @@ -8,7 +8,7 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase test "resized variation" do variant = @blob.variant(resize: "100x100").processed - assert_match /racecar.jpg/, variant.service_url + assert_match(/racecar.jpg/, variant.service_url) image = read_image_variant(variant) assert_equal 100, image.width @@ -17,11 +17,11 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase test "resized and monochrome variation" do variant = @blob.variant(resize: "100x100", monochrome: true).processed - assert_match /racecar.jpg/, variant.service_url + assert_match(/racecar.jpg/, variant.service_url) image = read_image_variant(variant) assert_equal 100, image.width assert_equal 67, image.height - assert_match /Gray/, image.colorspace + assert_match(/Gray/, image.colorspace) end end diff --git a/activestorage/test/service/azure_storage_service_test.rb b/activestorage/test/service/azure_storage_service_test.rb index e2be510b60..a0c6a4de73 100644 --- a/activestorage/test/service/azure_storage_service_test.rb +++ b/activestorage/test/service/azure_storage_service_test.rb @@ -6,8 +6,15 @@ if SERVICE_CONFIGURATIONS[:azure] SERVICE = ActiveStorage::Service.configure(:azure, SERVICE_CONFIGURATIONS) include ActiveStorage::Service::SharedServiceTests - end + test "signed URL generation" do + url = @service.url(FIXTURE_KEY, expires_in: 5.minutes, + disposition: :inline, filename: "avatar.png", content_type: "image/png") + + assert_match(/(\S+)&rsct=image%2Fpng&rscd=inline%3B\+filename%3D%22avatar.png/, url) + assert_match SERVICE_CONFIGURATIONS[:azure][:container], url + end + end else puts "Skipping Azure Storage Service tests because no Azure configuration was supplied" end diff --git a/activestorage/test/service/configurations.yml b/activestorage/test/service/configurations.yml index d7aa672573..56ed37be5d 100644 --- a/activestorage/test/service/configurations.yml +++ b/activestorage/test/service/configurations.yml @@ -1,10 +1,10 @@ -s3: - service: S3 - access_key_id: <%= ENV["AWS_ACCESS_KEY_ID"] %> - secret_access_key: <%= ENV["AWS_SECRET_KEY"] %> - region: us-east-2 - bucket: rails-ci-activestorage - +# s3: +# service: S3 +# access_key_id: "" +# secret_access_key: "" +# region: "" +# bucket: "" +# # gcs: # service: GCS # keyfile: { diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb index a625521601..2fdb113616 100644 --- a/activestorage/test/service/disk_service_test.rb +++ b/activestorage/test/service/disk_service_test.rb @@ -6,7 +6,7 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase include ActiveStorage::Service::SharedServiceTests test "url generation" do - assert_match /rails\/active_storage\/disk\/.*\/avatar\.png\?.+disposition=inline/, - @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: "avatar.png", content_type: "image/png") + assert_match(/rails\/active_storage\/disk\/.*\/avatar\.png\?.+disposition=inline/, + @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: "avatar.png", content_type: "image/png")) end end diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb index 6d613fd4fc..57e13e6538 100644 --- a/activestorage/test/service/s3_service_test.rb +++ b/activestorage/test/service/s3_service_test.rb @@ -33,7 +33,7 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr url = @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: "avatar.png", content_type: "image/png") - assert_match /s3\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url + assert_match(/s3\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url) assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], url end diff --git a/activestorage/test/template/image_tag_test.rb b/activestorage/test/template/image_tag_test.rb new file mode 100644 index 0000000000..b285cd2e14 --- /dev/null +++ b/activestorage/test/template/image_tag_test.rb @@ -0,0 +1,36 @@ +require "test_helper" +require "database/setup" + +class ActiveStorage::ImageTagTest < ActionView::TestCase + tests ActionView::Helpers::AssetTagHelper + + setup do + @blob = create_image_blob filename: "racecar.jpg" + end + + test "blob" do + assert_dom_equal %(<img alt="Racecar" src="#{polymorphic_url @blob}" />), image_tag(@blob) + end + + test "variant" do + variant = @blob.variant(resize: "100x100") + assert_dom_equal %(<img alt="Racecar" src="#{polymorphic_url variant}" />), image_tag(variant) + end + + test "attachment" do + attachment = ActiveStorage::Attachment.new(blob: @blob) + assert_dom_equal %(<img alt="Racecar" src="#{polymorphic_url attachment}" />), image_tag(attachment) + end + + test "error when attachment's empty" do + @user = User.create!(name: "DHH") + + assert_not @user.avatar.attached? + assert_raises(ArgumentError) { image_tag(@user.avatar) } + end + + test "error when object can't be resolved into url" do + unresolvable_object = ActionView::Helpers::AssetTagHelper + assert_raises(ArgumentError) { image_tag(unresolvable_object) } + end +end diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 5b6d3b64c2..1190daa7b0 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -4,13 +4,19 @@ require "bundler/setup" require "active_support" require "active_support/test_case" require "active_support/testing/autorun" -require "byebug" + +begin + require "byebug" +rescue LoadError +end require "active_job" ActiveJob::Base.queue_adapter = :test -ActiveJob::Base.logger = nil +ActiveJob::Base.logger = ActiveSupport::Logger.new(nil) -require "active_storage" +# Filter out Minitest backtrace while allowing backtrace from other libraries +# to be shown. +Minitest.backtrace_filter = Minitest::BacktraceFilter.new require "yaml" SERVICE_CONFIGURATIONS = begin @@ -54,3 +60,8 @@ end require "global_id" GlobalID.app = "ActiveStorageExampleApp" ActiveRecord::Base.send :include, GlobalID::Identification + +class User < ActiveRecord::Base + has_one_attached :avatar + has_many_attached :highlights +end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 6908882123..d5087f67af 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,7 +1,7 @@ * Update `String#camelize` to provide feedback when wrong option is passed `String#camelize` was returning nil without any feedback when an - invalid option was passed as parameter. + invalid option was passed as a parameter. Previously: @@ -17,9 +17,9 @@ * Fix modulo operations involving durations - Rails 5.1 introduce an `ActiveSupport::Duration::Scalar` class as a wrapper - around a numeric value as a way of ensuring a duration was the outcome of - an expression. However the implementation was missing support for modulo + Rails 5.1 introduced `ActiveSupport::Duration::Scalar` as a wrapper + around numeric values as a way of ensuring a duration was the outcome of + an expression. However, the implementation was missing support for modulo operations. This support has now been added and should result in a duration being returned from expressions involving modulo operations. @@ -135,15 +135,15 @@ * Fix implicit coercion calculations with scalars and durations - Previously calculations where the scalar is first would be converted to a duration - of seconds but this causes issues with dates being converted to times, e.g: + Previously, calculations where the scalar is first would be converted to a duration + of seconds, but this causes issues with dates being converted to times, e.g: Time.zone = "Beijing" # => Asia/Shanghai date = Date.civil(2017, 5, 20) # => Mon, 20 May 2017 2 * 1.day # => 172800 seconds date + 2 * 1.day # => Mon, 22 May 2017 00:00:00 CST +08:00 - Now the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain + Now, the `ActiveSupport::Duration::Scalar` calculation methods will try to maintain the part structure of the duration where possible, e.g: Time.zone = "Beijing" # => Asia/Shanghai diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 6565d53a06..c979af9de3 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -273,6 +273,8 @@ class Module def method_missing(method, *args, &block) if #{target}.respond_to?(method) #{target}.public_send(method, *args, &block) + elsif #{target}.nil? + raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil" else super end diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb index 9ab1546064..4e6d8e4585 100644 --- a/activesupport/lib/active_support/current_attributes.rb +++ b/activesupport/lib/active_support/current_attributes.rb @@ -33,7 +33,7 @@ module ActiveSupport # # private # def authenticate - # if authenticated_user = User.find_by(id: cookies.signed[:user_id]) + # if authenticated_user = User.find_by(id: cookies.encrypted[:user_id]) # Current.user = authenticated_user # else # redirect_to new_session_url diff --git a/ci/travis.rb b/ci/travis.rb index 8339e0d08b..2313bece60 100755 --- a/ci/travis.rb +++ b/ci/travis.rb @@ -28,6 +28,7 @@ class Build "av" => "actionview", "aj" => "activejob", "ac" => "actioncable", + "ast" => "activestorage", "guides" => "guides" } @@ -163,6 +164,7 @@ ENV["GEM"].split(",").each do |gem| next if gem == "aj:integration" && isolated next if gem == "guides" && isolated next if gem == "av:ujs" && isolated + next if gem == "ast" && isolated build = Build.new(gem, isolated: isolated) results[build.key] = build.run! diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 50a28571b4..31151e0329 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -64,7 +64,7 @@ module ApplicationCable private def find_verified_user - if verified_user = User.find_by(id: cookies.signed[:user_id]) + if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user else reject_unauthorized_connection diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 7751ac00df..6562dc3a98 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -64,7 +64,7 @@ Rails. Mailers are conceptually similar to controllers, and so we get a mailer, a directory for views, and a test. If you didn't want to use a generator, you could create your own file inside of -app/mailers, just make sure that it inherits from `ActionMailer::Base`: +`app/mailers`, just make sure that it inherits from `ActionMailer::Base`: ```ruby class MyMailer < ActionMailer::Base diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 443be77934..2606bfe280 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -260,40 +260,48 @@ backends you need to specify the queues to listen to. Callbacks --------- -Active Job provides hooks during the life cycle of a job. Callbacks allow you to -trigger logic during the life cycle of a job. - -### Available callbacks - -* `before_enqueue` -* `around_enqueue` -* `after_enqueue` -* `before_perform` -* `around_perform` -* `after_perform` - -### Usage +Active Job provides hooks to trigger logic during the life cycle of a job. Like +other callbacks in Rails, you can implement the callbacks as ordinary methods +and use a macro-style class method to register them as callbacks: ```ruby class GuestsCleanupJob < ApplicationJob queue_as :default - before_enqueue do |job| - # Do something with the job instance - end - - around_perform do |job, block| - # Do something before perform - block.call - # Do something after perform - end + around_perform :around_cleanup def perform # Do something later end + + private + def around_cleanup(job) + # Do something before perform + yield + # Do something after perform + end end ``` +The macro-style class methods can also receive a block. Consider using this +style if the code inside your block is so short that it fits in a single line. +For example, you could send metrics for every job enqueued: + +```ruby +class ApplicationJob + before_enqueue { |job| $statsd.increment "#{job.name.underscore}.enqueue" } +end +``` + +### Available callbacks + +* `before_enqueue` +* `around_enqueue` +* `after_enqueue` +* `before_perform` +* `around_perform` +* `after_perform` + Action Mailer ------------ diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index bead931529..1212ae53bc 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -811,6 +811,7 @@ When you declare a `belongs_to` association, the declaring class automatically g * `build_association(attributes = {})` * `create_association(attributes = {})` * `create_association!(attributes = {})` +* `reload_association` In all of these methods, `association` is replaced with the symbol passed as the first argument to `belongs_to`. For example, given the declaration: @@ -828,6 +829,7 @@ author= build_author create_author create_author! +reload_author ``` NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix. @@ -840,10 +842,10 @@ The `association` method returns the associated object, if any. If no associated @author = @book.author ``` -If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object. +If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload_association` on the parent object. ```ruby -@author = @book.reload.author +@author = @book.reload_author ``` ##### `association=(associate)` @@ -1161,6 +1163,7 @@ When you declare a `has_one` association, the declaring class automatically gain * `build_association(attributes = {})` * `create_association(attributes = {})` * `create_association!(attributes = {})` +* `reload_association` In all of these methods, `association` is replaced with the symbol passed as the first argument to `has_one`. For example, given the declaration: @@ -1178,6 +1181,7 @@ account= build_account create_account create_account! +reload_account ``` NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix. @@ -1190,10 +1194,10 @@ The `association` method returns the associated object, if any. If no associated @account = @supplier.account ``` -If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload` on the parent object. +If the associated object has already been retrieved from the database for this object, the cached version will be returned. To override this behavior (and force a database read), call `#reload_association` on the parent object. ```ruby -@account = @supplier.reload.account +@account = @supplier.reload_account ``` ##### `association=(associate)` @@ -1443,6 +1447,7 @@ When you declare a `has_many` association, the declaring class automatically gai * `collection.build(attributes = {}, ...)` * `collection.create(attributes = {})` * `collection.create!(attributes = {})` +* `collection.reload` In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration: @@ -1471,11 +1476,12 @@ books.exists?(...) books.build(attributes = {}, ...) books.create(attributes = {}) books.create!(attributes = {}) +books.reload ``` ##### `collection` -The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. +The `collection` method returns a Relation of all of the associated objects. If there are no associated objects, it returns an empty Relation. ```ruby @books = @author.books @@ -1609,6 +1615,14 @@ The `collection.create` method returns a single or array of new objects of the a Does the same as `collection.create` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid. +##### `collection.reload` + +The `collection.reload` method returns a Relation of all of the associated objects, forcing a database read. If there are no associated objects, it returns an empty Relation. + +```ruby +@books = @author.books.reload +``` + #### Options for `has_many` While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options: @@ -1965,6 +1979,7 @@ When you declare a `has_and_belongs_to_many` association, the declaring class au * `collection.build(attributes = {})` * `collection.create(attributes = {})` * `collection.create!(attributes = {})` +* `collection.reload` In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_and_belongs_to_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration: @@ -1993,6 +2008,7 @@ assemblies.exists?(...) assemblies.build(attributes = {}, ...) assemblies.create(attributes = {}) assemblies.create!(attributes = {}) +assemblies.reload ``` ##### Additional Column Methods @@ -2004,7 +2020,7 @@ WARNING: The use of extra attributes on the join table in a `has_and_belongs_to_ ##### `collection` -The `collection` method returns an array of all of the associated objects. If there are no associated objects, it returns an empty array. +The `collection` method returns a Relation of all of the associated objects. If there are no associated objects, it returns an empty Relation. ```ruby @assemblies = @part.assemblies @@ -2116,6 +2132,14 @@ The `collection.create` method returns a new object of the associated type. This Does the same as `collection.create`, but raises `ActiveRecord::RecordInvalid` if the record is invalid. +##### `collection.reload` + +The `collection.reload` method returns a Relation of all of the associated objects, forcing a database read. If there are no associated objects, it returns an empty Relation. + +```ruby +@assemblies = @part.assemblies.reload +``` + #### Options for `has_and_belongs_to_many` While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_and_belongs_to_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options: diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 61c4bd1e61..f6b7b3b5a7 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -383,7 +383,7 @@ indicates whether boolean values are stored in sqlite3 databases as 1 and 0 or set to false is deprecated. SQLite databases have used 't' and 'f' to serialize boolean values and must have old data converted to 1 and 0 (its native boolean serialization) before setting this flag to true. Conversion can be accomplished -by setting up a rake task which runs +by setting up a Rake task which runs ```ruby ExampleModel.where("boolean_column = 't'").update_all(boolean_column: 1) @@ -423,7 +423,7 @@ The schema dumper adds one additional configuration option: * `config.action_controller.per_form_csrf_tokens` configures whether CSRF tokens are only valid for the method/action they were generated for. -* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController:Base`. This is false by default, but enabled when loading defaults for Rails 5.2. +* `config.action_controller.default_protect_from_forgery` determines whether forgery protection is added on `ActionController:Base`. This is false by default, but enabled when loading defaults for Rails 5.2. * `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`. diff --git a/guides/source/generators.md b/guides/source/generators.md index be1be75e7a..389224d908 100644 --- a/guides/source/generators.md +++ b/guides/source/generators.md @@ -627,7 +627,7 @@ This method also takes a block: ```ruby lib "super_special.rb" do - puts "Super special!" + "puts 'Super special!'" end ``` @@ -636,7 +636,7 @@ end Creates a Rake file in the `lib/tasks` directory of the application. ```ruby -rakefile "test.rake", "hello there" +rakefile "test.rake", 'task(:hello) { puts "Hello, there" }' ``` This method also takes a block: diff --git a/guides/source/routing.md b/guides/source/routing.md index f52b1862a8..638f77be13 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -47,7 +47,7 @@ get '/patients/:id', to: 'patients#show', as: 'patient' and your application contains this code in the controller: ```ruby -@patient = Patient.find(17) +@patient = Patient.find(params[:id]) ``` and this in the corresponding view: diff --git a/guides/source/testing.md b/guides/source/testing.md index f71e963716..1c648ac47f 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -614,7 +614,7 @@ $ bin/rails generate system_test users create test/system/users_test.rb ``` -Here's what a freshly-generated system test looks like: +Here's what a freshly generated system test looks like: ```ruby require "application_system_test_case" @@ -788,7 +788,7 @@ $ bin/rails generate integration_test user_flows create test/integration/user_flows_test.rb ``` -Here's what a freshly-generated integration test looks like: +Here's what a freshly generated integration test looks like: ```ruby require 'test_helper' diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index ec41d17a9f..509b8d20e3 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,5 +1,17 @@ +* Add git_source to `Gemfile` for plugin generator. + + *Yoshiyuki Hirano* + +* Add `--skip-action-cable` option to the plugin generator. + + *bogdanvlviv* + +* Deprecate support of use `Rails::Application` subclass to start Rails server. + + *Yuji Yaginuma* + * Add `ruby x.x.x` version to `Gemfile` and create `.ruby-version` - root file containing current Ruby version when new Rails applications are + root file containing the current Ruby version when new Rails applications are created. *Alberto Almagro* @@ -32,7 +44,7 @@ *Robin Dupret*, *Kasper Timm Hansen* -* Allow to pass a custom connection name to the `rails dbconsole` +* Allow passing a custom connection name to the `rails dbconsole` command when using a 3-level database configuration. $ bin/rails dbconsole -c replica diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index ce258341f6..de69b4586b 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -2,6 +2,8 @@ require "fileutils" require "optparse" require "action_dispatch" require "rails" +require "active_support/deprecation" +require "active_support/core_ext/string/filters" require_relative "../../dev_caching" module Rails @@ -18,10 +20,15 @@ module Rails set_environment end - # TODO: this is no longer required but we keep it for the moment to support older config.ru files. def app @app ||= begin app = super + if app.is_a?(Class) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Use `Rails::Application` subclass to start the server is deprecated and will be removed in Rails 6.0. + Please change `run #{app}` to `run Rails.application` in config.ru. + MSG + end app.respond_to?(:to_app) ? app.to_app : app end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 5b16ada442..f68e13aa8b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -47,6 +47,7 @@ Rails.application.configure do # Store uploaded files on the local file system (see config/storage.yml for options) config.active_storage.service = :local + <%- unless options[:skip_action_cable] -%> # Mount Action Cable outside main process or domain # config.action_cable.mount_path = nil diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb index 51639b67a0..89d2efab2b 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/application_controller_renderer.rb @@ -1,6 +1,8 @@ # Be sure to restart your server when you modify this file. -# ApplicationController.renderer.defaults.merge!( -# http_host: 'example.org', -# https: false -# ) +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/railties/lib/rails/generators/rails/app/templates/config/storage.yml b/railties/lib/rails/generators/rails/app/templates/config/storage.yml index 522048701c..089ed4567a 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/storage.yml +++ b/railties/lib/rails/generators/rails/app/templates/config/storage.yml @@ -21,12 +21,12 @@ local: # keyfile: <%%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket -# Use rails secrets:edit to set the Azure secret (as shared:azure:storage_access_key) +# Use rails secrets:edit to set the Azure Storage secret (as shared:azure_storage:storage_access_key) # microsoft: -# service: Azure +# service: AzureStorage # path: your_azure_storage_path # storage_account_name: your_account_name -# storage_access_key: <%%= Rails.application.secrets.dig(:azure, :secret_access_key) %> +# storage_access_key: <%%= Rails.application.secrets.dig(:azure_storage, :storage_access_key) %> # container: your_container_name # mirror: diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index 61c54b4222..a40fa78132 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -76,8 +76,8 @@ module Rails template "test/test_helper.rb" template "test/%namespaced_name%_test.rb" append_file "Rakefile", <<-EOF -#{rakefile_test_tasks} +#{rakefile_test_tasks} task default: :test EOF if engine? @@ -86,7 +86,7 @@ task default: :test end PASSTHROUGH_OPTIONS = [ - :skip_active_record, :skip_action_mailer, :skip_javascript, :skip_sprockets, :database, + :skip_active_record, :skip_action_mailer, :skip_javascript, :skip_action_cable, :skip_sprockets, :database, :javascript, :quiet, :pretend, :force, :skip ] diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile index 22a4548ff2..290259b4db 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile @@ -1,4 +1,5 @@ source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } <% if options[:skip_gemspec] -%> <%= '# ' if options.dev? || options.edge? -%>gem 'rails', '<%= Array(rails_version_specifier).join("', '") %>' diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile index 3581dd401a..f3efe21cf1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/Rakefile +++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile @@ -13,17 +13,16 @@ RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_files.include('README.md') rdoc.rdoc_files.include('lib/**/*.rb') end - <% if engine? && !options[:skip_active_record] && with_dummy_app? -%> + APP_RAKEFILE = File.expand_path("<%= dummy_path -%>/Rakefile", __dir__) load 'rails/tasks/engine.rake' -<% end %> - +<% end -%> <% if engine? -%> -load 'rails/tasks/statistics.rake' -<% end %> +load 'rails/tasks/statistics.rake' +<% end -%> <% unless options[:skip_gemspec] -%> require 'bundler/gem_tasks' -<% end %> +<% end -%> diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt index ffa277e334..aed5adf8ee 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt @@ -3,7 +3,9 @@ ENGINE_ROOT = File.expand_path('..', __dir__) ENGINE_PATH = File.expand_path('../lib/<%= namespaced_name -%>/engine', __dir__) +<% if with_dummy_app? -%> APP_PATH = File.expand_path('../<%= dummy_path -%>/config/application', __dir__) +<% end -%> # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) diff --git a/railties/lib/rails/generators/rails/plugin/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore index 54c78d7927..757172e6a6 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/gitignore +++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore @@ -1,7 +1,7 @@ .bundle/ log/*.log pkg/ -<% unless options[:skip_test] && options[:dummy_path] == 'test/dummy' -%> +<% if with_dummy_app? -%> <%= dummy_path %>/db/*.sqlite3 <%= dummy_path %>/db/*.sqlite3-journal <%= dummy_path %>/log/*.log diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb index 5e0cfd6c21..47b56ae3df 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb +++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb @@ -3,16 +3,18 @@ require_relative 'boot' <% if include_all_railties? -%> require 'rails/all' <% else -%> +require "rails" # Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" <%= comment_if :skip_active_record %>require "active_record/railtie" require "action_controller/railtie" -require "action_view/railtie" <%= comment_if :skip_action_mailer %>require "action_mailer/railtie" -require "active_job/railtie" +require "action_view/railtie" require "active_storage/engine" <%= comment_if :skip_action_cable %>require "action_cable/engine" -<%= comment_if :skip_test %>require "rails/test_unit/railtie" <%= comment_if :skip_sprockets %>require "sprockets/railtie" +<%= comment_if :skip_test %>require "rails/test_unit/railtie" <% end -%> Bundler.require(*Rails.groups) diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 381bc2694f..c2f6a5a95c 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -453,10 +453,8 @@ module ApplicationTests secret_key_base: 123 YAML - app "development" - assert_raise(ArgumentError) do - app.key_generator + app "development" end end diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb new file mode 100644 index 0000000000..07880a5025 --- /dev/null +++ b/railties/test/application/server_test.rb @@ -0,0 +1,31 @@ +require "isolation/abstract_unit" +require "rails/command" +require "rails/commands/server/server_command" + +module ApplicationTests + class ServerTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + test "deprecate support of older `config.ru`" do + remove_file "config.ru" + app_file "config.ru", <<-RUBY + require_relative 'config/environment' + run AppTemplate::Application + RUBY + + server = Rails::Server.new(config: "#{app_path}/config.ru") + server.app + + log = File.read(Rails.application.config.paths["log"].first) + assert_match(/DEPRECATION WARNING: Use `Rails::Application` subclass to start the server is deprecated/, log) + end + end +end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 44c4688aa4..ff73014046 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -394,6 +394,16 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_default_frameworks_are_required_when_others_are_removed + run_generator [destination_root, "--skip-active-record", "--skip-action-mailer", "--skip-action-cable", "--skip-sprockets", "--skip-test"] + assert_file "config/application.rb", /require\s+["']rails["']/ + assert_file "config/application.rb", /require\s+["']active_model\/railtie["']/ + assert_file "config/application.rb", /require\s+["']active_job\/railtie["']/ + assert_file "config/application.rb", /require\s+["']action_controller\/railtie["']/ + assert_file "config/application.rb", /require\s+["']action_view\/railtie["']/ + assert_file "config/application.rb", /require\s+["']active_storage\/engine["']/ + end + def test_generator_defaults_to_puma_version run_generator [destination_root] assert_gem "puma", "'~> 3.7'" @@ -449,22 +459,26 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_if_skip_sprockets_is_given run_generator [destination_root, "--skip-sprockets"] + assert_no_file "config/initializers/assets.rb" - assert_file "config/application.rb" do |content| - assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content) - end + + assert_file "config/application.rb", /#\s+require\s+["']sprockets\/railtie["']/ + assert_file "Gemfile" do |content| assert_no_match(/sass-rails/, content) assert_no_match(/uglifier/, content) assert_no_match(/coffee-rails/, content) end + assert_file "config/environments/development.rb" do |content| - assert_no_match(/config\.assets\.debug = true/, content) + assert_no_match(/config\.assets\.debug/, content) end + assert_file "config/environments/production.rb" do |content| - assert_no_match(/config\.assets\.digest = true/, content) - assert_no_match(/config\.assets\.js_compressor = :uglifier/, content) - assert_no_match(/config\.assets\.css_compressor = :sass/, content) + assert_no_match(/config\.assets\.digest/, content) + assert_no_match(/config\.assets\.js_compressor/, content) + assert_no_match(/config\.assets\.css_compressor/, content) + assert_no_match(/config\.assets\.compile/, content) end end @@ -473,7 +487,8 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ assert_no_file "config/cable.yml" assert_no_file "app/assets/javascripts/cable.js" - assert_no_file "app/channels" + assert_no_directory "app/assets/javascripts/channels" + assert_no_directory "app/channels" assert_file "Gemfile" do |content| assert_no_match(/redis/, content) end @@ -486,10 +501,15 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_if_skip_test_is_given run_generator [destination_root, "--skip-test"] + + assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ + assert_file "Gemfile" do |content| assert_no_match(/capybara/, content) assert_no_match(/selenium-webdriver/, content) end + + assert_no_directory("test") end def test_generator_if_skip_system_test_is_given @@ -498,6 +518,10 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_no_match(/capybara/, content) assert_no_match(/selenium-webdriver/, content) end + + assert_directory("test") + + assert_no_directory("test/system") end def test_does_not_generate_system_test_files_if_skip_system_test_is_given @@ -570,6 +594,11 @@ class AppGeneratorTest < Rails::Generators::TestCase run_generator([destination_root]) assert_file "package.json", /dependencies/ assert_file "config/initializers/assets.rb", /node_modules/ + + assert_file ".gitignore" do |content| + assert_match(/node_modules/, content) + assert_match(/yarn-error\.log/, content) + end end def test_generator_for_yarn_skipped @@ -654,18 +683,6 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "lib/test_file.rb", "heres test data" end - def test_tests_are_removed_from_frameworks_if_skip_test_is_given - run_generator [destination_root, "--skip-test"] - assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ - end - - def test_no_active_record_or_tests_if_skips_given - run_generator [destination_root, "--skip-test", "--skip-active-record"] - assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ - assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ - assert_file "config/application.rb", /\s+require\s+["']active_job\/railtie["']/ - end - def test_pretend_option output = run_generator [File.join(destination_root, "myapp"), "--pretend"] assert_no_match(/run bundle install/, output) @@ -896,18 +913,6 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_directory("test/system") end - def test_system_tests_are_not_generated_on_system_test_skip - run_generator [destination_root, "--skip-system-test"] - - assert_no_directory("test/system") - end - - def test_system_tests_are_not_generated_on_test_skip - run_generator [destination_root, "--skip-test"] - - assert_no_directory("test/system") - end - private def stub_rails_application(root) Rails.application.config.root = root diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 1dfff38543..6637c329c8 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -82,6 +82,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/ end + def test_inclusion_of_git_source + run_generator [destination_root] + assert_file "Gemfile", /git_source/ + end + def test_inclusion_of_a_debugger run_generator [destination_root, "--full"] if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx" @@ -97,23 +102,44 @@ class PluginGeneratorTest < Rails::Generators::TestCase run_generator [destination_root, "-T", "--full"] assert_no_directory "test/integration/" - assert_no_file "test" + assert_no_directory "test" assert_file "Rakefile" do |contents| assert_no_match(/APP_RAKEFILE/, contents) end + assert_file "bin/rails" do |contents| + assert_no_match(/APP_PATH/, contents) + end end - def test_generating_adds_dummy_app_in_full_mode_without_sprockets - run_generator [destination_root, "-S", "--full"] + def test_generating_adds_dummy_app_without_sprockets + run_generator [destination_root, "--skip-sprockets"] + + assert_no_file "test/dummy/config/initializers/assets.rb" + + assert_file "test/dummy/config/application.rb", /#\s+require\s+["']sprockets\/railtie["']/ - assert_file "test/dummy/config/environments/production.rb" do |contents| - assert_no_match(/config\.assets/, contents) + assert_file "Gemfile" do |content| + assert_no_match(/sass-rails/, content) + assert_no_match(/uglifier/, content) + assert_no_match(/coffee-rails/, content) + end + + assert_file "test/dummy/config/environments/development.rb" do |content| + assert_no_match(/config\.assets\.debug/, content) + end + + assert_file "test/dummy/config/environments/production.rb" do |content| + assert_no_match(/config\.assets\.digest/, content) + assert_no_match(/config\.assets\.js_compressor/, content) + assert_no_match(/config\.assets\.css_compressor/, content) + assert_no_match(/config\.assets\.compile/, content) end end def test_generating_adds_dummy_app_rake_tasks_without_unit_test_files run_generator [destination_root, "-T", "--mountable", "--dummy-path", "my_dummy_app"] assert_file "Rakefile", /APP_RAKEFILE/ + assert_file "bin/rails", /APP_PATH/ end def test_generating_adds_dummy_app_without_javascript_and_assets_deps @@ -134,8 +160,8 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_ensure_that_test_dummy_can_be_generated_from_a_template FileUtils.cd(Rails.root) run_generator([destination_root, "-m", "lib/create_test_dummy_template.rb", "--skip-test"]) - assert_file "spec/dummy" - assert_no_file "test" + assert_directory "spec/dummy" + assert_no_directory "test" end def test_database_entry_is_generated_for_sqlite3_by_default_in_full_mode @@ -169,9 +195,19 @@ class PluginGeneratorTest < Rails::Generators::TestCase end assert_file "test/dummy/config/environments/production.rb" do |content| assert_match(/# config\.action_mailer\.raise_delivery_errors = false/, content) + assert_match(/^ config\.read_encrypted_secrets = true/, content) end end + def test_default_frameworks_are_required_when_others_are_removed + run_generator [destination_root, "--skip-active-record", "--skip-action-mailer", "--skip-action-cable", "--skip-sprockets"] + assert_file "test/dummy/config/application.rb", /require\s+["']rails["']/ + assert_file "test/dummy/config/application.rb", /require\s+["']active_model\/railtie["']/ + assert_file "test/dummy/config/application.rb", /require\s+["']active_job\/railtie["']/ + assert_file "test/dummy/config/application.rb", /require\s+["']action_controller\/railtie["']/ + assert_file "test/dummy/config/application.rb", /require\s+["']action_view\/railtie["']/ + end + def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given run_generator [destination_root, "--skip-active-record"] assert_file "test/dummy/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ @@ -197,6 +233,18 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_file "test/dummy/config/environments/production.rb" do |content| assert_no_match(/config\.action_mailer/, content) end + assert_no_directory "test/dummy/app/mailers" + end + + def test_action_cable_is_removed_from_frameworks_if_skip_action_cable_is_given + run_generator [destination_root, "--skip-action-cable"] + assert_file "test/dummy/config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ + assert_no_file "test/dummy/config/cable.yml" + assert_no_file "test/dummy/app/assets/javascripts/cable.js" + assert_no_directory "test/dummy/app/channels" + assert_file "Gemfile" do |content| + assert_no_match(/redis/, content) + end end def test_ensure_that_database_option_is_passed_to_app_generator @@ -459,10 +507,9 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_creating_dummy_without_tests_but_with_dummy_path run_generator [destination_root, "--dummy_path", "spec/dummy", "--skip-test"] - assert_file "spec/dummy" - assert_file "spec/dummy/config/application.rb" - assert_no_file "test" - assert_no_file "test/test_helper.rb" + assert_directory "spec/dummy" + assert_file "spec/dummy/config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/ + assert_no_directory "test" assert_file ".gitignore" do |contents| assert_match(/spec\/dummy/, contents) end @@ -499,9 +546,9 @@ class PluginGeneratorTest < Rails::Generators::TestCase def test_skipping_test_files run_generator [destination_root, "--skip-test"] - assert_no_file "test" + assert_no_directory "test" assert_file ".gitignore" do |contents| - assert_no_match(/test\dummy/, contents) + assert_no_match(/test\/dummy/, contents) end end diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 145c2ec7b6..ea942a3975 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -136,7 +136,7 @@ module RailtiesTest output = `bundle exec rake railties:install:migrations`.split("\n") assert_match(/Copied migration \d+_create_users\.bukkits\.rb from bukkits/, output.first) - assert_match(/Copied migration \d+_create_blogs\.blog_engine\.rb from blog_engine/, output.last) + assert_match(/Copied migration \d+_create_blogs\.blog_engine\.rb from blog_engine/, output.second) end end @@ -171,7 +171,7 @@ module RailtiesTest Dir.chdir(app_path) do output = `bundle exec rake railties:install:migrations`.split("\n") - assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.first) + assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.second) assert_match(/Copied migration \d+_create_keys\.api_engine\.rb from api_engine/, output.last) end end |