传到controller的params中的文件,会被包装成ActionDispatch::Http::UploadedFile,于是搜下它在哪里生成

$ gems git:(master) ag UploadedFile -G 'action.*5\.1'
actionpack-5.1.2/lib/action_controller/metal/strong_parameters.rb
456:    # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+.
888:        ActionDispatch::Http::UploadedFile,
889:        Rack::Test::UploadedFile,

actionpack-5.1.2/lib/action_controller/test_case.rb
142:          when Rack::Test::UploadedFile

actionpack-5.1.2/lib/action_dispatch.rb
78:    autoload :UploadedFile, "action_dispatch/http/upload"

actionpack-5.1.2/lib/action_dispatch/http/upload.rb
11:    class UploadedFile

actionpack-5.1.2/lib/action_dispatch/request/utils.rb
50:              ActionDispatch::Http::UploadedFile.new(params)

actionpack-5.1.2/lib/action_dispatch/testing/test_process.rb
7:      # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionDispatch::IntegrationTest.fixture_path, path), type)</tt>:
20:        Rack::Test::UploadedFile.new(path, mime_type, binary)

如无意外,就是actionpack-5.1.2/lib/action_dispatch/request/utils.rb了,于是加入断点

From: /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/http/upload.rb @ line 27 ActionDispatch::Http::UploadedFile#initialize:

    26: def initialize(hash) # :nodoc:
 => 27:   binding.pry
    28:   @tempfile = hash[:tempfile]
    29:   raise(ArgumentError, ":tempfile is required") unless @tempfile
    30: 
    31:   if hash[:filename]
    32:     @original_filename = hash[:filename].dup
    33: 
    34:     begin
    35:       @original_filename.encode!(Encoding::UTF_8)
    36:     rescue EncodingError
    37:       @original_filename.force_encoding(Encoding::UTF_8)
    38:     end
    39:   else
    40:     @original_filename = nil
    41:   end
    42: 
    43:   @content_type      = hash[:type]
    44:   @headers           = hash[:head]
    45: end

[2] pry(#<ActionDispatch::Http::UploadedFile>)> _bsi_
=> {0=>#<Binding:69810035258640 ActionDispatch::Http::UploadedFile#initialize /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/http/upload.rb:27>,
 1=>#<Binding:69810096889400 ActionDispatch::Request::Utils::NoNilParamEncoder.normalize_encode_params /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/request/utils.rb:50>,
 2=>#<Binding:69810096945400 ActionDispatch::Request::Utils::NoNilParamEncoder.block in normalize_encode_params /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/request/utils.rb:53>,
 3=>#<Binding:69810096984900 ActionDispatch::Request::Utils::NoNilParamEncoder.normalize_encode_params /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/request/utils.rb:52>,
 4=>#<Binding:69810097032940 ActionDispatch::Request::Utils::NoNilParamEncoder.block in normalize_encode_params /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/request/utils.rb:53>,
 5=>#<Binding:69810097080980 ActionDispatch::Request::Utils::NoNilParamEncoder.normalize_encode_params /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/request/utils.rb:52>,
 6=>#<Binding:69810097145420 ActionDispatch::Request::Utils.normalize_encode_params /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/request/utils.rb:20>,
 7=>#<Binding:69810097193520 ActionDispatch::Request#block in POST /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/http/request.rb:361>,
 8=>#<Binding:69810032817940 ActionDispatch::Request#fetch_header /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/request.rb:57>,
 9=>#<Binding:69810032633460 ActionDispatch::Request#POST /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/http/request.rb:357>,
 10=>#<Binding:69810097372300 ActionDispatch::Request#parameters /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/http/parameters.rb:53>,
 11=>#<Binding:69810097411820 ActionDispatch::Request#filtered_parameters /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/http/filter_parameters.rb:41>,
 12=>#<Binding:69810097459460 StudentsController#process_action /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_controller/metal/instrumentation.rb:21>,
 13=>#<Binding:69810097507080 StudentsController#process_action /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_controller/metal/params_wrapper.rb:252>,
 14=>#<Binding:69810097587500 StudentsController#process_action /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.2/lib/active_record/railties/controller_runtime.rb:22>,
 15=>#<Binding:69810035015520 StudentsController#process /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/abstract_controller/base.rb:124>,
 16=>#<Binding:69810034443500 StudentsController#process /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionview-5.1.2/lib/action_view/rendering.rb:30>,
 17=>#<Binding:69810097756960 StudentsController#dispatch /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_controller/metal.rb:189>,
 18=>#<Binding:69810097804600 StudentsController.dispatch /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_controller/metal.rb:253>,
 19=>#<Binding:69810097852180 ActionDispatch::Routing::RouteSet::Dispatcher#dispatch /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/routing/route_set.rb:49>,
 20=>#<Binding:69810097891740 ActionDispatch::Routing::RouteSet::Dispatcher#serve /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/routing/route_set.rb:31>,
 21=>#<Binding:69810097939440 ActionDispatch::Journey::Router#block in serve /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/journey/router.rb:46>,
 22=>#<Binding:69810034636800 ActionDispatch::Journey::Router#serve /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/journey/router.rb:33>,
 23=>#<Binding:69810098069320 ActionDispatch::Routing::RouteSet#call /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/routing/route_set.rb:832>,

其中ActionDispatch::Request::Utils.normalize_encode_params发于这里

module ActionDispatch
  class Request

    def POST
      fetch_header("action_dispatch.request.request_parameters") do
        pr = parse_formatted_parameters(params_parsers) do |params|
          super || {}
        end
        self.request_parameters = Request::Utils.normalize_encode_params(pr)
      end
    rescue Http::Parameters::ParseError # one of the parse strategies blew up
      self.request_parameters = Request::Utils.normalize_encode_params(super || {})
      raise
    rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
      raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
    end

    alias :request_parameters :POST

    def request_parameters=(params)                                                                                                                                                                                                     
      raise if params.nil?
      set_header("action_dispatch.request.request_parameters".freeze, params)
    end

它的上几层是ActionDispatch::Request#filtered_parameters,而filtered_parameters发于这里

module ActionController
  module Instrumentation
    extend ActiveSupport::Concern        

    def process_action(*args)
      raw_payload = {
        controller: self.class.name,
        action: action_name,
        params: request.filtered_parameters,
        headers: request.headers,
        format: request.format.ref,
        method: request.request_method,
        path: request.fullpath
      }   

      ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)

      ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
        begin
          result = super
          payload[:status] = response.status
          result
        ensure
          append_info_to_payload(payload)
        end
      end
    end

再回头观察ActionDispatch::Http::UploadedFile初始化时传入的参数,其中有:tempfile指向上传的文件,命名为RackMultipartXXX,估计是前面rack栈对http请求的读取,而生成的临时文件

[5] pry(#<ActionDispatch::Http::UploadedFile>)> hash
=> {:filename=>"win10-md5.txt",
 :type=>"text/plain",
 :name=>"student[photo]",
 :tempfile=>#<File:/tmp/RackMultipart20170830-3092-7hj5e8.txt>,
 :head=>"Content-Disposition: form-data; name=\"student[photo]\"; filename=\"win10-md5.txt\"\r\n" + "Content-Type: text/plain\r\n"}

而rails的参数处理也针对:tempfile来抽取成ActionDispatch::Http::UploadedFile

module ActionDispatch                                                                                                                                                                                                                   
  class Request
    class Utils # :nodoc: 
      class ParamEncoder # :nodoc:
        # Convert nested Hash to HashWithIndifferentAccess.
        #
        def self.normalize_encode_params(params)
          case params
          when Array
            handle_array params
          when Hash
            if params.has_key?(:tempfile)
              ActionDispatch::Http::UploadedFile.new(params)
            else
              params.each_with_object({}) do |(key, val), new_hash|
                new_hash[key] = normalize_encode_params(val)
              end.with_indifferent_access
            end
          else
            params
          end
        end                                                                                                                                                                                                                            
      end

不过,这已经递归到photo这层了,未encode的params是这样的

=> {"utf8"=>"✓",
 "_method"=>"patch",
 "authenticity_token"=>"wQ8nK2K0kxpo8o7HqukKiquNkvvpLZnEqJ9sPLSnWy43oZ4+TK4kIk4nC6uZ7Kj0aufzPXNpVCWSA3hxvtGFYA==",
 "student"=>
  {"name"=>"c",
   "grade"=>"1",
   "photo"=>
    {:filename=>"win10-md5.txt",
     :type=>"text/plain",
     :name=>"student[photo]",
     :tempfile=>#<File:/tmp/RackMultipart20170830-3320-uq32df.txt>,
     :head=>"Content-Disposition: form-data; name=\"student[photo]\"; filename=\"win10-md5.txt\"\r\n" + "Content-Type: text/plain\r\n"}},
 "commit"=>"Update Student"}

而controller中的params(StrongParameters),它其实是包装了request.parameters

module StrongParameters
  extend ActiveSupport::Concern
  include ActiveSupport::Rescuable

  def params                                                                                                                                                                                                                         
    @_params ||= Parameters.new(request.parameters)
  end

而request.parameters主要是合成request_parameters(POST)和query_parameters(GET)

module ActionDispatch                                                                                                                                                                                                                   
  module Http
    module Parameters

      def parameters                                                                                                                                                                                                                    
        params = get_header("action_dispatch.request.parameters")
        return params if params

        params = begin
                   request_parameters.merge(query_parameters)
                 rescue EOFError
                   query_parameters.dup
                 end
        params.merge!(path_parameters)
        params = set_binary_encoding(params)
        set_header("action_dispatch.request.parameters", params)
        params
      end

request_parameters(刚才分析过)和query_parameters(GET)如下

module ActionDispatch
  class Request

    def GET
      fetch_header("action_dispatch.request.query_parameters") do |k|
        rack_query_params = super || {}
        # Check for non UTF-8 parameter values, which would cause errors later
        Request::Utils.check_param_encoding(rack_query_params)
        set_header k, Request::Utils.normalize_encode_params(rack_query_params)
      end
    rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
      raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
    end

    alias :query_parameters :GET

    # Override Rack's POST method to support indifferent access
    def POST
      fetch_header("action_dispatch.request.request_parameters") do
        pr = parse_formatted_parameters(params_parsers) do |params|
          super || {}
        end
        self.request_parameters = Request::Utils.normalize_encode_params(pr)
      end
    rescue Http::Parameters::ParseError # one of the parse strategies blew up
      self.request_parameters = Request::Utils.normalize_encode_params(super || {})
      raise
    rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
      raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
    end

    alias :request_parameters :POST

检查fetch_header来源,可知其最终会作用于request.env(也就是贯穿rack栈的那个hash)的fetch,因此,经normalize_encode_params包装成ActionDispatch::Http::UploadedFile的上传文件便得以在controller的params中又再取出来使用

至此可知,就是rack会读取http请求,将文件数据包装成一个File,,然后Rails又包装成ActionDispatch::Http::UploadedFile

那么rack又是怎样包装File的呢?搜索RackMultipart,然后加入断点

From: /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart/parser.rb @ line 11 :

     6:
     7:     class Parser
     8:       BUFSIZE = 16384
     9:       TEXT_PLAIN = "text/plain"
    10:       TEMPFILE_FACTORY = lambda { |filename, content_type|
 => 11:         binding.pry
    12:         Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))])
    13:       }
    14:
    15:       class BoundedIO # :nodoc:
    16:         def initialize(io, content_length)

[1] pry(Rack::Multipart::Parser)> _bsi_
=> {0=>#<Binding:70276671071600 Rack::Multipart::Parser.block in <class:Parser> /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart/parser.rb:11>,
 1=>#<Binding:70276671104460 Rack::Multipart::Parser::Collector#on_mime_head /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart/parser.rb:134>,
 2=>#<Binding:70276671129120 Rack::Multipart::Parser#handle_mime_head /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart/parser.rb:259>,
 3=>#<Binding:70276671145660 Rack::Multipart::Parser#block in run_parser /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart/parser.rb:212>,
 4=>#<Binding:70276671170340 Rack::Multipart::Parser#run_parser /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart/parser.rb:205>,
 5=>#<Binding:70276671195000 Rack::Multipart::Parser#on_read /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart/parser.rb:188>,
 6=>#<Binding:70276671219600 Rack::Multipart::Parser.parse /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart/parser.rb:69>,
 7=>#<Binding:70276671252320 Rack::Multipart.extract_multipart /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/multipart.rb:52>,
 8=>#<Binding:70276671268820 Rack::Request#parse_multipart /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/request.rb:472>,
 9=>#<Binding:70276671293420 Rack::Request#POST /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/request.rb:335>,
 10=>#<Binding:70276671318020 Rack::MethodOverride#method_override_param /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/method_override.rb:39>,
 11=>#<Binding:70276671342540 Rack::MethodOverride#method_override /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/method_override.rb:27>,
 12=>#<Binding:70276671366880 Rack::MethodOverride#call /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/method_override.rb:15>,
 13=>#<Binding:70276671391100 Rack::Runtime#call /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/runtime.rb:22>,
 14=>#<Binding:70276671407620 ActionDispatch::Executor#call /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/middleware/executor.rb:12>,
 15=>#<Binding:70276671430880 ActionDispatch::Static#call /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/actionpack-5.1.2/lib/action_dispatch/middleware/static.rb:125>,
 16=>#<Binding:70276671495580 Rack::Sendfile#call /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/rack-2.0.3/lib/rack/sendfile.rb:111>,
 17=>#<Binding:70276671518980 School::Application#call /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/railties-5.1.2/lib/rails/engine.rb:522>,
 18=>#<Binding:70276671534700 Puma::Configuration::ConfigMiddleware#call /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/puma-3.9.1/lib/puma/configuration.rb:224>,
 19=>#<Binding:70276671558460 Puma::Server#handle_request /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/puma-3.9.1/lib/puma/server.rb:602>,
 20=>#<Binding:70276671582160 Puma::Server#process_client /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/puma-3.9.1/lib/puma/server.rb:435>,
 21=>#<Binding:70276671597740 Puma::Server#block in run /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/puma-3.9.1/lib/puma/server.rb:299>,
 22=>#<Binding:70276671621740 Puma::ThreadPool#block in spawn_thread /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/puma-3.9.1/lib/puma/thread_pool.rb:120>}
[2] pry(Rack::Multipart::Parser)>