PageRenderTime 52ms CodeModel.GetById 22ms app.highlight 26ms RepoModel.GetById 0ms app.codeStats 0ms

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