Changeset 560
- Timestamp:
- 06/19/08 07:41:58 (2 months ago)
- Files:
-
- thingfish/trunk/Rakefile (modified) (2 diffs)
- thingfish/trunk/docs/manual/layouts/default.page (modified) (1 diff)
- thingfish/trunk/docs/manual/src/Developers_Guide/connecting.page (modified) (3 diffs)
- thingfish/trunk/docs/manual/src/Developers_Guide/cookbook.page (copied) (copied from thingfish/branches/mp3-jukebox/docs/manual/src/Developers_Guide/cookbook.page)
- thingfish/trunk/docs/manual/src/Developers_Guide/index.page (modified) (1 diff)
- thingfish/trunk/docs/manual/src/Developers_Guide/ruby-client.page (modified) (1 diff)
- thingfish/trunk/docs/manual/src/Hackers_Guide/server-architecture.page (modified) (1 diff)
- thingfish/trunk/docs/manual/src/Hackers_Guide/writing-filestores.page (modified) (1 diff)
- thingfish/trunk/docs/manual/src/Hackers_Guide/writing-filters.page (modified) (2 diffs)
- thingfish/trunk/docs/manual/src/Hackers_Guide/writing-handlers.page (modified) (10 diffs)
- thingfish/trunk/docs/manual/src/Hackers_Guide/writing-metastores.page (modified) (1 diff)
- thingfish/trunk/docs/manual/src/getting-started.page (modified) (4 diffs)
- thingfish/trunk/docs/manual/src/index.page (modified) (1 diff)
- thingfish/trunk/lib/thingfish/daemon.rb (modified) (5 diffs)
- thingfish/trunk/lib/thingfish/exceptions.rb (modified) (3 diffs)
- thingfish/trunk/lib/thingfish/handler/default.rb (modified) (2 diffs)
- thingfish/trunk/lib/thingfish/handler/simplemetadata.rb (modified) (1 diff)
- thingfish/trunk/lib/thingfish/handler/simplesearch.rb (modified) (3 diffs)
- thingfish/trunk/lib/thingfish/mixins.rb (modified) (1 diff)
- thingfish/trunk/lib/thingfish/request.rb (modified) (11 diffs)
- thingfish/trunk/plugins/.skel/lib/thingfish/handler/TEMPLATE.rb.erb (modified) (1 diff)
- thingfish/trunk/plugins/.skel/spec/thingfish/handler/TEMPLATE_spec.rb.erb (modified) (1 diff)
- thingfish/trunk/plugins/thingfish-filter-basicauth (copied) (copied from thingfish/branches/mp3-jukebox/plugins/thingfish-filter-basicauth)
- thingfish/trunk/plugins/thingfish-metastore-rdf/README (modified) (1 diff)
- thingfish/trunk/plugins/thingfish-metastore-rdf/lib/thingfish/metastore/rdf.rb (modified) (12 diffs)
- thingfish/trunk/plugins/thingfish-metastore-rdf/spec/thingfish/metastore/rdf_spec.rb (modified) (3 diffs)
- thingfish/trunk/spec/thingfish/daemon_spec.rb (modified) (1 diff)
- thingfish/trunk/spec/thingfish/handler/default_spec.rb (modified) (10 diffs)
- thingfish/trunk/spec/thingfish/handler/simplemetadata_spec.rb (modified) (1 diff)
- thingfish/trunk/spec/thingfish/handler/simplesearch_spec.rb (modified) (1 diff)
- thingfish/trunk/spec/thingfish/request_spec.rb (modified) (12 diffs)
- thingfish/trunk/utils.rb (modified) (1 diff)
- thingfish/trunk/var/www/static/css/default.css (modified) (2 diffs)
- thingfish/trunk/var/www/static/images/delete.png (modified) (previous)
- thingfish/trunk/var/www/static/js/jquery.js (modified) (2 diffs)
- thingfish/trunk/var/www/static/js/manual.js (copied) (copied from thingfish/branches/mp3-jukebox/var/www/static/js/manual.js)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
thingfish/trunk/Rakefile
r556 r560 94 94 Pathname.glob( RAKE_TASKDIR + '*.rb' ).each do |tasklib| 95 95 trace "Loading task lib #{tasklib}" 96 begin 97 require tasklib 98 rescue => err 99 fail "Tasklib #{tasklib}: #{err.message}" 100 end 96 require tasklib 101 97 end 102 98 … … 136 132 end 137 133 134 task :clobber_manual do 135 rmtree( targetdir, :verbose => true ) 136 end 137 138 138 139 139 ### Cruisecontrol task thingfish/trunk/docs/manual/layouts/default.page
r540 r560 7 7 8 8 <script type="text/javascript" src="/js/jquery.js"></script> 9 <script type="text/javascript" src="/js/manual.js"></script> 9 10 <script type="text/javascript" src="/js/jquery.ThickBox.js"></script> 10 11 thingfish/trunk/docs/manual/src/Developers_Guide/connecting.page
r549 r560 12 12 h2. <%= page.config['title'] %> 13 13 14 <div id="auto-toc" /> 15 14 16 There are several options for connecting: you can use a client library if one has 15 17 been written for your language, or you can use HTTP to talk to the server directly. … … 17 19 h3. The Basics 18 20 19 To test out the particulars of your connection, you can connect to the daemon with a20 web browser by pointing it at the URL:21 To test out the particulars of your connection, if you have the @html@ filter loaded, you can 22 connect to the daemon with a web browser by pointing it at the URL: 21 23 22 24 <pre> … … 42 44 <dd> 43 45 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. 48 51 49 52 <?example { language: ruby, caption: "Server introspection hash" } ?> thingfish/trunk/docs/manual/src/Developers_Guide/index.page
r546 r560 15 15 * <?link The Javascript Client Library ?> 16 16 * <?link Writing Your Own Client Library ?> 17 * <?link ThingFish Usage Cookbook ?> thingfish/trunk/docs/manual/src/Developers_Guide/ruby-client.page
r547 r560 10 10 11 11 h2. <%= page.config['title'] %> 12 13 <div id="auto-toc" /> 12 14 13 15 The @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 11 11 12 12 h2. <%= page.config['title'] %> 13 14 <div id="auto-toc" /> 13 15 14 16 The server has five major components: thingfish/trunk/docs/manual/src/Hackers_Guide/writing-filestores.page
r543 r560 11 11 h2. <%= page.config['title'] %> 12 12 13 « Coming Soon! » 13 <div id="auto-toc" /> 14 15 The back end of a ThingFish instance... 16 17 h3. What's a FileStore do? 18 19 h3. The mandatory interface 20 21 h3. Optional interface 14 22 15 23 24 25 thingfish/trunk/docs/manual/src/Hackers_Guide/writing-filters.page
r550 r560 11 11 12 12 h2. <%= page.config['title'] %> 13 14 <div id="auto-toc" /> 13 15 14 16 h3. How's a filter different than a handler? … … 28 30 a JPEG via the client's @Accept@ header. 29 31 30 h3. Rest of this coming soon... 32 33 h3. What does a filter look like? 34 35 Great question! Here's one that we'll use to point out some filtery concepts. 36 37 <?example { language: ruby, caption: A minimal filter. } ?> 38 require 'thingfish/constants' 39 require 'thingfish/filter' 40 41 class 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 53 end 54 <?end example?> 55 56 |_. line number |_. what's it doing? | 57 | 4 | All handlers inherit from the @ThingFish::Handler@ parent class. 58 The handler name needs to match the filename it is stored in - 59 in 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. 63 These special methods receive a @ThingFish::Request@@ and a @ThingFish::Response@ object 64 to 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 66 easily be an erb template, or even an @IO@ object. @IO@ objects will automatically buffer out 67 to 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 70 the content we want, setting this to @OK@ stops further processing, and lets the response 71 through successfully. | 72 73 h3. Installing the filter 74 75 Alright, now that you've got this wicked cool Time handler, how do we tell ThingFish 76 to actually use it? So far, all we really need to do is to load it when a particular 77 URI is requested. This is done in the @thingfish.conf@ file, under the @handlers@ 78 section. If you wanted to install it under a single URI called @/what-time-is-it@, 79 here's how you'd do it: 80 81 <?example { language: yaml, caption: "Our handler's config section" } ?> 82 plugins: 83 handlers: 84 - time: /what-time-is-it 85 <?end example?> 86 87 88 h3. 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 5 5 filters: 6 6 - erb 7 - api 8 - links 7 9 - examples 8 10 - textile … … 10 12 11 13 h2. <%= page.config['title'] %> 14 15 <div id="auto-toc" /> 12 16 13 17 h3. What does a handler look like? … … 163 167 ThingFish::ResourceLoader 164 168 165 ### Send the local time to the requester in plain text.169 ### Send the local time to the requester. 166 170 def handle_get_request( request, response ) 167 171 … … 195 199 <?end example?> 196 200 201 |_. line number |_. what's it doing? | 202 | 8 | Including the @ThingFish::ResourceLoader@ mixin sets up the resource directory for 203 this 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 205 each plugin is in a @thingfish@ directory under Ruby's @datadir@ by default. Each plugin 206 gets its own directory based on its name, so our TimeHandler plugin's resource directory 207 is @#{datadir}/thingfish/timehandler/@. This can be changed by setting the @resource_dir@ 208 key in the handler's config. | 209 | 33 | Interpolate the calculated @timestring@ into the template where the first @%s@ is. | 210 197 211 And the template we'll use looks like: 198 212 … … 223 237 <?end example?> 224 238 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? | 235 240 | 13 | Really Big Fonts -- +10 pts on the Web 2.0 scorecard! | 236 241 | 20 | This is where @timestring@ will be inserted. | … … 265 270 ThingFish::ResourceLoader 266 271 267 ### Send the local time to the requester in plain text.272 ### Send the local time to the requester. 268 273 def handle_get_request( request, response ) 269 274 … … 297 302 <?end example?> 298 303 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 309 body to the rendered output. | 310 299 311 And then we'll change the @%s@ placeholders into ERb directives in the template: 300 312 … … 325 337 <?end example?> 326 338 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? | 334 340 | 7 | We'll set the @title@ dynamically too now. | 335 341 | 20 | This will get the @timestring@ just like before. | 336 342 337 343 338 h3. Linking to Other Handlers 344 h3. Serving Static Content 345 346 If your handler has images, javascript, styles, or other static resources you'd like to link from 347 the HTML you generate, you can do so easily with the <?api ThingFish::StaticResourcesHandler ?> 348 mixin. This mixin installs a fallthrough handler that will serve files from a subdirectory inside of 349 the handler's @resources@ directory. 350 351 <?example { language: ruby, caption: A handler that also serves static content. } ?> 352 require 'thingfish/constants' 353 require 'thingfish/handler' 354 require 'thingfish/mixins' 355 require 'time' 356 357 class 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 390 end 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. 396 The 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 399 ThingFish::StaticResourcesHandler mixin, if the request matches a file in the static resources 400 directory, it will be served. | 401 402 In 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 404 would determine the correct mimetype and serve it. 405 406 407 h3. Finding Other Handler URIs 339 408 340 409 Sometimes you'll want one handler to provide a link to another handler, but since the URI … … 379 448 ThingFish::ResourceLoader 380 449 381 ### Send the local time to the requester in plain text.450 ### Send the local time to the requester. 382 451 def handle_get_request( request, response ) 383 452 … … 424 493 h3. Index page content 425 494 495 When ThingFish is set up to provide an HTML interface (by enabling the HTML filter), requests to 496 the @/@ URI will return an HTML page that serves as an index of all the functionality offered by 497 the 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 499 URI: 500 426 501 <?example { language: ruby, caption: A simple index page content provider callback } ?> 427 502 def 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>" 504 end 505 <?end example?> 506 431 507 432 508 h3. Content Negotiation thingfish/trunk/docs/manual/src/Hackers_Guide/writing-metastores.page
r543 r560 11 11 h2. <%= page.config['title'] %> 12 12 13 14 Differences between simple metastores and advanced 15 16 strong data typing (date range searches) 17 multiple values for a metakey (tags) 18 native syntax support for POSTed search (SparQL, SQL, etc.) 19 13 20 « Coming Soon! » 14 21 thingfish/trunk/docs/manual/src/getting-started.page
r547 r560 11 11 h1. <%= page.config['title'] %> 12 12 13 <div id="auto-toc" /> 14 13 15 h2. Installation 14 16 … … 112 114 test. You'll probably want to get more elaborate with your configuration fairly 113 115 quickly, however. 116 117 Here's an example of a fairly full-featured config: 114 118 115 119 <?example { language: yaml, caption: Example comprehensive config file } ?> … … 137 141 root: /home/thingfish/var 138 142 metastore: 139 name: marshalled140 root: /home/thingfish/var143 name: sequel 144 sequel_connect: postgres://thingfish@localhost/tf 141 145 filters: 142 146 - html … … 153 157 uris: /upload 154 158 resource_dir: /home/thingfish/plugins/thingfish-formuploadhandler/resources 155 - metadata:159 - simplemetadata: 156 160 uris: /metadata 157 resource_dir: /home/thingfish/var/www 161 resource_dir: var/www 162 - simplesearch: 163 uris: /search 164 resource_dir: var/www 158 165 - status: 159 166 uris: /status thingfish/trunk/docs/manual/src/index.page
r546 r560 12 12 13 13 h1. <%= page.config['title'] %> 14 15 <div id="auto-toc" /> 14 16 15 17 ThingFish is a network-accessable, searchable, extensible datastore. It can be used to store chunks thingfish/trunk/lib/thingfish/daemon.rb
r548 r560 235 235 end 236 236 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 237 266 238 267 ######### … … 350 379 self.send_error_response( response, request, err.status, client, err ) 351 380 352 rescue => err381 rescue Exception => err 353 382 self.send_error_response( response, request, HTTP::SERVER_ERROR, client, err ) 354 355 383 end 356 384 … … 362 390 begin 363 391 filter.handle_request( request, response ) 364 rescue => err392 rescue Exception => err 365 393 self.log.error "Request filter raised a %s: %s" % 366 394 [ err.class.name, err.message ] … … 384 412 begin 385 413 filter.handle_response( response, request ) 386 rescue => err414 rescue Exception => err 387 415 self.log.error "Response filter raised a %s: %s" % 388 416 [ err.class.name, err.message ] … … 480 508 handlers.each do |handler| 481 509 response.handlers << handler 510 request.check_body_ios 482 511 handler.process( request, response ) 483 request.check_body_ios484 512 break if response.is_handled? || client.closed? 485 513 end thingfish/trunk/lib/thingfish/exceptions.rb
r466 r560 56 56 class ClientError < ThingFish::Error; end 57 57 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 59 83 class RequestError < ThingFish::Error 60 84 include ThingFish::Constants … … 68 92 end 69 93 70 # Upload exceeded quota94 # 413: Upload exceeded quota 71 95 class RequestEntityTooLargeError < ThingFish::RequestError 72 96 include ThingFish::Constants … … 78 102 end 79 103 80 # Client requested a mimetype we don't know how to convert to104 # 406: Client requested a mimetype we don't know how to convert to 81 105 class RequestNotAcceptableError < ThingFish::RequestError 82 106 include ThingFish::Constants thingfish/trunk/lib/thingfish/handler/default.rb
r464 r560 242 242 ### data (POST to /) 243 243 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 244 249 245 250 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 248 254 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 ) 249 258 250 259 response.status = HTTP::CREATED … … 267 276 body, metadata = request.get_body_and_metadata 268 277 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 ) 269 282 270 283 response.content_type = RUBY_MIMETYPE thingfish/trunk/lib/thingfish/handler/simplemetadata.rb
r466 r560 206 206 template = self.get_erb_resource( "metadata/#{template_name}.rhtml" ) 207 207 return template.result( binding() ) 208 end209 210 211 ### Overridden: check to be sure the metastore used is a212 ### ThingFish::SimpleMetaStore.213 def listener=( listener )214 raise ThingFish::ConfigError,215 "This handler must be used with a ThingFish::SimpleMetaStore." unless216 listener.metastore.is_a?( ThingFish::SimpleMetaStore )217 super218 208 end 219 209 thingfish/trunk/lib/thingfish/handler/simplesearch.rb
r466 r560 31 31 require 'thingfish/constants' 32 32 require 'thingfish/handler' 33 require 'thingfish/metastore/simple' 33 34 require 'thingfish/mixins' 34 35 … … 74 75 args = request.query_args.reject { |k,v| v.nil? } 75 76 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 77 82 78 83 response.status = HTTP::OK … … 105 110 return content 106 111 end 107 108 109 ### Overridden: check to be sure the metastore used is a110 ### ThingFish::SimpleMetaStore.111 def listener=( listener )112 raise ThingFish::ConfigError,113 "This handler must be used with a ThingFish::SimpleMetaStore." unless114 listener.metastore.is_a?( ThingFish::SimpleMetaStore )115 super116 end117 118 112 end # ThingFish::SearchHandler 119 113 thingfish/trunk/lib/thingfish/mixins.rb
r487 r560 348 348 syms.each do |sym| 349 349 define_method( sym ) { 350 raise NotImplementedError,350 raise ::NotImplementedError, 351 351 "%p does not provide an implementation of #%s" % [ self.class, sym ], 352 352 caller(1) thingfish/trunk/lib/thingfish/request.rb
r505 r560 83 83 @body = nil 84 84 @profile = false 85 86 @body_key_mapping = {}85 @authed_user = nil 86 87 87 @related_resources = Hash.new {|h,k| h[k] = {} } 88 88 @mongrel_request = mongrel_request … … 119 119 # } 120 120 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 121 126 122 127 … … 256 261 257 262 258 ### Get the body IO and the merged hash of metadata259 def get_body_and_metadata260 raise ArgumentError, "Can't return a single body for a multipart request" if261 self.has_multipart_body?262 263 default_metadata = {264 :useragent => self.headers[ :user_agent ],265 :uploadaddress => self.remote_addr266 }267 268 # Read title out of the content-disposition269 if self.headers[:content_disposition] &&270 self.headers[:content_disposition] =~ /filename="(?:.*\\)?(.+?)"/i271 default_metadata[ :title ] = $1272 end273 274 extracted_metadata = self.metadata[ @mongrel_request.body ] || {}275 276 # Content metadata is determined from http headers277 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, merged284 end285 286 287 263 ### Attach additional body and metadata information to the primary 288 264 ### body, that will be stored with related_to metakeys. … … 294 270 ### +related_metadata+:: 295 271 ### 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, 305 278 ] 306 279 self.log.error( errmsg ) … … 309 282 310 283 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 318 292 ### Append the specified additional +metadata+ for the given +resource+, which should be one 319 293 ### of the entity bodies yielded by #each_body 320 294 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" % [ 328 298 resource, 329 @body_key_mapping.keys,330 299 ] 331 300 self.log.error( errmsg ) … … 333 302 end 334 303 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 350 336 ### Call the provided block once for each entity body of the request, which may 351 337 ### be multiple times in the case of a multipart request. If +include_appended_resources+ … … 376 362 ### Check the body IO objects to ensure they're still open. 377 363 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 ) 383 381 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 384 387 body.rewind 385 388 end … … 467 470 468 471 472 ### Return the path portion of the request's URI 473 def path 474 return self.uri.path 475 end 476 477 469 478 ### Returns the requester IP address as an IPAddr object. 470 479 def remote_addr … … 490 499 ######### 491 500 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
