Changeset 559

Show
Ignore:
Timestamp:
06/19/08 07:32:26 (2 months ago)
Author:
mahlon
Message:
  • Remove the IO duping in favor of a full filehandle replacement, when
    the unexpected happens to handles in naughty filters. This also
    removes the need for the maintenance of body keys.

* Traded ThingFish::Request#get_body_and_metadata for

ThingFish::Request#entity_bodies as the public interface to all
top-level bodies in a given request.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • thingfish/branches/mp3-jukebox/lib/thingfish/daemon.rb

    r558 r559  
    508508        handlers.each do |handler| 
    509509            response.handlers << handler 
     510            request.check_body_ios 
    510511            handler.process( request, response ) 
    511             request.check_body_ios 
    512512            break if response.is_handled? || client.closed? 
    513513        end 
  • thingfish/branches/mp3-jukebox/lib/thingfish/exceptions.rb

    r466 r559  
    5656    class ClientError < ThingFish::Error; end 
    5757 
    58     # Something was wrong with a request 
     58    # 500: The server was unable to handle the request even though it was valid 
     59    class ServerError < ThingFish::Error 
     60        include ThingFish::Constants 
     61         
     62        def initialize( *args ) 
     63            super 
     64            @status = HTTP::SERVER_ERROR 
     65        end 
     66         
     67        attr_reader :status 
     68    end 
     69     
     70    # 501: We received a request that we don't quite know how to handle. 
     71    class NotImplementedError < ThingFish::ServerError 
     72        include ThingFish::Constants 
     73         
     74        def initialize( *args ) 
     75            super 
     76            @status = HTTP::NOT_IMPLEMENTED 
     77        end 
     78         
     79        attr_reader :status 
     80    end 
     81     
     82    # 400: Something was wrong with a request 
    5983    class RequestError < ThingFish::Error 
    6084        include ThingFish::Constants 
     
    6892    end 
    6993 
    70     # Upload exceeded quota 
     94    # 413: Upload exceeded quota 
    7195    class RequestEntityTooLargeError < ThingFish::RequestError 
    7296        include ThingFish::Constants 
     
    78102    end 
    79103     
    80     # Client requested a mimetype we don't know how to convert to 
     104    # 406: Client requested a mimetype we don't know how to convert to 
    81105    class RequestNotAcceptableError < ThingFish::RequestError 
    82106        include ThingFish::Constants 
  • thingfish/branches/mp3-jukebox/lib/thingfish/handler/default.rb

    r558 r559  
    242242    ### data (POST to /) 
    243243    def handle_create_request( request, response ) 
     244         
     245        if request.entity_bodies.length > 1 
     246            self.log.error "Can't handle multipart request (%p)" % [ request.entity_bodies ] 
     247            raise ThingFish::NotImplementedError, "multipart upload not implemented" 
     248        end 
    244249 
    245250        uuid = nil 
    246          
     251 
    247252        # Store the primary resource 
    248         body, metadata = request.get_body_and_metadata 
     253        body, metadata = request.entity_bodies.to_a.flatten 
    249254        uuid = self.daemon.store_resource( body, metadata ) 
    250255     
  • thingfish/branches/mp3-jukebox/lib/thingfish/mixins.rb

    r487 r559  
    348348                syms.each do |sym| 
    349349                    define_method( sym ) { 
    350                         raise NotImplementedError, 
     350                        raise ::NotImplementedError, 
    351351                            "%p does not provide an implementation of #%s" % [ self.class, sym ], 
    352352                            caller(1) 
  • thingfish/branches/mp3-jukebox/lib/thingfish/request.rb

    r558 r559  
    8585        @authed_user     = nil 
    8686         
    87         @body_key_mapping  = {} 
    8887        @related_resources = Hash.new {|h,k| h[k] = {} } 
    8988        @mongrel_request   = mongrel_request 
     
    262261 
    263262 
    264     ### Get the body IO and the merged hash of metadata 
    265     def get_body_and_metadata 
    266         raise ArgumentError, "Can't return a single body for a multipart request" if 
    267             self.has_multipart_body? 
    268          
    269         default_metadata = { 
    270             :useragent     => self.headers[ :user_agent ], 
    271             :uploadaddress => self.remote_addr 
    272         } 
    273  
    274         # Read title out of the content-disposition 
    275         if self.headers[:content_disposition] && 
    276             self.headers[:content_disposition] =~ /filename="(?:.*\\)?(.+?)"/i 
    277             default_metadata[ :title ] = $1 
    278         end 
    279          
    280         extracted_metadata = self.metadata[ @mongrel_request.body ] || {} 
    281  
    282         # Content metadata is determined from http headers 
    283         merged = extracted_metadata.merge({ 
    284             :format => self.content_type, 
    285             :extent => self.headers[ :content_length ], 
    286         }) 
    287         merged.update( default_metadata ) 
    288          
    289         return @mongrel_request.body, merged 
    290     end 
    291      
    292      
    293263    ### Attach additional body and metadata information to the primary 
    294264    ### body, that will be stored with related_to metakeys. 
     
    300270    ### +related_metadata+:: 
    301271    ###    The metadata to attach to the new resource, as a Hash. 
    302     def append_related_resource( body, related_body, related_metadata={} ) 
    303         # Convert the body to the key of the related resources hash 
    304         bodykey = self.make_body_key( body ) 
    305          
    306         unless original_body = @body_key_mapping[ bodykey ] 
    307             errmsg = "Cannot append a resource related to %p: %p isn't one of %p" % [ 
    308                 body, 
    309                 bodykey, 
    310                 @body_key_mapping.keys, 
     272    def append_related_resource( resource, related_resource, related_metadata={} ) 
     273 
     274        unless @entity_bodies.key?( resource ) || @related_resources.key?( resource ) 
     275            errmsg = "Cannot append %p related to %p: it is not a part of the request" % [ 
     276                related_resource, 
     277                resource, 
    311278              ] 
    312279            self.log.error( errmsg ) 
     
    314281        end 
    315282 
    316         related_bodykey = self.make_body_key( related_body ) 
    317         @body_key_mapping[ related_bodykey ] = related_body 
    318  
    319283        related_metadata[:relation] ||= 'appended' 
    320         self.related_resources[ original_body ][ related_body ] = related_metadata 
    321     end 
    322      
    323      
     284        self.related_resources[ resource ][ related_resource ] = related_metadata 
     285         
     286        # Add the related_resource as a key so future checks are aware that 
     287        # it is part of this request 
     288        self.related_resources[ related_resource ] = {} 
     289    end 
     290     
     291 
    324292    ### Append the specified additional +metadata+ for the given +resource+, which should be one 
    325293    ### of the entity bodies yielded by #each_body 
    326     ### 
    327     ### TODO: Do we need this method after bodykey removal? 
    328     ### 
    329294    def append_metadata_for( resource, metadata ) 
    330         # Convert the body to the key of the related resources hash 
    331         bodykey = self.make_body_key( resource ) 
    332          
    333         unless original_body = @body_key_mapping[ bodykey ] 
    334             errmsg = "Cannot append metadata related to %p(%p): %p isn't one of %p" % [ 
    335                 body, 
    336                 bodykey, 
     295 
     296        unless @entity_bodies.key?( resource ) || @related_resources.key?( resource ) 
     297            errmsg = "Cannot append metadata related to %p: it is not a part of the request" % [ 
    337298                resource, 
    338                 @body_key_mapping.keys, 
    339299              ] 
    340300            self.log.error( errmsg ) 
     
    342302        end 
    343303 
    344         self.metadata[ original_body ].merge!( metadata ) 
    345     end 
    346      
    347      
    348     ### Generate a key based on the body object that will be the same even after duplication. This 
    349     ### is used to work around our workaround for StringIO's behavior when #dup'ed. 
    350     def make_body_key( body ) 
    351         if body.respond_to?( :string ) 
    352             return Digest::MD5.hexdigest( body.string ) 
    353         else 
    354             return "%s:%d" % [ body.path, body.object_id * 2 ] 
    355         end 
    356     end 
    357      
    358      
     304        self.metadata[ resource ].merge!( metadata ) 
     305    end 
     306 
     307     
     308    ### Returns the entity bodies of the request along with any related metadata as 
     309    ### a Hash: 
     310    ### { 
     311    ###    <body io> => { <body metadata> }, 
     312    ###    ... 
     313    ### } 
     314    def entity_bodies 
     315        # Parse the request's body parts if they aren't already 
     316        unless @entity_bodies 
     317            if self.has_multipart_body? 
     318                self.log.debug "Parsing multiple entity bodies." 
     319                @entity_bodies, @form_metadata = self.parse_multipart_body 
     320            else 
     321                self.log.debug "Parsing single entity body." 
     322                body, metadata = self.get_body_and_metadata 
     323                 
     324                @entity_bodies = { body => metadata } 
     325                @form_metadata = {} 
     326            end 
     327 
     328            self.log.debug "Parsed %d bodies and %d form_metadata (%p)" %  
     329                [@entity_bodies.length, @form_metadata.length, @form_metadata.keys] 
     330        end 
     331 
     332        return @entity_bodies 
     333    end 
     334 
     335 
    359336    ### Call the provided block once for each entity body of the request, which may 
    360337    ### be multiple times in the case of a multipart request. If +include_appended_resources+  
     
    385362    ### Check the body IO objects to ensure they're still open. 
    386363    def check_body_ios 
    387         [ self.entity_bodies, self.related_resources ].each do |hash| 
    388             hash.each do |body, _| 
    389                 if body.closed? 
    390                      
    391                     # TODO asap:  :) 
    392                     # substitute body for a fresh and clean filehandle, 
    393                     # since filehandle closure has been such a poopy problem 
    394                     # in the past.  this will remove the need for bodykeys, as well. 
    395                      
    396                     self.log.warn "Entity body closed: %p" % [ body ] 
    397                     body.open  
     364        self.each_body do |body,_| 
     365            if body.closed? 
     366                self.log.warn "Body IO unexpectedly closed -- reopening a new handle" 
     367                                 
     368                # Create a new IO based on what the original type was 
     369                clone = case body 
     370                    when StringIO 
     371                        StringIO.new( body.string ) 
     372                    else 
     373                        File.open( body.path, 'r' ) 
     374                    end 
     375                 
     376                # Retain the original IO's metadata 
     377                @entity_bodies[ clone ] = @entity_bodies.delete( body ) if @entity_bodies.key?( body ) 
     378                @related_resources[ clone ] = @related_resources.delete( body ) if @related_resources.key?( body ) 
     379                @related_resources.each do |_,hash| 
     380                    hash[ clone ] = hash.delete( body ) if hash.key?( body ) 
    398381                end 
    399                  
     382 
     383                self.log.debug "Body %p (%d) replaced with %p (%d)" % [  
     384                    body, body.object_id, clone, clone.object_id 
     385                ] 
     386            else 
    400387                body.rewind 
    401388            end 
     
    512499    ######### 
    513500 
    514     ### Returns the entity bodies of the request along with any related metadata as 
    515     ### a Hash: 
    516     ### { 
    517     ###    <body io> => { <body metadata> }, 
    518     ###    ... 
    519     ### } 
    520     def entity_bodies 
    521         # Parse the request's body parts if they aren't already 
    522         unless @entity_bodies 
    523             if self.has_multipart_body? 
    524                 self.log.debug "Parsing multiple entity bodies." 
    525                 @entity_bodies, @form_metadata = self.parse_multipart_body 
    526             else 
    527                 self.log.debug "Parsing single entity body." 
    528                 body, metadata = self.get_body_and_metadata 
    529                  
    530                 @entity_bodies = { body => metadata } 
    531                 @form_metadata = {} 
    532             end 
    533  
    534             # Generate keys for each body that can be used to map IO copies given to filters 
    535             # back to the original body. 
    536             @entity_bodies.each do |body, _| 
    537                 bodykey = self.make_body_key( body ) 
    538                 @body_key_mapping[ bodykey ] = body 
    539                 self.log.debug "Made body key %p from body %p" % [ bodykey, body ] 
    540             end 
    541  
    542             self.log.debug "Parsed %d bodies and %d form_metadata (%p)" %  
    543                 [@entity_bodies.length, @form_metadata.length, @form_metadata.keys] 
    544         end 
    545  
    546         return @entity_bodies 
    547     end 
    548  
    549  
     501    ### Get the body IO and the merged hash of metadata 
     502    def get_body_and_metadata 
     503        raise ArgumentError, "Can't return a single body for a multipart request" if 
     504            self.has_multipart_body? 
     505         
     506        default_metadata = { 
     507            :useragent     => self.headers[ :user_agent ], 
     508            :uploadaddress => self.remote_addr 
     509        } 
     510 
     511        # Read title out of the content-disposition 
     512        if self.headers[:content_disposition] && 
     513            self.headers[:content_disposition] =~ /filename="(?:.*\\)?(.+?)"/i 
     514            default_metadata[ :title ] = $1 
     515        end 
     516         
     517        extracted_metadata = self.metadata[ @mongrel_request.body ] || {} 
     518 
     519        # Content metadata is determined from http headers 
     520        merged = extracted_metadata.merge({ 
     521            :format => self.content_type, 
     522            :extent => self.headers[ :content_length ], 
     523        }) 
     524        merged.update( default_metadata ) 
     525         
     526        return @mongrel_request.body, merged 
     527    end 
     528     
     529     
    550530    ### For each resource => metadata pair returned by the current +iterator+, merge the 
    551531    ### toplevel metadata with the resource-specific metadata and pass both to the 
     
    556536        # Call the block for every resource 
    557537        iterator.each do |body, body_metadata| 
    558             self.log.debug "Prepping %s for yield with metadata: %p" % 
    559                 [ body, body_metadata ] 
    560538            body_metadata[ :format ] ||= DEFAULT_CONTENT_TYPE 
    561539            extracted_metadata = self.metadata[body] || {} 
     
    566544            merged.update( immutable_metadata ) 
    567545 
    568             # We have to explicitly case this because StringIO doesn't behave like a 
    569             # real IO when #dup'ed; closing the original also closes the copy. 
    570             clone = case body 
    571                 when StringIO 
    572                     StringIO.new( body.string ) 
    573                 else 
    574                     body.dup 
    575                 end 
    576  
    577             @body_key_mapping[ self.make_body_key(clone) ] = body 
    578             block.call( clone, merged ) 
     546            block.call( body, merged ) 
    579547             
    580548            # Recurse if the appended resources should be included 
  • thingfish/branches/mp3-jukebox/spec/thingfish/handler/default_spec.rb

    r558 r559  
    109109 
    110110        full_metadata = mock( "metadata fetched from the store", :null_object => true ) 
    111         @metastore.should_receive( :get_properties ). 
    112             and_return( full_metadata ) 
    113  
    114         @request.should_receive( :get_body_and_metadata ).and_return([ body, metadata ]) 
     111        @metastore.should_receive( :get_properties ).and_return( full_metadata ) 
     112 
     113        @request.should_receive( :entity_bodies ).twice.and_return({ body => metadata }) 
    115114 
    116115        @response_headers.should_receive( :[]= ). 
     
    137136        md = stub( "metadata hash" ) 
    138137         
    139         @request.should_receive( :get_body_and_metadata ).and_return([ body, md ]
     138        @request.should_receive( :entity_bodies ).twice.and_return({ body => md }
    140139        @daemon.should_receive( :store_resource ). 
    141140            with( body, md ). 
     
    175174    end 
    176175     
     176 
     177    it "sends a NOT_IMPLEMENTED response for multipart POST to /" do 
     178        uri = URI.parse( "http://thingfish.laika.com:3474/" ) 
     179        @request.should_receive( :uri ).at_least( :once ).and_return( uri ) 
     180 
     181        @request.should_receive( :entity_bodies ).twice.and_return({ :body1 => :md1, :body2 => :md2 }) 
     182 
     183        lambda { 
     184            @handler.handle_post_request( @request, @response ) 
     185        }.should raise_error( ThingFish::NotImplementedError, /not implemented/ ) 
     186    end 
    177187 
    178188     
  • thingfish/branches/mp3-jukebox/spec/thingfish/request_spec.rb

    r554 r559  
    220220 
    221221        upload = mock( "Mock Upload Tempfile" ) 
    222         upload.should_receive( :path ).and_return( TEMPFILE_PATH ) 
    223         duped_upload = mock( "Mock Upload Tempfile duplicate" ) 
    224         duped_upload.should_receive( :path ).at_least( :once ).and_return( TEMPFILE_PATH ) 
    225          
    226         upload.should_receive( :dup ).and_return( duped_upload ) 
    227          
    228222        @mongrel_request.stub!( :params ).and_return( params ) 
    229223        @mongrel_request.stub!( :body ).and_return( upload ) 
     
    266260        request.each_body do |body, metadata| 
    267261            request.append_related_resource( body, generated_resource, metadata ) 
     262            ThingFish.logger.debug "Request related resources is now: %p" % [ request.related_resources ] 
    268263            request.append_related_resource( generated_resource, sub_generated_resource, sub_metadata ) 
    269264        end 
     
    656651           "of the resource iterator" do 
    657652            io1 = mock( "filehandle 1" ) 
    658             io1_dup = mock( "duplicated filehandle 1" ) 
    659  
    660653            io2 = mock( "filehandle 2" ) 
    661             io2_dup = mock( "duplicated filehandle 2" ) 
    662654 
    663655            resource1 = mock( "extracted body 1" ) 
    664             resource1_dup = mock( "duplicated extracted body 1" ) 
    665656 
    666657            parser = mock( "multipart parser", :null_object => true ) 
     
    681672                and_return([ entity_bodies, form_metadata ]) 
    682673 
    683             io1.should_receive( :dup ).at_least(:once).and_return( io1_dup ) 
    684             io1.stub!( :path ).and_return( :a_path ) 
    685             io1_dup.stub!( :path ).and_return( :a_path ) 
    686             io2.should_receive( :dup ).at_least(:once).and_return( io2_dup ) 
    687             io2.stub!( :path ).and_return( :another_path ) 
    688             io2_dup.stub!( :path ).and_return( :another_path ) 
    689             resource1.should_receive( :dup ).at_least(:once).and_return( resource1_dup ) 
    690             resource1.stub!( :path ).and_return( :a_third_path ) 
    691             resource1_dup.stub!( :path ).and_return( :a_third_path ) 
    692  
    693674            yielded_pairs = {} 
    694675            @request.each_body( true ) do |res, parsed_metadata| 
    695                 if res == io1_dup 
     676                if res == io1 
    696677                    thumb_metadata = { 
    697678                        :relation => 'thumbnail', 
     
    699680                        :title    => 'filename1_thumb.jpg', 
    700681                      } 
    701                     @request.append_related_resource( io1_dup, resource1, thumb_metadata ) 
     682                    @request.append_related_resource( io1, resource1, thumb_metadata ) 
    702683                end 
    703684                     
     
    706687 
    707688            yielded_pairs.keys.should have(3).members 
    708             yielded_pairs.keys.should include( io1_dup ) 
    709             yielded_pairs.keys.should include( io2_dup ) 
    710             yielded_pairs.keys.should include( resource1_dup ) 
    711  
    712             yielded_pairs[ io1_dup ][ :title ].should == 'filename1' 
    713             yielded_pairs[ io1_dup ][ :format ].should == 'format1' 
    714             yielded_pairs[ io1_dup ][ :useragent ].should == "Hotdogs" 
    715             yielded_pairs[ io1_dup ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' ) 
    716  
    717             yielded_pairs[ io2_dup ][ :title ].should == 'filename2' 
    718             yielded_pairs[ io2_dup ][ :format ].should == "format2" 
    719             yielded_pairs[ io2_dup ][ :useragent ].should == "Hotdogs" 
    720             yielded_pairs[ io2_dup ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' )   
    721  
    722             yielded_pairs[ resource1_dup ][ :title ].should == 'filename1_thumb.jpg' 
    723             yielded_pairs[ resource1_dup ][ :format ].should == 'image/jpeg' 
    724             yielded_pairs[ resource1_dup ][ :useragent ].should == "Hotdogs" 
    725             yielded_pairs[ resource1_dup ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' )     
     689            yielded_pairs.keys.should include( io1, io2, resource1 ) 
     690 
     691            yielded_pairs[ io1 ][ :title ].should == 'filename1' 
     692            yielded_pairs[ io1 ][ :format ].should == 'format1' 
     693            yielded_pairs[ io1 ][ :useragent ].should == "Hotdogs" 
     694            yielded_pairs[ io1 ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' ) 
     695 
     696            yielded_pairs[ io2 ][ :title ].should == 'filename2' 
     697            yielded_pairs[ io2 ][ :format ].should == "format2" 
     698            yielded_pairs[ io2 ][ :useragent ].should == "Hotdogs" 
     699            yielded_pairs[ io2 ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' )   
     700 
     701            yielded_pairs[ resource1 ][ :title ].should == 'filename1_thumb.jpg' 
     702            yielded_pairs[ resource1 ][ :format ].should == 'image/jpeg' 
     703            yielded_pairs[ resource1 ][ :useragent ].should == "Hotdogs" 
     704            yielded_pairs[ resource1 ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' )     
    726705        end 
    727706     
     
    729708            "the block of the body iterator" do 
    730709            io1 = mock( "filehandle 1" ) 
    731             io1_dup = mock( "duplicated filehandle 1" ) 
    732              
    733710            io2 = mock( "filehandle 2" ) 
    734             io2_dup = mock( "duplicated filehandle 2" ) 
    735711 
    736712            parser = mock( "multipart parser", :null_object => true ) 
     
    751727                and_return([ entity_bodies, form_metadata ]) 
    752728             
    753             io1.should_receive( :dup ).and_return( io1_dup ) 
    754             io1.stub!( :path ).and_return( :a_path ) 
    755             io1_dup.stub!( :path ).and_return( :another_path ) 
    756             io2.should_receive( :dup ).and_return( io2_dup ) 
    757             io2.stub!( :path ).and_return( :another_path ) 
    758             io2_dup.stub!( :path ).and_return( :another_path ) 
    759          
    760729            yielded_pairs = {} 
    761730            @request.each_body do |body, parsed_metadata| 
     
    764733         
    765734            yielded_pairs.keys.should have(2).members 
    766             yielded_pairs.keys.should include( io1_dup ) 
    767             yielded_pairs.keys.should include( io2_dup ) 
    768  
    769             yielded_pairs[ io1_dup ][ :title ].should == 'filename1' 
    770             yielded_pairs[ io1_dup ][ :format ].should == 'format1' 
    771             yielded_pairs[ io1_dup ][ :useragent ].should == "Hotdogs" 
    772             yielded_pairs[ io1_dup ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' ) 
    773  
    774             yielded_pairs[ io2_dup ][ :title ].should == 'filename2' 
    775             yielded_pairs[ io2_dup ][ :format ].should == "format2" 
    776             yielded_pairs[ io2_dup ][ :useragent ].should == "Hotdogs" 
    777             yielded_pairs[ io2_dup ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' )   
    778         end 
    779      
    780  
    781         it "creates distinct duplicates for StringIO bodies" do 
    782             io1 = StringIO.new("foom!") 
    783             io2 = StringIO.new("DOOOOOM") 
    784              
    785             parser = mock( "multipart parser", :null_object => true ) 
    786             entity_bodies = { 
    787                 io1 => {:title  => "filename1",:format => "format1",:extent => 100292}, 
    788                 io2 => {:title  => "filename2",:format => "format2",:extent => 100234} 
    789               } 
    790             form_metadata = { 
    791                 'foo' => 1, 
    792                 :title => "a bogus filename", 
    793                 :useragent => 'Clumpy the Clown', 
    794               } 
    795  
    796             ThingFish::MultipartMimeParser.stub!( :new ).and_return( parser ) 
    797             @mongrel_request.should_receive( :body ).once.and_return( :body ) 
    798             parser.should_receive( :parse ).once. 
    799                 with( :body, 'greatgoatsofgerta' ). 
    800                 and_return([ entity_bodies, form_metadata ]) 
    801              
    802             @request.each_body do |body, parsed_metadata| 
    803                 body.read     # modify the pointer on the duped StringIO 
    804             end 
    805          
    806             io1.pos.should == 0 
    807             io2.pos.should == 0 
    808         end 
    809          
    810          
     735            yielded_pairs.keys.should include( io1, io2 ) 
     736 
     737            yielded_pairs[ io1 ][ :title ].should == 'filename1' 
     738            yielded_pairs[ io1 ][ :format ].should == 'format1' 
     739            yielded_pairs[ io1 ][ :useragent ].should == "Hotdogs" 
     740            yielded_pairs[ io1 ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' ) 
     741 
     742            yielded_pairs[ io2 ][ :title ].should == 'filename2' 
     743            yielded_pairs[ io2 ][ :format ].should == "format2" 
     744            yielded_pairs[ io2 ][ :useragent ].should == "Hotdogs" 
     745            yielded_pairs[ io2 ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' )   
     746        end 
     747     
     748 
    811749        it "ensures each part sent to the body has the default content-type " + 
    812750           "if none is explicitly provided by the request" do 
    813751            io1 = mock( "filehandle 1" ) 
    814             io1_dup = mock( "duplicated filehandle 1" ) 
    815752            io2 = mock( "filehandle 2" ) 
    816             io2_dup = mock( "duplicated filehandle 2" ) 
    817753 
    818754            parser = mock( "multipart parser", :null_object => true ) 
     
    833769                and_return([ entity_bodies, form_metadata ]) 
    834770 
    835             io1.should_receive( :dup ).and_return( io1_dup ) 
    836             io1.stub!( :path ).and_return( :a_path ) 
    837             io1_dup.stub!( :path ).and_return( :a_path ) 
    838             io2.should_receive( :dup ).and_return( io2_dup ) 
    839             io2.stub!( :path ).and_return( :another_path ) 
    840             io2_dup.stub!( :path ).and_return( :another_path ) 
    841  
    842771            yielded_pairs = {} 
    843772            @request.each_body do |body, parsed_metadata| 
     
    846775 
    847776            yielded_pairs.keys.should have(2).members 
    848             yielded_pairs.keys.should include( io1_dup ) 
    849             yielded_pairs.keys.should include( io2_dup ) 
    850  
    851             yielded_pairs[ io1_dup ][ :title ].should == 'filename1' 
    852             yielded_pairs[ io1_dup ][ :format ].should == DEFAULT_CONTENT_TYPE 
    853             yielded_pairs[ io1_dup ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' ) 
    854             yielded_pairs[ io2_dup ][ :title ].should == 'filename2' 
    855             yielded_pairs[ io2_dup ][ :format ].should == DEFAULT_CONTENT_TYPE 
    856             yielded_pairs[ io2_dup ][ :useragent ].should == "Hotdogs" 
    857             yielded_pairs[ io2_dup ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' )   
     777            yielded_pairs.keys.should include( io1, io2 ) 
     778 
     779            yielded_pairs[ io1 ][ :title ].should == 'filename1' 
     780            yielded_pairs[ io1 ][ :format ].should == DEFAULT_CONTENT_TYPE 
     781            yielded_pairs[ io1 ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' ) 
     782            yielded_pairs[ io2 ][ :title ].should == 'filename2' 
     783            yielded_pairs[ io2 ][ :format ].should == DEFAULT_CONTENT_TYPE 
     784            yielded_pairs[ io2 ][ :useragent ].should == "Hotdogs" 
     785            yielded_pairs[ io2 ][ :uploadaddress ].should == IPAddr.new( '127.0.0.1' )   
    858786        end 
    859787