thingfish 0.5.0.pre20160707192835

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.simplecov +7 -0
  3. data/History.rdoc +5 -0
  4. data/LICENSE +29 -0
  5. data/Manifest.txt +39 -0
  6. data/Procfile +4 -0
  7. data/README.rdoc +92 -0
  8. data/Rakefile +92 -0
  9. data/bin/tfprocessord +6 -0
  10. data/bin/thingfish +10 -0
  11. data/etc/thingfish.conf.example +26 -0
  12. data/lib/strelka/app/metadata.rb +38 -0
  13. data/lib/strelka/httprequest/metadata.rb +70 -0
  14. data/lib/thingfish.rb +43 -0
  15. data/lib/thingfish/behaviors.rb +263 -0
  16. data/lib/thingfish/datastore.rb +55 -0
  17. data/lib/thingfish/datastore/memory.rb +93 -0
  18. data/lib/thingfish/handler.rb +728 -0
  19. data/lib/thingfish/metastore.rb +55 -0
  20. data/lib/thingfish/metastore/memory.rb +201 -0
  21. data/lib/thingfish/mixins.rb +57 -0
  22. data/lib/thingfish/processor.rb +79 -0
  23. data/lib/thingfish/processor/mp3.rb +167 -0
  24. data/lib/thingfish/processordaemon.rb +16 -0
  25. data/lib/thingfish/spechelpers.rb +165 -0
  26. data/spec/data/APIC-1-image.mp3 +0 -0
  27. data/spec/data/APIC-2-images.mp3 +0 -0
  28. data/spec/data/PIC-1-image.mp3 +0 -0
  29. data/spec/data/PIC-2-images.mp3 +0 -0
  30. data/spec/helpers.rb +67 -0
  31. data/spec/spec.opts +4 -0
  32. data/spec/thingfish/datastore/memory_spec.rb +19 -0
  33. data/spec/thingfish/datastore_spec.rb +64 -0
  34. data/spec/thingfish/handler_spec.rb +838 -0
  35. data/spec/thingfish/metastore/memory_spec.rb +17 -0
  36. data/spec/thingfish/metastore_spec.rb +96 -0
  37. data/spec/thingfish/mixins_spec.rb +63 -0
  38. data/spec/thingfish/processor/mp3_spec.rb +50 -0
  39. data/spec/thingfish/processor_spec.rb +65 -0
  40. data/spec/thingfish_spec.rb +23 -0
  41. metadata +244 -0
