thingfish 0.5.0.pre20160707192835

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: