Changeset 560

Show
Ignore:
Timestamp:
06/19/08 07:41:58 (2 months ago)
Author:
mahlon
Message:

Merged ThingFish "engine" work done in the
mp3-jukebox branch.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • thingfish/trunk/Rakefile

    r556 r560  
    9494Pathname.glob( RAKE_TASKDIR + '*.rb' ).each do |tasklib| 
    9595    trace "Loading task lib #{tasklib}" 
    96     begin 
    97         require tasklib 
    98     rescue => err 
    99         fail "Tasklib #{tasklib}: #{err.message}" 
    100     end 
     96    require tasklib 
    10197end 
    10298 
     
    136132end 
    137133 
     134task :clobber_manual do 
     135    rmtree( targetdir, :verbose => true ) 
     136end 
     137 
    138138 
    139139### Cruisecontrol task 
  • thingfish/trunk/docs/manual/layouts/default.page

    r540 r560  
    77     
    88    <script type="text/javascript" src="/js/jquery.js"></script> 
     9    <script type="text/javascript" src="/js/manual.js"></script> 
    910    <script type="text/javascript" src="/js/jquery.ThickBox.js"></script> 
    1011 
  • thingfish/trunk/docs/manual/src/Developers_Guide/connecting.page

    r549 r560  
    1212h2. <%= page.config['title'] %> 
    1313 
     14<div id="auto-toc" /> 
     15 
    1416There are several options for connecting: you can use a client library if one has 
    1517been written for your language, or you can use HTTP to talk to the server directly. 
     
    1719h3. The Basics 
    1820 
    19 To test out the particulars of your connection, you can connect to the daemon with a 
    20 web browser by pointing it at the URL: 
     21To test out the particulars of your connection, if you have the @html@ filter loaded, you can 
     22connect to the daemon with a web browser by pointing it at the URL: 
    2123 
    2224<pre> 
     
    4244    <dd> 
    4345      Fetch information about the server. The format of the information depends on the requested 
    44       type -- a @text/html@ request will display a server overview for web browsers, while 
    45       code-centric content types like @application/json@ and @text/x-yaml@ will return a serialized 
    46       hash of server information. This information can be later used to build REST queries to 
    47       access ThingFish's other services. 
     46      type -- a @text/html@ request will display a server overview for web browsers (assuming the 
     47      @html@ filter is loaded), while code-centric content types like @application/json@ and 
     48      @text/x-yaml@ will return a serialized hash of server information (assuming the @json@ and 
     49      @yaml@ filters, respectively, are loaded). This information can later be used to build REST 
     50      queries to access ThingFish's other services. 
    4851 
    4952<?example { language: ruby, caption: "Server introspection hash" } ?> 
  • thingfish/trunk/docs/manual/src/Developers_Guide/index.page

    r546 r560  
    1515* <?link The Javascript Client Library ?> 
    1616* <?link Writing Your Own Client Library ?> 
     17* <?link ThingFish Usage Cookbook ?> 
  • thingfish/trunk/docs/manual/src/Developers_Guide/ruby-client.page

    r547 r560  
    1010 
    1111h2. <%= page.config['title'] %> 
     12 
     13<div id="auto-toc" /> 
    1214 
    1315The @ThingFish::Client@ class is the main Ruby interface to the ThingFish store. It can be 
  • thingfish/trunk/docs/manual/src/Hackers_Guide/server-architecture.page

    r547 r560  
    1111 
    1212h2. <%= page.config['title'] %> 
     13 
     14<div id="auto-toc" /> 
    1315 
    1416The server has five major components: 
  • thingfish/trunk/docs/manual/src/Hackers_Guide/writing-filestores.page

    r543 r560  
    1111h2. <%= page.config['title'] %> 
    1212 
    13 Â« Coming Soon! » 
     13<div id="auto-toc" /> 
     14 
     15The back end of a ThingFish instance... 
     16 
     17h3. What's a FileStore do? 
     18 
     19h3. The mandatory interface 
     20 
     21h3. Optional interface 
    1422 
    1523 
     24 
     25 
  • thingfish/trunk/docs/manual/src/Hackers_Guide/writing-filters.page

    r550 r560  
    1111 
    1212h2. <%= page.config['title'] %> 
     13 
     14<div id="auto-toc" /> 
    1315 
    1416h3. How's a filter different than a handler? 
     
    2830a JPEG via the client's @Accept@ header. 
    2931 
    30 h3. Rest of this coming soon... 
     32 
     33h3. What does a filter look like? 
     34 
     35Great question!  Here's one that we'll use to point out some filtery concepts. 
     36 
     37<?example { language: ruby, caption: A minimal filter. } ?> 
     38require 'thingfish/constants' 
     39require 'thingfish/filter' 
     40 
     41class ThingFish::YAMLFilter < ThingFish::Filter 
     42        include ThingFish::Constants 
     43 
     44        HANDLED_TYPES = [ ThingFish::AcceptParam.parse(RUBY_MIMETYPE) ] 
     45        YAML_MIMETYPE = 'text/x-yaml' 
     46 
     47        ### Send the local time to the requester in plain text. 
     48        def handle_get_request( request, response ) 
     49                response.body   = Time.now.to_s 
     50                response.headers[:content_type] = 'text/plain' 
     51                response.status = HTTP::OK 
     52        end 
     53end 
     54<?end example?> 
     55 
     56|_. line number |_. what's it doing? | 
     57| 4 | All handlers inherit from the @ThingFish::Handler@ parent class. 
     58The handler name needs to match the filename it is stored in - 
     59in this case, the file would be called _time.rb_, and stored in a _lib/handler_ directory. | 
     60| 5 | Import some useful constants, like the @HTTP::OK@ a little further down | 
     61| 8 | Actions are defined in special methods that match the naming convention 
     62@handle_«METHOD»_request@.  We're only going to accept @GET@ methods for this example. 
     63These special methods receive a @ThingFish::Request@@ and a @ThingFish::Response@ object  
     64to fiddle with as you see fit. | 
     65| 9 | We're just using a simple time string as the response here, but this could just as  
     66easily be an erb template, or even an @IO@ object.  @IO@ objects will automatically buffer out  
     67to the client. | 
     68| 10 | Set the content type of the response. | 
     69| 11 | The default response code is a @404 (Not Found)@; after we're sure we've generated 
     70the content we want, setting this to @OK@ stops further processing, and lets the response  
     71through successfully. | 
     72 
     73h3. Installing the filter 
     74 
     75Alright, now that you've got this wicked cool Time handler, how do we tell ThingFish 
     76to actually use it?  So far, all we really need to do is to load it when a particular 
     77URI is requested.  This is done in the @thingfish.conf@ file, under the @handlers@ 
     78section.  If you wanted to install it under a single URI called @/what-time-is-it@, 
     79here's how you'd do it: 
     80 
     81<?example { language: yaml, caption: "Our handler's config section" } ?> 
     82plugins: 
     83    handlers: 
     84        - time: /what-time-is-it 
     85<?end example?> 
     86 
     87 
     88h3. Appending Related Resources 
     89 
     90(Describe how to append things like thumbnails, previews, etc.) 
     91 
  • thingfish/trunk/docs/manual/src/Hackers_Guide/writing-handlers.page

    r547 r560  
    55filters: 
    66  - erb 
     7  - api 
     8  - links 
    79  - examples 
    810  - textile 
     
    1012 
    1113h2. <%= page.config['title'] %> 
     14 
     15<div id="auto-toc" /> 
    1216 
    1317h3. What does a handler look like? 
     
    163167        ThingFish::ResourceLoader 
    164168 
    165     ### Send the local time to the requester in plain text
     169    ### Send the local time to the requester
    166170    def handle_get_request( request, response ) 
    167171 
     
    195199<?end example?> 
    196200 
     201|_. line number |_. what's it doing? | 
     202| 8 | Including the @ThingFish::ResourceLoader@ mixin sets up the resource directory for 
     203this handler and adds a few methods for dealing with files contained in it. | 
     204| 15 | Load the HTML file we're going to use as a template. The resources directory for  
     205each plugin is in a @thingfish@ directory under Ruby's @datadir@ by default. Each plugin  
     206gets its own directory based on its name, so our TimeHandler plugin's resource directory 
     207is @#{datadir}/thingfish/timehandler/@. This can be changed by setting the @resource_dir@ 
     208key in the handler's config. | 
     209| 33 | Interpolate the calculated @timestring@ into the template where the first @%s@ is. | 
     210 
    197211And the template we'll use looks like: 
    198212 
     
    223237<?end example?> 
    224238 
    225 |_\2. Handler Code | 
    226 | 8 | Including the @ThingFish::ResourceLoader@ mixin sets up the resource directory for 
    227 this handler and adds a few methods for dealing with files contained in it. | 
    228 | 15 | Load the HTML file we're going to use as a template. The resources directory for  
    229 each plugin is in a @thingfish@ directory under Ruby's @datadir@ by default. Each plugin  
    230 gets its own directory based on its name, so our TimeHandler plugin's resource directory 
    231 is @#{datadir}/thingfish/timehandler/@. This can be changed by setting the @resource_dir@ 
    232 key in the handler's config. | 
    233 | 33 | Interpolate the calculated @timestring@ into the template where the first @%s@ is. | 
    234 |_\2. Template (@timehandler.html@) | 
     239|_. line number |_. what's it doing? | 
    235240| 13 | Really Big Fonts -- +10 pts on the Web 2.0 scorecard! | 
    236241| 20 | This is where @timestring@ will be inserted. | 
     
    265270        ThingFish::ResourceLoader 
    266271 
    267     ### Send the local time to the requester in plain text
     272    ### Send the local time to the requester
    268273    def handle_get_request( request, response ) 
    269274 
     
    297302<?end example?> 
    298303 
     304|_. line number |_. what's it doing? | 
     305| 7 | Still including the @ThingFish::ResourceLoader@ mixin. | 
     306| 15-16 | Set the page's title as a simple variable that will be in the Binding; the  
     307@timestring@ gets set as before, and is accessable from the template as well. | 
     308| 33 | Pass the Binding for the current scope into the ERb template and set the response's  
     309body to the rendered output. | 
     310 
    299311And then we'll change the @%s@ placeholders into ERb directives in the template: 
    300312 
     
    325337<?end example?> 
    326338 
    327 |_\2. Handler Code | 
    328 | 7 | Still including the @ThingFish::ResourceLoader@ mixin. | 
    329 | 15-16 | Set the page's title as a simple variable that will be in the Binding; the  
    330 @timestring@ gets set as before, and is accessable from the template as well. | 
    331 | 33 | Pass the Binding for the current scope into the ERb template and set the response's  
    332 body to the rendered output. | 
    333 |_\2. Template (@timehandler.rhtml@) | 
     339|_. line number |_. what's it doing? | 
    334340| 7  | We'll set the @title@ dynamically too now. | 
    335341| 20 | This will get the @timestring@ just like before. | 
    336342 
    337343 
    338 h3. Linking to Other Handlers 
     344h3. Serving Static Content 
     345 
     346If your handler has images, javascript, styles, or other static resources you'd like to link from 
     347the HTML you generate, you can do so easily with the <?api ThingFish::StaticResourcesHandler ?> 
     348mixin. This mixin installs a fallthrough handler that will serve files from a subdirectory inside of 
     349the handler's @resources@ directory. 
     350 
     351<?example { language: ruby, caption: A handler that also serves static content. } ?> 
     352require 'thingfish/constants' 
     353require 'thingfish/handler' 
     354require 'thingfish/mixins' 
     355require 'time' 
     356 
     357class TimeHandler < ThingFish::Handler 
     358    include ThingFish::Constants, 
     359        ThingFish::ResourceLoader, 
     360        ThingFish::StaticResourcesHandler 
     361 
     362    # Set the name of the static content subdirectory 
     363    static_resources_dir "static_content" 
     364 
     365    ### Send the local time to the requester. 
     366    def handle_get_request( request, response ) 
     367 
     368        uri  = request.path_info 
     369 
     370        # Calculate the time string based on the URI like before 
     371        case uri 
     372        when '', '/' 
     373            title = "TimeServer 3000" 
     374            timestring = Time.now.to_s 
     375            handler_uri = self.find_handler_uris.first 
     376 
     377            template = self.get_erb_resource( 'timehandler.rhtml' ) 
     378            response.body = template.result( binding() ) 
     379 
     380            response.headers[:content_type] = 'text/html' 
     381            response.status = HTTP::OK 
     382 
     383        else 
     384         
     385            # Fall through to the static handler 
     386            return 
     387        end 
     388 
     389    end 
     390end 
     391<?end example?> 
     392 
     393|_. line number |_. what's it doing? | 
     394| 9 | Include the static resources fallback handler. | 
     395| 12 | Set the name of the directory that will contain static content specific to this handler.  
     396The path is relative to the handler's @resource_dir@. If this is not specified, it defaults to 
     397@'static'@. | 
     398| 35 | Normally, this declines the request and would result in a NOT_FOUND. Instead, because of the 
     399ThingFish::StaticResourcesHandler mixin, if the request matches a file in the static resources 
     400directory, it will be served. | 
     401 
     402In the above example, if a request came in for @/what-time-is-it/clock.jpg@, the handler would initially decline it, and the static handler would take over.  If there was a file at 
     403@static_content/clock.jpg@ under the handler's configured resources directory, the static handler 
     404would determine the correct mimetype and serve it. 
     405 
     406 
     407h3. Finding Other Handler URIs 
    339408 
    340409Sometimes you'll want one handler to provide a link to another handler, but since the URI 
     
    379448        ThingFish::ResourceLoader 
    380449 
    381     ### Send the local time to the requester in plain text
     450    ### Send the local time to the requester
    382451    def handle_get_request( request, response ) 
    383452 
     
    424493h3. Index page content 
    425494 
     495When ThingFish is set up to provide an HTML interface (by enabling the HTML filter), requests to 
     496the @/@ URI will return an HTML page that serves as an index of all the functionality offered by  
     497the server. If you wish your handler to appear in this list, you should override the  
     498@make_index_content@ method and return an HTML fragment that includes a link to the handler's 
     499URI: 
     500 
    426501<?example { language: ruby, caption: A simple index page content provider callback } ?> 
    427502def make_index_content( uri ) 
    428     return "Oh snap!  What <a href=\"#{uri}\">time</a> is it?<br />" 
    429 end 
    430 <?end example?> 
     503    return "<p>Oh snap!  What <a href=\"#{uri}\">time</a> is it?</p>" 
     504end 
     505<?end example?> 
     506 
    431507 
    432508h3. Content Negotiation 
  • thingfish/trunk/docs/manual/src/Hackers_Guide/writing-metastores.page

    r543 r560  
    1111h2. <%= page.config['title'] %> 
    1212 
     13 
     14Differences between simple metastores and advanced 
     15 
     16strong data typing (date range searches) 
     17multiple values for a metakey (tags) 
     18native syntax support for POSTed search (SparQL, SQL, etc.) 
     19 
    1320« Coming Soon! » 
    1421 
  • thingfish/trunk/docs/manual/src/getting-started.page

    r547 r560  
    1111h1. <%= page.config['title'] %> 
    1212 
     13<div id="auto-toc" /> 
     14 
    1315h2. Installation 
    1416 
     
    112114test.  You'll probably want to get more elaborate with your configuration fairly  
    113115quickly, however. 
     116 
     117Here's an example of a fairly full-featured config: 
    114118 
    115119<?example { language: yaml, caption: Example comprehensive config file } ?> 
     
    137141        root: /home/thingfish/var 
    138142    metastore: 
    139         name: marshalled 
    140         root: /home/thingfish/var 
     143        name: sequel 
     144        sequel_connect: postgres://thingfish@localhost/tf 
    141145    filters: 
    142146        - html 
     
    153157            uris: /upload 
    154158            resource_dir: /home/thingfish/plugins/thingfish-formuploadhandler/resources 
    155         - metadata: 
     159        - simplemetadata: 
    156160            uris: /metadata 
    157             resource_dir: /home/thingfish/var/www 
     161            resource_dir: var/www 
     162        - simplesearch: 
     163            uris: /search 
     164            resource_dir: var/www 
    158165        - status: 
    159166            uris: /status 
  • thingfish/trunk/docs/manual/src/index.page

    r546 r560  
    1212 
    1313h1. <%= page.config['title'] %> 
     14 
     15<div id="auto-toc" /> 
    1416 
    1517ThingFish is a network-accessable, searchable, extensible datastore. It can be used to store chunks 
  • thingfish/trunk/lib/thingfish/daemon.rb

    r548 r560  
    235235    end 
    236236     
     237     
     238    ### Recursively store all resources in the specified +request+ that are related to +body+. 
     239    ### Use the specified +uuid+ for the 'related_to' metadata value. 
     240    def store_related_resources( body, uuid, request ) 
     241        request.related_resources[ body ].each do |related_resource, related_metadata| 
     242            related_metadata[ :related_to ] = uuid 
     243            metadata = request.metadata[related_resource].merge( related_metadata ) 
     244 
     245            subuuid = self.store_resource( related_resource, metadata ) 
     246            self.store_related_resources( related_resource, subuuid, request ) 
     247        end 
     248    rescue => err 
     249        self.log.error "%s while storing related resource: %s" % [ err.class.name, err.message ] 
     250        self.log.debug "Backtrace: %s" % [ err.backtrace.join("\n  ") ] 
     251    end 
     252     
     253     
     254    ### Remove any resources that have a +related_to+ of the given UUID, and a +relation+ of  
     255    ### 'appended'. 
     256    def purge_related_resources( uuid ) 
     257        uuids = @metastore.find_by_exact_properties( :related_to => uuid.to_s, :relation => 'appended' ) 
     258        uuids.each do |subuuid| 
     259            self.log.debug "purging appended resource %s for %s" % [ subuuid, uuid ] 
     260            @metastore.delete_resource( subuuid ) 
     261            @filestore.delete( subuuid ) 
     262        end 
     263    end 
     264     
     265     
    237266 
    238267    ######### 
     
    350379        self.send_error_response( response, request, err.status, client, err ) 
    351380 
    352     rescue => err 
     381    rescue Exception => err 
    353382        self.send_error_response( response, request, HTTP::SERVER_ERROR, client, err ) 
    354          
    355383    end 
    356384     
     
    362390            begin 
    363391                filter.handle_request( request, response ) 
    364             rescue => err 
     392            rescue Exception => err 
    365393                self.log.error "Request filter raised a %s: %s" %  
    366394                    [ err.class.name, err.message ] 
     
    384412            begin 
    385413                filter.handle_response( response, request ) 
    386             rescue => err 
     414            rescue Exception => err 
    387415                self.log.error "Response filter raised a %s: %s" %  
    388416                    [ err.class.name, err.message ] 
     
    480508        handlers.each do |handler| 
    481509            response.handlers << handler 
     510            request.check_body_ios 
    482511            handler.process( request, response ) 
    483             request.check_body_ios 
    484512            break if response.is_handled? || client.closed? 
    485513        end 
  • thingfish/trunk/lib/thingfish/exceptions.rb

    r466 r560  
    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/trunk/lib/thingfish/handler/default.rb

    r464 r560  
    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          
    247         body, metadata = request.get_body_and_metadata 
     251 
     252        # Store the primary resource 
     253        body, metadata = request.entity_bodies.to_a.flatten 
    248254        uuid = self.daemon.store_resource( body, metadata ) 
     255     
     256        # Store any related resources, linked to the primary 
     257        self.daemon.store_related_resources( body, uuid, request ) 
    249258 
    250259        response.status = HTTP::CREATED 
     
    267276        body, metadata = request.get_body_and_metadata 
    268277        self.daemon.store_resource( body, metadata, uuid ) 
     278         
     279        # Purge any old related resources, then store any new ones linked to the primary 
     280        self.daemon.purge_related_resources( uuid ) 
     281        self.daemon.store_related_resources( body, uuid, request ) 
    269282         
    270283        response.content_type = RUBY_MIMETYPE 
  • thingfish/trunk/lib/thingfish/handler/simplemetadata.rb

    r466 r560  
    206206        template = self.get_erb_resource( "metadata/#{template_name}.rhtml" ) 
    207207        return template.result( binding() ) 
    208     end 
    209      
    210      
    211     ### Overridden: check to be sure the metastore used is a  
    212     ### ThingFish::SimpleMetaStore. 
    213     def listener=( listener ) 
    214         raise ThingFish::ConfigError,  
    215             "This handler must be used with a ThingFish::SimpleMetaStore." unless 
    216             listener.metastore.is_a?( ThingFish::SimpleMetaStore ) 
    217         super 
    218208    end 
    219209     
  • thingfish/trunk/lib/thingfish/handler/simplesearch.rb

    r466 r560  
    3131require 'thingfish/constants' 
    3232require 'thingfish/handler' 
     33require 'thingfish/metastore/simple' 
    3334require 'thingfish/mixins' 
    3435 
     
    7475        args = request.query_args.reject { |k,v| v.nil? } 
    7576 
    76         uuids = @metastore.find_by_matching_properties( args ) 
     77        uuids = if args.empty? 
     78            [] 
     79        else 
     80            @metastore.find_by_matching_properties( args ) 
     81        end 
    7782 
    7883        response.status = HTTP::OK 
     
    105110        return content 
    106111    end 
    107      
    108      
    109     ### Overridden: check to be sure the metastore used is a  
    110     ### ThingFish::SimpleMetaStore. 
    111     def listener=( listener ) 
    112         raise ThingFish::ConfigError,  
    113             "This handler must be used with a ThingFish::SimpleMetaStore." unless 
    114             listener.metastore.is_a?( ThingFish::SimpleMetaStore ) 
    115         super 
    116     end 
    117      
    118112end # ThingFish::SearchHandler 
    119113 
  • thingfish/trunk/lib/thingfish/mixins.rb

    r487 r560  
    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/trunk/lib/thingfish/request.rb

    r505 r560  
    8383        @body            = nil 
    8484        @profile         = false 
    85          
    86         @body_key_mapping  = {} 
     85        @authed_user     = nil 
     86         
    8787        @related_resources = Hash.new {|h,k| h[k] = {} } 
    8888        @mongrel_request   = mongrel_request 
     
    119119    # } 
    120120    attr_reader :related_resources 
     121     
     122    # The name of the authenticated user 
     123    attr_accessor :authed_user 
     124    alias_method :authenticated_user, :authed_user 
     125    alias_method :remote_user, :authed_user 
    121126     
    122127     
     
    256261 
    257262 
    258     ### Get the body IO and the merged hash of metadata 
    259     def get_body_and_metadata 
    260         raise ArgumentError, "Can't return a single body for a multipart request" if 
    261             self.has_multipart_body? 
    262          
    263         default_metadata = { 
    264             :useragent     => self.headers[ :user_agent ], 
    265             :uploadaddress => self.remote_addr 
    266         } 
    267  
    268         # Read title out of the content-disposition 
    269         if self.headers[:content_disposition] && 
    270             self.headers[:content_disposition] =~ /filename="(?:.*\\)?(.+?)"/i 
    271             default_metadata[ :title ] = $1 
    272         end 
    273          
    274         extracted_metadata = self.metadata[ @mongrel_request.body ] || {} 
    275  
    276         # Content metadata is determined from http headers 
    277         merged = extracted_metadata.merge({ 
    278             :format => self.content_type, 
    279             :extent => self.headers[ :content_length ], 
    280         }) 
    281         merged.update( default_metadata ) 
    282          
    283         return @mongrel_request.body, merged 
    284     end 
    285      
    286      
    287263    ### Attach additional body and metadata information to the primary 
    288264    ### body, that will be stored with related_to metakeys. 
     
    294270    ### +related_metadata+:: 
    295271    ###    The metadata to attach to the new resource, as a Hash. 
    296     def append_related_resource( body, related_body, related_metadata={} ) 
    297         # Convert the body to the key of the related resources hash 
    298         bodykey = self.make_body_key( body ) 
    299          
    300         unless original_body = @body_key_mapping[ bodykey ] 
    301             errmsg = "Cannot append a resource related to %p: %p isn't one of %p" % [ 
    302                 body, 
    303                 bodykey, 
    304                 @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, 
    305278              ] 
    306279            self.log.error( errmsg ) 
     
    309282 
    310283        related_metadata[:relation] ||= 'appended' 
    311         related_bodykey = self.make_body_key( related_body ) 
    312         self.metadata[ related_body ] = {} 
    313         @body_key_mapping[ related_bodykey ] = related_body 
    314         self.related_resources[ original_body ][ related_body ] = related_metadata 
    315     end 
    316      
    317      
     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 
    318292    ### Append the specified additional +metadata+ for the given +resource+, which should be one 
    319293    ### of the entity bodies yielded by #each_body 
    320294    def append_metadata_for( resource, metadata ) 
    321         # Convert the body to the key of the related resources hash 
    322         bodykey = self.make_body_key( resource ) 
    323          
    324         unless original_body = @body_key_mapping[ bodykey ] 
    325             errmsg = "Cannot append metadata related to %p(%p): %p isn't one of %p" % [ 
    326                 body, 
    327                 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" % [ 
    328298                resource, 
    329                 @body_key_mapping.keys, 
    330299              ] 
    331300            self.log.error( errmsg ) 
     
    333302        end 
    334303 
    335         self.metadata[ original_body ].merge!( metadata ) 
    336     end 
    337      
    338      
    339     ### Generate a key based on the body object that will be the same even after duplication. This 
    340     ### is used to work around our workaround for StringIO's behavior when #dup'ed. 
    341     def make_body_key( body ) 
    342         if body.respond_to?( :string ) 
    343             return Digest::MD5.hexdigest( body.string ) 
    344         else 
    345             return "%s:%d" % [ body.path, body.object_id * 2 ] 
    346         end 
    347     end 
    348      
    349      
     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 
    350336    ### Call the provided block once for each entity body of the request, which may 
    351337    ### be multiple times in the case of a multipart request. If +include_appended_resources+  
     
    376362    ### Check the body IO objects to ensure they're still open. 
    377363    def check_body_ios 
    378         [ self.entity_bodies, self.related_resources ].each do |hash| 
    379             hash.each do |body, _| 
    380                 if body.closed? 
    381                     self.log.warn "Entity body closed: %p" % [ body ] 
    382                     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 ) 
    383381                end 
     382 
     383                self.log.debug "Body %p (%d) replaced with %p (%d)" % [  
     384                    body, body.object_id, clone, clone.object_id 
     385                ] 
     386            else 
    384387                body.rewind 
    385388            end 
     
    467470     
    468471     
     472    ### Return the path portion of the request's URI 
     473    def path 
     474        return self.uri.path 
     475    end 
     476     
     477     
    469478    ### Returns the requester IP address as an IPAddr object. 
    470479    def remote_addr 
     
    490499    ######### 
    491500 
    492     ### Returns the entity bodies of the request along with any related metadata as 
    493     ### a Hash: 
    494     ### { 
    495     ###    <body io> => { <body metadata> }, 
    496     ###    ... 
    497     ### } 
    498     def entity_bodies 
    499         # Parse the request's body parts if they aren't already 
    500         unless @entity_bodies 
    501             if self.has_multipart_body? 
    502                 self.log.debug "Parsing multiple entity bodies." 
    503                 @entity_bodies, @form_metadata = self.parse_multipart_body 
    504             else 
    505                 self.log.debug "Parsing single entity body." 
    506                 body, metad