@@ -0,0 +1,728 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'strelka'
5
+ require 'strelka/app'
6
+
7
+ require 'configurability'
8
+ require 'loggability'
9
+
10
+ require 'thingfish' unless defined?( Thingfish )
11
+ require 'thingfish/processor'
12
+
13
+ #
14
+ # Network-accessable datastore service
15
+ #
16
+ class Thingfish::Handler < Strelka::App
17
+ extend Loggability,
18
+ Configurability
19
+
20
+
21
+ # Strelka App ID
22
+ ID = 'thingfish'
23
+
24
+
25
+ # Loggability API -- log to the :thingfish logger
26
+ log_to :thingfish
27
+
28
+
29
+ # Configurability API -- set config defaults
30
+ CONFIG_DEFAULTS = {
31
+ datastore: 'memory',
32
+ metastore: 'memory',
33
+ processors: [],
34
+ event_socket_uri: 'tcp://127.0.0.1:3475',
35
+ }
36
+
37
+ # Metadata keys which aren't directly modifiable via the REST API
38
+ # :TODO: Consider making either all of these or a subset of them
39
+ # be immutable.
40
+ OPERATIONAL_METADATA_KEYS = %w[
41
+ format
42
+ extent
43
+ created
44
+ uploadaddress
45
+ ]
46
+
47
+ # Metadata keys that must be provided by plugins for related resources
48
+ REQUIRED_RELATED_METADATA_KEYS = %w[
49
+ relationship
50
+ format
51
+ ]
52
+
53
+
54
+ require 'thingfish/mixins'
55
+ require 'thingfish/datastore'
56
+ require 'thingfish/metastore'
57
+ extend Strelka::MethodUtilities
58
+
59
+
60
+ ##
61
+ # Configurability API
62
+ config_key :thingfish
63
+
64
+ # The configured datastore type
65
+ singleton_attr_accessor :datastore
66
+
67
+ # The configured metastore type
68
+ singleton_attr_accessor :metastore
69
+
70
+ # The ZMQ socket for publishing various resource events.
71
+ singleton_attr_accessor :event_socket_uri
72
+
73
+ # The list of configured processors.
74
+ singleton_attr_accessor :processors
75
+
76
+
77
+ ### Configurability API -- install the configuration
78
+ def self::configure( config=nil )
79
+ config = self.defaults.merge( config || {} )
80
+
81
+ self.datastore = config[:datastore]
82
+ self.metastore = config[:metastore]
83
+ self.event_socket_uri = config[:event_socket_uri]
84
+
85
+ self.processors = self.load_processors( config[:processors] )
86
+ self.processors.each do |processor|
87
+ self.filter( :request, &processor.method(:process_request) )
88
+ self.filter( :response, &processor.method(:process_response) )
89
+ end
90
+ end
91
+
92
+
93
+ ### Load the Thingfish::Processors in the given +processor_list+ and return an instance
94
+ ### of each one.
95
+ def self::load_processors( processor_list )
96
+ self.log.info "Loading processors"
97
+ processors = []
98
+
99
+ processor_list.each do |processor_type|
100
+ begin
101
+ processors << Thingfish::Processor.create( processor_type )
102
+ self.log.debug " loaded %s: %p" % [ processor_type, processors.last ]
103
+ rescue LoadError => err
104
+ self.log.error "%p: %s while loading the %s processor" %
105
+ [ err.class, err.message, processor_type ]
106
+ end
107
+ end
108
+
109
+ return processors
110
+ end
111
+
112
+
113
+ ### Set up the metastore, datastore, and event socket when the handler is
114
+ ### created.
115
+ def initialize( * ) # :notnew:
116
+ super
117
+
118
+ @datastore = Thingfish::Datastore.create( self.class.datastore )
119
+ @metastore = Thingfish::Metastore.create( self.class.metastore )
120
+ @event_socket = nil
121
+ end
122
+
123
+
124
+ ######
125
+ public
126
+ ######
127
+
128
+ # The datastore
129
+ attr_reader :datastore
130
+
131
+ # The metastore
132
+ attr_reader :metastore
133
+
134
+ # The PUB socket on which resource events are published
135
+ attr_reader :event_socket
136
+
137
+
138
+ ### Run the handler -- overridden to set up the event socket on startup.
139
+ def run
140
+ self.setup_event_socket
141
+ super
142
+ end
143
+
144
+
145
+ ### Set up the event socket.
146
+ def setup_event_socket
147
+ if self.class.event_socket_uri && ! @event_socket
148
+ @event_socket = Mongrel2.zmq_context.socket( :PUB )
149
+ @event_socket.linger = 0
150
+ @event_socket.bind( self.class.event_socket_uri )
151
+ end
152
+ end
153
+
154
+
155
+ ### Shutdown handler hook.
156
+ def shutdown
157
+ self.event_socket.close if self.event_socket
158
+ super
159
+ end
160
+
161
+
162
+ ### Restart handler hook.
163
+ def restart
164
+ if self.event_socket
165
+ oldsock = @event_socket
166
+ @event_socket = @event_socket.dup
167
+ oldsock.close
168
+ end
169
+
170
+ super
171
+ end
172
+
173
+
174
+ ########################################################################
175
+ ### P L U G I N S
176
+ ########################################################################
177
+
178
+ #
179
+ # Strelka plugin for Thingfish metadata
180
+ #
181
+ plugin :metadata
182
+
183
+
184
+ #
185
+ # Global params
186
+ #
187
+ plugin :parameters
188
+ param :uuid
189
+ param :key, :word
190
+ param :limit, :integer, "The maximum number of records to return."
191
+ param :offset, :integer, "The offset into the result set to use as the first result."
192
+ param :order, /^(?<fields>[[:word:]]+(?:,[[:word:]]+)*)/,
193
+ "The name(s) of the fields to order results by."
194
+ param :direction, /^(asc|desc)$/i, "The order direction (ascending or descending)"
195
+ param :casefold, :boolean, "Whether or not to convert to lowercase before matching"
196
+ param :relationship, :word, "The name of the relationship between two resources"
197
+
198
+
199
+ #
200
+ # Content negotiation
201
+ #
202
+ plugin :negotiation
203
+
204
+
205
+ #
206
+ # Filters
207
+ #
208
+ plugin :filters
209
+
210
+ ### Modify outgoing headers on all responses to include version info.
211
+ ###
212
+ filter :response do |res|
213
+ res.headers.x_thingfish = Thingfish.version_string( true )
214
+ end
215
+
216
+
217
+ #
218
+ # Routing
219
+ #
220
+ plugin :routing
221
+ router :exclusive
222
+
223
+ # GET /serverinfo
224
+ # Return various information about the handler configuration.
225
+ get '/serverinfo' do |req|
226
+ res = req.response
227
+ info = {
228
+ :version => Thingfish.version_string( true ),
229
+ :metastore => self.metastore.class.name,
230
+ :datastore => self.datastore.class.name
231
+ }
232
+
233
+ res.for( :text, :json, :yaml ) { info }
234
+ return res
235
+ end
236
+
237
+
238
+ #
239
+ # Datastore routes
240
+ #
241
+
242
+ # GET /
243
+ # Fetch a list of all objects
244
+ get do |req|
245
+ finish_with HTTP::BAD_REQUEST, req.params.error_messages.join(', ') unless req.params.okay?
246
+
247
+ uuids = self.metastore.search( req.params.valid )
248
+ self.log.debug "UUIDs are: %p" % [ uuids ]
249
+
250
+ base_uri = req.base_uri
251
+ list = uuids.collect do |uuid|
252
+ uri = base_uri.dup
253
+ uri.path += '/' unless uri.path[-1] == '/'
254
+ uri.path += uuid
255
+
256
+ metadata = self.metastore.fetch( uuid )
257
+ metadata['uri'] = uri.to_s
258
+ metadata['uuid'] = uuid
259
+
260
+ metadata
261
+ end
262
+
263
+ res = req.response
264
+ res.for( :json, :yaml ) { list }
265
+ res.for( :text ) do
266
+ list.collect {|entry| "%s [%s, %0.2fB]" % entry.values_at(:url, :format, :extent) }
267
+ end
268
+
269
+ return res
270
+ end
271
+
272
+
273
+ # GET /«uuid»/related
274
+ # Fetch a list of all objects related to «uuid»
275
+ get ':uuid/related' do |req|
276
+ finish_with HTTP::BAD_REQUEST, req.params.error_messages.join(', ') unless req.params.okay?
277
+
278
+ uuid = req.params[ :uuid ]
279
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
280
+
281
+ uuids = self.metastore.fetch_related_oids( uuid )
282
+ self.log.debug "Related UUIDs are: %p" % [ uuids ]
283
+
284
+ base_uri = req.base_uri
285
+ list = uuids.collect do |uuid|
286
+ uri = base_uri.dup
287
+ uri.path += '/' unless uri.path[-1] == '/'
288
+ uri.path += uuid
289
+
290
+ metadata = self.metastore.fetch( uuid )
291
+ metadata['uri'] = uri.to_s
292
+ metadata['uuid'] = uuid
293
+
294
+ metadata
295
+ end
296
+
297
+ res = req.response
298
+ res.for( :json, :yaml ) { list }
299
+ res.for( :text ) do
300
+ list.collect {|entry| "%s [%s, %0.2fB]" % entry.values_at(:url, :format, :extent) }
301
+ end
302
+
303
+ return res
304
+ end
305
+
306
+
307
+ # GET /«uuid»/related/«relationship»
308
+ # Get the data for the resource related to the one to the given +uuid+ via the
309
+ # specified +relationship+.
310
+ get ':uuid/related/:relationship' do |req|
311
+ uuid = req.params[:uuid]
312
+ rel = req.params[:relationship]
313
+
314
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
315
+
316
+ criteria = {
317
+ 'relation' => uuid,
318
+ 'relationship' => rel,
319
+ }
320
+ uuid = self.metastore.search( criteria: criteria, include_related: true ).first or
321
+ finish_with( HTTP::NOT_FOUND, "No such related resource." )
322
+
323
+ object = self.datastore.fetch( uuid ) or
324
+ raise "Metadata for non-existant resource %p" % [ uuid ]
325
+ metadata = self.metastore.fetch( uuid )
326
+
327
+ res = req.response
328
+ res.body = object
329
+ res.content_type = metadata['format']
330
+
331
+ return res
332
+ end
333
+
334
+
335
+ # GET /«uuid»
336
+ # Fetch an object by ID
337
+ get ':uuid' do |req|
338
+ uuid = req.params[:uuid]
339
+ object = self.datastore.fetch( uuid )
340
+ metadata = self.metastore.fetch( uuid )
341
+
342
+ finish_with HTTP::NOT_FOUND, "No such object." unless object && metadata
343
+
344
+ res = req.response
345
+ res.content_type = metadata['format']
346
+
347
+ if object.respond_to?( :path )
348
+ path = Pathname( object.path )
349
+ self.log.debug "Server is chrooted to: %p" % [ req.server_chroot ]
350
+ chroot_path = path.relative_path_from( req.server_chroot )
351
+ res.extend_reply_with( :sendfile )
352
+ res.headers.content_length = object.size
353
+ res.extended_reply_data << File::SEPARATOR + chroot_path.to_s
354
+ else
355
+ res.body = object
356
+ end
357
+
358
+ return res
359
+ end
360
+
361
+
362
+ # POST /
363
+ # Upload a new object.
364
+ post do |req|
365
+ uuid, metadata = self.save_resource( req )
366
+ self.send_event( :created, :uuid => uuid )
367
+
368
+ uri = req.base_uri.dup
369
+ uri.path += '/' unless uri.path[-1] == '/'
370
+ uri.path += uuid
371
+
372
+ res = req.response
373
+ res.headers.location = uri
374
+ res.headers.x_thingfish_uuid = uuid
375
+ res.status = HTTP::CREATED
376
+
377
+ res.for( :text, :json, :yaml ) { metadata }
378
+
379
+ return res
380
+ end
381
+
382
+
383
+ # PUT /«uuid»
384
+ # Replace the data associated with +uuid+.
385
+ put ':uuid' do |req|
386
+ uuid = req.params[:uuid]
387
+ self.datastore.include?( uuid ) or
388
+ finish_with HTTP::NOT_FOUND, "No such object."
389
+
390
+ self.remove_related_resources( uuid )
391
+ self.save_resource( req, uuid )
392
+ self.send_event( :replaced, :uuid => uuid )
393
+
394
+ res = req.response
395
+ res.status = HTTP::NO_CONTENT
396
+
397
+ return res
398
+ end
399
+
400
+
401
+ # DELETE /«uuid»
402
+ # Remove the object associated with +uuid+.
403
+ delete ':uuid' do |req|
404
+ uuid = req.params[:uuid]
405
+
406
+ self.datastore.remove( uuid ) or finish_with( HTTP::NOT_FOUND, "No such object." )
407
+ metadata = self.metastore.remove( uuid )
408
+ self.remove_related_resources( uuid )
409
+ self.send_event( :deleted, :uuid => uuid )
410
+
411
+ res = req.response
412
+ res.status = HTTP::OK
413
+
414
+ # TODO: Remove in favor of default metadata when the metastore
415
+ # knows what that is.
416
+ res.for( :text ) do
417
+ "%d bytes for %s deleted." % [ metadata['extent'], uuid ]
418
+ end
419
+ res.for( :json, :yaml ) {{ uuid: uuid, extent: metadata['extent'] }}
420
+
421
+ return res
422
+ end
423
+
424
+
425
+ #
426
+ # Metastore routes
427
+ #
428
+
429
+ # GET /«uuid»/metadata
430
+ # Fetch all metadata for «uuid».
431
+ get ':uuid/metadata' do |req|
432
+ uuid = req.params[:uuid]
433
+
434
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
435
+
436
+ res = req.response
437
+ res.status = HTTP::OK
438
+ res.for( :json, :yaml ) { self.metastore.fetch( uuid ) }
439
+
440
+ return res
441
+ end
442
+
443
+
444
+ # GET /«uuid»/metadata/«key»
445
+ # Fetch metadata value associated with «key» for «uuid».
446
+ get ':uuid/metadata/:key' do |req|
447
+ uuid = req.params[:uuid]
448
+ key = req.params[:key]
449
+
450
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
451
+
452
+ res = req.response
453
+ res.status = HTTP::OK
454
+ res.for( :json, :yaml ) { self.metastore.fetch_value( uuid, key ) }
455
+
456
+ return res
457
+ end
458
+
459
+
460
+ # POST /«uuid»/metadata/«key»
461
+ # Create a metadata value associated with «key» for «uuid».
462
+ post ':uuid/metadata/:key' do |req|
463
+ uuid = req.params[:uuid]
464
+ key = req.params[:key]
465
+
466
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
467
+ finish_with( HTTP::CONFLICT, "Key already exists." ) unless
468
+ self.metastore.fetch_value( uuid, key ).nil?
469
+
470
+ self.metastore.merge( uuid, key => req.body.read )
471
+ self.send_event( :metadata_updated, :uuid => uuid, :key => key )
472
+
473
+ res = req.response
474
+ res.headers.location = req.uri.to_s
475
+ res.body = nil
476
+ res.status = HTTP::CREATED
477
+
478
+ return res
479
+ end
480
+
481
+
482
+ # PUT /«uuid»/metadata/«key»
483
+ # Replace or create a metadata value associated with «key» for «uuid».
484
+ put ':uuid/metadata/:key' do |req|
485
+ uuid = req.params[:uuid]
486
+ key = req.params[:key]
487
+
488
+ finish_with( HTTP::FORBIDDEN, "Protected metadata." ) if
489
+ OPERATIONAL_METADATA_KEYS.include?( key )
490
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
491
+ previous_value = self.metastore.fetch( uuid, key )
492
+
493
+ self.metastore.merge( uuid, key => req.body.read )
494
+ self.send_event( :metadata_replaced, :uuid => uuid, :key => key )
495
+
496
+ res = req.response
497
+ res.body = nil
498
+
499
+ if previous_value
500
+ res.status = HTTP::NO_CONTENT
501
+ else
502
+ res.headers.location = req.uri.to_s
503
+ res.status = HTTP::CREATED
504
+ end
505
+
506
+ return res
507
+ end
508
+
509
+
510
+ # PUT /«uuid»/metadata
511
+ # Replace user metadata for «uuid».
512
+ put ':uuid/metadata' do |req|
513
+ uuid = req.params[:uuid]
514
+
515
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
516
+
517
+ op_metadata = self.metastore.fetch( uuid, *OPERATIONAL_METADATA_KEYS )
518
+ new_metadata = self.extract_metadata( req )
519
+ self.metastore.save( uuid, new_metadata.merge(op_metadata) )
520
+ self.send_event( :metadata_replaced, :uuid => uuid )
521
+
522
+ res = req.response
523
+ res.status = HTTP::NO_CONTENT
524
+
525
+ return res
526
+ end
527
+
528
+
529
+ # POST /«uuid»/metadata
530
+ # Merge new metadata into the existing metadata for «uuid».
531
+ post ':uuid/metadata' do |req|
532
+ uuid = req.params[:uuid]
533
+
534
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
535
+
536
+ new_metadata = self.extract_metadata( req )
537
+ self.metastore.merge( uuid, new_metadata )
538
+ self.send_event( :metadata_updated, :uuid => uuid )
539
+
540
+ res = req.response
541
+ res.status = HTTP::NO_CONTENT
542
+
543
+ return res
544
+ end
545
+
546
+
547
+ # DELETE /«uuid»/metadata
548
+ # Remove all (but operational) metadata associated with «uuid».
549
+ delete ':uuid/metadata' do |req|
550
+ uuid = req.params[:uuid]
551
+
552
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
553
+
554
+ self.metastore.remove_except( uuid, *OPERATIONAL_METADATA_KEYS )
555
+ self.send_event( :metadata_deleted, :uuid => uuid )
556
+
557
+ res = req.response
558
+ res.status = HTTP::NO_CONTENT
559
+
560
+ return res
561
+ end
562
+
563
+
564
+ # DELETE /«uuid»/metadata/«key»
565
+ # Remove the metadata associated with «key» for the given «uuid».
566
+ delete ':uuid/metadata/:key' do |req|
567
+ uuid = req.params[:uuid]
568
+ key = req.params[:key]
569
+
570
+ finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
571
+ finish_with( HTTP::FORBIDDEN, "Protected metadata." ) if
572
+ OPERATIONAL_METADATA_KEYS.include?( key )
573
+
574
+ self.metastore.remove( uuid, key )
575
+ self.send_event( :metadata_deleted, :uuid => uuid, :key => key )
576
+
577
+ res = req.response
578
+ res.status = HTTP::NO_CONTENT
579
+
580
+ return res
581
+ end
582
+
583
+
584
+ #########
585
+ protected
586
+ #########
587
+
588
+
589
+ ### Save the resource in the given +request+'s body and any associated metadata
590
+ ### or additional resources.
591
+ def save_resource( request, uuid=nil )
592
+ metadata = request.metadata
593
+ metadata.merge!( self.extract_header_metadata(request) )
594
+ metadata.merge!( self.extract_default_metadata(request) )
595
+
596
+ if uuid
597
+ self.log.info "Replacing resource %s (encoding: %p)" %
598
+ [ uuid, request.headers.content_encoding ]
599
+ self.datastore.replace( uuid, request.body )
600
+ self.metastore.merge( uuid, metadata )
601
+ else
602
+ self.log.info "Saving new resource (encoding: %p)." %
603
+ [ request.headers.content_encoding ]
604
+ uuid = self.datastore.save( request.body )
605
+ self.metastore.save( uuid, metadata )
606
+ end
607
+
608
+ self.save_related_resources( request, uuid )
609
+
610
+ return uuid, metadata
611
+ end
612
+
613
+
614
+ ### Save any related resources in the given +request+ with a relationship to the
615
+ ### resource with the given +uuid+.
616
+ def save_related_resources( request, uuid )
617
+ request.related_resources.each do |io, metadata|
618
+ self.log.debug "Saving a resource related to %s: %p" % [ uuid, metadata ]
619
+ next unless self.check_related_metadata( metadata )
620
+
621
+ self.log.debug " related metadata checks passed; storing it."
622
+ metadata = self.extract_connection_metadata( request ).merge( metadata )
623
+ self.log.debug " metadata is: %p" % [ metadata ]
624
+ r_uuid = self.datastore.save( io )
625
+ metadata['created'] = Time.now.getgm
626
+ metadata['relation'] = uuid
627
+
628
+ self.metastore.save( r_uuid, metadata )
629
+ self.log.debug " %s for %s saved as %s" %
630
+ [ metadata['relationship'], uuid, r_uuid ]
631
+ end
632
+ end
633
+
634
+
635
+ ### Remove any resources that are related to the one with the specified +uuid+.
636
+ def remove_related_resources( uuid )
637
+ self.metastore.fetch_related_oids( uuid ).each do |r_uuid|
638
+ self.datastore.remove( r_uuid )
639
+ self.metastore.remove( r_uuid )
640
+ self.log.info "Removed related resource %s for %s." % [ r_uuid, uuid ]
641
+ end
642
+ end
643
+
644
+
645
+ ### Do some consistency checks on the given +metadata+ for a related resource,
646
+ ### returning +true+ if it meets the requirements.
647
+ def check_related_metadata( metadata )
648
+ REQUIRED_RELATED_METADATA_KEYS.each do |attribute|
649
+ unless metadata[ attribute ]
650
+ self.log.error "Metadata for required resource must include '#{attribute}' attribute!"
651
+ return false
652
+ end
653
+ end
654
+ return true
655
+ end
656
+
657
+
658
+ ### Overridden from the base handler class to allow spooled uploads.
659
+ def handle_async_upload_start( request )
660
+ self.log.info "Starting asynchronous upload: %s" %
661
+ [ request.headers.x_mongrel2_upload_start ]
662
+ return nil
663
+ end
664
+
665
+
666
+ ### Return a Hash of default metadata extracted from the given +request+.
667
+ def extract_default_metadata( request )
668
+ return self.extract_connection_metadata( request ).merge(
669
+ 'extent' => request.headers.content_length,
670
+ 'format' => request.content_type,
671
+ 'created' => Time.now.getgm
672
+ )
673
+ end
674
+
675
+
676
+ ### Return a Hash of metadata extracted from the connection information
677
+ ### of the given +request+.
678
+ def extract_connection_metadata( request )
679
+ return {
680
+ 'useragent' => request.headers.user_agent,
681
+ 'uploadaddress' => request.remote_ip,
682
+ }
683
+ end
684
+
685
+
686
+ ### Extract and validate supplied metadata from the +request+.
687
+ def extract_metadata( req )
688
+ new_metadata = req.params.fields.dup
689
+ new_metadata.delete( :uuid )
690
+
691
+ protected_keys = OPERATIONAL_METADATA_KEYS & new_metadata.keys
692
+
693
+ unless protected_keys.empty?
694
+ finish_with HTTP::FORBIDDEN,
695
+ "Unable to alter protected metadata. (%s)" % [ protected_keys.join(', ') ]
696
+ end
697
+
698
+ return new_metadata
699
+ end
700
+
701
+
702
+ ### Extract metadata from X-ThingFish-* headers from the given +request+ and return
703
+ ### them as a Hash.
704
+ def extract_header_metadata( request )
705
+ self.log.debug "Extracting metadata from headers: %p" % [ request.headers ]
706
+ metadata = {}
707
+ request.headers.each do |header, value|
708
+ name = header.downcase[ /^x_thingfish_(?<name>[[:alnum:]\-]+)$/i, :name ] or next
709
+ self.log.debug "Found metadata header %p" % [ header ]
710
+ metadata[ name ] = value
711
+ end
712
+
713
+ return metadata
714
+ end
715
+
716
+
717
+ ### Send an event of +type+ with the given +msg+ over the zmq event socket.
718
+ def send_event( type, msg )
719
+ esock = self.event_socket or return
720
+ self.log.debug "Publishing %p event: %p" % [ type, msg ]
721
+ esock.sendm( type.to_s )
722
+ esock.send( Yajl.dump(msg) )
723
+ end
724
+
725
+
726
+ end # class Thingfish::Handler
727
+
728
+ # vim: set nosta noet ts=4 sw=4: