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.
- checksums.yaml +7 -0
- data/.simplecov +7 -0
- data/History.rdoc +5 -0
- data/LICENSE +29 -0
- data/Manifest.txt +39 -0
- data/Procfile +4 -0
- data/README.rdoc +92 -0
- data/Rakefile +92 -0
- data/bin/tfprocessord +6 -0
- data/bin/thingfish +10 -0
- data/etc/thingfish.conf.example +26 -0
- data/lib/strelka/app/metadata.rb +38 -0
- data/lib/strelka/httprequest/metadata.rb +70 -0
- data/lib/thingfish.rb +43 -0
- data/lib/thingfish/behaviors.rb +263 -0
- data/lib/thingfish/datastore.rb +55 -0
- data/lib/thingfish/datastore/memory.rb +93 -0
- data/lib/thingfish/handler.rb +728 -0
- data/lib/thingfish/metastore.rb +55 -0
- data/lib/thingfish/metastore/memory.rb +201 -0
- data/lib/thingfish/mixins.rb +57 -0
- data/lib/thingfish/processor.rb +79 -0
- data/lib/thingfish/processor/mp3.rb +167 -0
- data/lib/thingfish/processordaemon.rb +16 -0
- data/lib/thingfish/spechelpers.rb +165 -0
- data/spec/data/APIC-1-image.mp3 +0 -0
- data/spec/data/APIC-2-images.mp3 +0 -0
- data/spec/data/PIC-1-image.mp3 +0 -0
- data/spec/data/PIC-2-images.mp3 +0 -0
- data/spec/helpers.rb +67 -0
- data/spec/spec.opts +4 -0
- data/spec/thingfish/datastore/memory_spec.rb +19 -0
- data/spec/thingfish/datastore_spec.rb +64 -0
- data/spec/thingfish/handler_spec.rb +838 -0
- data/spec/thingfish/metastore/memory_spec.rb +17 -0
- data/spec/thingfish/metastore_spec.rb +96 -0
- data/spec/thingfish/mixins_spec.rb +63 -0
- data/spec/thingfish/processor/mp3_spec.rb +50 -0
- data/spec/thingfish/processor_spec.rb +65 -0
- data/spec/thingfish_spec.rb +23 -0
- 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:
|