/lib/thingfish/handler/default.rb

https://bitbucket.org/laika/thingfish · Ruby · 381 lines · 231 code · 76 blank · 74 comment · 15 complexity · 09fb601214d76646932e152afc0bc726 MD5 · raw file

  1. #!/usr/bin/env ruby
  2. require 'pp'
  3. require 'time'
  4. require 'uuidtools'
  5. require 'thingfish'
  6. require 'thingfish/constants'
  7. require 'thingfish/handler'
  8. require 'thingfish/mixins'
  9. # The default top-level handler for the thingfish daemon. This handler provides
  10. # five basic services:
  11. #
  12. # 1. Top-level index <GET />
  13. # 2. Upload a new resource <POST />
  14. # 3. Fetch a resource by UUID <GET /51a01b12-b706-11db-a0e3-cb820d1598b5>
  15. # 4. Overwrite a resource by UUID <PUT /51a01b12-b706-11db-a0e3-cb820d1598b5>
  16. # 5. Delete a resource by UUID <DELETE /51a01b12-b706-11db-a0e3-cb820d1598b5>
  17. #
  18. # This constitutes the core of the data-storage functionality of ThingFish.
  19. #
  20. # == Version
  21. #
  22. # $Id$
  23. #
  24. # == Authors
  25. #
  26. # * Michael Granger <ged@FaerieMUD.org>
  27. # * Mahlon E. Smith <mahlon@martini.nu>
  28. #
  29. # :include: LICENSE
  30. #
  31. #---
  32. #
  33. # Please see the file LICENSE in the top-level directory for licensing details.
  34. #
  35. class ThingFish::DefaultHandler < ThingFish::Handler
  36. include ThingFish::Constants,
  37. ThingFish::Constants::Patterns,
  38. ThingFish::Loggable,
  39. ThingFish::StaticResourcesHandler
  40. # Config defaults
  41. CONFIG_DEFAULTS = {
  42. :html_index => 'index.rhtml',
  43. :cache_expiration => 30.minutes,
  44. :resource_dir => nil, # Use the ThingFish::Handler default
  45. }
  46. #################################################################
  47. ### I N S T A N C E M E T H O D S
  48. #################################################################
  49. ### Set up a new DefaultHandler
  50. def initialize( path, options={} )
  51. super( path, CONFIG_DEFAULTS.merge(options) )
  52. end
  53. ######
  54. public
  55. ######
  56. ### Handler API: Handle a GET request
  57. def handle_get_request( path_info, request, response )
  58. case path_info
  59. # If this is a request to the root, handle it ourselves
  60. when ''
  61. self.log.debug "Handling an index request"
  62. self.handle_index_fetch_request( request, response )
  63. # Likewise for a request to <a uuid>
  64. when UUID_URL
  65. self.log.debug "Handling a UUID request"
  66. uuid = parse_uuid( $1 )
  67. self.handle_resource_fetch_request( request, response, uuid )
  68. # Fall through to the static handler for GET requests
  69. else
  70. self.log.debug "No GET handler for %p; leaving the response alone" %
  71. [ path_info ]
  72. end
  73. end
  74. ### Handler API: Handle a POST request
  75. def handle_post_request( path_info, request, response )
  76. case path_info
  77. when ''
  78. self.handle_create_request( request, response )
  79. when UUID_URL
  80. self.build_method_not_allowed_response( response, :POST,
  81. %w{GET PUT DELETE} )
  82. else
  83. self.log.debug "No POST handler for %p, falling through" % path_info
  84. end
  85. end
  86. ### Handler API: Handle a PUT request
  87. def handle_put_request( path_info, request, response )
  88. case path_info
  89. when ''
  90. self.build_method_not_allowed_response( response, :PUT,
  91. %w{GET POST} )
  92. when UUID_URL
  93. # :TODO: Implement multipart update
  94. raise ThingFish::RequestError, "Multipart update not currently supported" if
  95. request.has_multipart_body?
  96. uuid = parse_uuid( $1 )
  97. self.handle_update_uuid_request( request, response, uuid )
  98. else
  99. self.log.debug "No PUT handler for %p, falling through" % path_info
  100. end
  101. end
  102. ### Handler API: Handle a DELETE request
  103. def handle_delete_request( path_info, request, response )
  104. case path_info
  105. when ''
  106. self.build_method_not_allowed_response( response, :DELETE, %w{GET POST} )
  107. when UUID_URL
  108. uuid = parse_uuid( $1 )
  109. self.handle_delete_uuid_request( request, response, uuid )
  110. else
  111. self.log.debug "No DELETE handler for %p, falling through" % path_info
  112. end
  113. end
  114. ### Make body content for an HTML response (HTML filter API)
  115. def make_html_content( body, request, response )
  116. self.log.debug "Loading index resource %p" % [@options[:html_index]]
  117. content = self.get_erb_resource( @options[:html_index] )
  118. handler_index_sections = self.get_handler_index_sections
  119. return content.result( binding() )
  120. end
  121. #########
  122. protected
  123. #########
  124. ### Make the content for the handler section of the index page.
  125. def make_index_content( uri )
  126. tmpl = self.get_erb_resource( "index_content.rhtml" )
  127. return tmpl.result( binding() )
  128. end
  129. ### Iterate over the loaded handlers and ask each for any content it wants shown
  130. ### on the index HTML page.
  131. def get_handler_index_sections
  132. self.log.debug "Fetching index sections for all registered handlers"
  133. handlers = self.daemon.urimap.map.sort_by {|uri,h| uri }
  134. return handlers.collect do |uri,handlers|
  135. self.log.debug " collecting index content from %p (%p)" %
  136. [handlers.collect {|h| h.class.name}, uri]
  137. handlers.
  138. select {|h| h.is_a?(ThingFish::Handler) }.
  139. collect {|h| h.make_index_content( uri ) }
  140. end.flatten.compact
  141. end
  142. ### Handle a request to fetch the index (GET to /)
  143. def handle_index_fetch_request( request, response )
  144. response.data[:title] = 'Version ' + ThingFish::VERSION
  145. response.data[:tagline] = 'Feed me.'
  146. response.content_type = RUBY_MIMETYPE
  147. response.body = {
  148. 'version' => ThingFish::VERSION,
  149. 'handlers' => self.daemon.handler_info,
  150. 'filters' => self.daemon.filter_info,
  151. }
  152. response.status = HTTP::OK
  153. end
  154. ### Handle fetching a file by UUID (GET to /{uuid})
  155. def handle_resource_fetch_request( request, response, uuid )
  156. if @filestore.has_file?( uuid )
  157. # Try to send a NOT MODIFIED response
  158. if self.can_send_cached_response?( request, uuid )
  159. self.log.info "Client has a cached copy of %s" % [uuid]
  160. response.status = HTTP::NOT_MODIFIED
  161. self.add_cache_headers( response, uuid )
  162. return
  163. else
  164. self.log.info "Sending resource %s" % [uuid]
  165. response.status = HTTP::OK
  166. response.content_type = @metastore[ uuid ].format
  167. self.add_cache_headers( response, uuid )
  168. # Add content disposition headers
  169. self.add_content_disposition( request, response, uuid )
  170. # Send an OK status with the Content-length set to the
  171. # size of the resource
  172. response.headers[ :content_length ] = @filestore.size( uuid )
  173. self.log.info "Setting response body to the resource IO"
  174. response.body = @filestore.fetch_io( uuid )
  175. end
  176. else
  177. response.status = HTTP::NOT_FOUND
  178. response.content_type = 'text/plain'
  179. response.body = "UUID '#{uuid}' not found in the filestore"
  180. end
  181. end
  182. ### Handle a request to create a new resource with the request body as
  183. ### data (POST to /)
  184. def handle_create_request( request, response )
  185. if request.bodies.length > 1
  186. self.log.error "Can't handle multipart request (%p)" % [ request.bodies ]
  187. raise ThingFish::NotImplementedError, "multipart upload not implemented"
  188. end
  189. uuid = nil
  190. # Store the primary resource
  191. body, metadata = request.bodies.to_a.flatten
  192. uuid = self.daemon.store_resource( body, metadata )
  193. # Store any related resources, linked to the primary
  194. self.daemon.store_related_resources( body, uuid, request )
  195. response.status = HTTP::CREATED
  196. response.headers[:location] = '/' + uuid.to_s
  197. response.content_type = RUBY_MIMETYPE
  198. response.body = @metastore.get_properties( uuid )
  199. rescue ThingFish::FileStoreQuotaError => err
  200. self.log.error "Quota error while creating a resource: %s" % [ err.message ]
  201. raise ThingFish::RequestEntityTooLargeError, err.message
  202. end
  203. ### Handle updating a file by UUID
  204. def handle_update_uuid_request( request, response, uuid )
  205. if request.bodies.length > 1
  206. self.log.error "Can't handle multipart request" % [ request.bodies ]
  207. raise ThingFish::NotImplementedError, "multipart upload not implemented"
  208. end
  209. # :TODO: Handle slow/big uploads by returning '202 Accepted' and spawning
  210. # a handler thread?
  211. new_resource = ! @filestore.has_file?( uuid )
  212. body, metadata = request.bodies.to_a.flatten
  213. self.daemon.store_resource( body, metadata, uuid )
  214. # Purge any old related resources, then store any new ones linked to the primary
  215. self.daemon.purge_related_resources( uuid )
  216. self.daemon.store_related_resources( body, uuid, request )
  217. response.content_type = RUBY_MIMETYPE
  218. response.body = @metastore.get_properties( uuid )
  219. if new_resource
  220. response.status = HTTP::CREATED
  221. response.headers[:location] = '/' + uuid.to_s
  222. else
  223. response.status = HTTP::OK
  224. end
  225. rescue ThingFish::FileStoreQuotaError => err
  226. self.log.error "Quota error while updating a resource: %s" % [ err.message ]
  227. raise ThingFish::RequestEntityTooLargeError, err.message
  228. end
  229. ### Handle deleting a file by UUID
  230. def handle_delete_uuid_request( request, response, uuid )
  231. if @filestore.has_file?( uuid )
  232. @filestore.delete( uuid )
  233. @metastore.delete_resource( uuid )
  234. response.status = HTTP::OK
  235. response.content_type = 'text/plain'
  236. response.body = "Resource '#{uuid}' deleted"
  237. else
  238. response.status = HTTP::NOT_FOUND
  239. response.content_type = 'text/plain'
  240. response.body = "Resource '#{uuid}' not found"
  241. end
  242. end
  243. ### Returns true if the given +request+'s headers indicate that the local copy
  244. ### of the data corresponding to the specified +uuid+ are cached remotely, and
  245. ### the client can just use the cached version. This usually means that the
  246. ### handler will send a 304 NOT MODIFIED.
  247. def can_send_cached_response?( request, uuid )
  248. metadata = @metastore[ uuid ]
  249. return request.is_cached_by_client?( metadata.checksum, metadata.modified )
  250. end
  251. ### Add cache control headers to the given +response+ for the specified +uuid+.
  252. def add_cache_headers( response, uuid )
  253. self.log.debug "Adding cache headers to response for %s" % [uuid]
  254. response.headers[ :etag ] = %q{"%s"} % [@metastore[ uuid ].checksum]
  255. response.headers[ :expires ] = 1.year.from_now.httpdate
  256. end
  257. ### Add content disposition handlers to the given +response+ for the
  258. ### specified +uuid+ if the 'attach' query argument exists. As described in
  259. ### RFC 2183, this is an optional, but convenient header when using UUID-keyed
  260. ### resources.
  261. def add_content_disposition( request, response, uuid )
  262. return unless request.query_args.has_key?( 'attach' )
  263. disposition = []
  264. disposition << 'attachment'
  265. if (( filename = request.query_args['attach'] || @metastore[ uuid ].title ))
  266. disposition << %{filename="%s"} % [ filename ]
  267. end
  268. if (( modtime = @metastore[ uuid ].modified ))
  269. modtime = Time.parse( modtime ) unless modtime.is_a?( Time )
  270. disposition << %{modification-date="%s"} % [ modtime.rfc822 ]
  271. end
  272. response.headers[ :content_disposition ] = disposition.join('; ')
  273. end
  274. #######
  275. private
  276. #######
  277. ### A more-efficient version of UUIDTools' UUID parser -- see
  278. ### experiments/bench-uuid-parse.rb in the subversion source.
  279. def parse_uuid( uuid_string )
  280. unless match = UUID_PATTERN.match( uuid_string )
  281. raise ArgumentError, "Invalid UUID %p." % [uuid_string]
  282. end
  283. uuid_components = match.captures
  284. time_low = uuid_components[0].to_i( 16 )
  285. time_mid = uuid_components[1].to_i( 16 )
  286. time_hi_and_version = uuid_components[2].to_i( 16 )
  287. clock_seq_hi_and_reserved = uuid_components[3].to_i( 16 )
  288. clock_seq_low = uuid_components[4].to_i( 16 )
  289. nodes = []
  290. 0.step( 11, 2 ) do |i|
  291. nodes << uuid_components[5][ i, 2 ].to_i( 16 )
  292. end
  293. return UUIDTools::UUID.new( time_low, time_mid, time_hi_and_version,
  294. clock_seq_hi_and_reserved, clock_seq_low, nodes )
  295. end
  296. end # ThingFish::DefaultHandler
  297. # vim: set nosta noet ts=4 sw=4: