thingfish 0.5.0.pre20161103181816 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org/"
2
+ gemspec
data/History.rdoc CHANGED
@@ -1,5 +1,7 @@
1
- == v0.5.0 [2011-01-18] Michael Granger <ged@FaerieMUD.org>
1
+ == v0.5.0 [2016-11-14] Michael Granger <ged@FaerieMUD.org>
2
+
3
+ First public release.
4
+
2
5
 
3
- Rewritten to use Strelka.
4
6
 
5
7
 
data/Manifest.txt CHANGED
@@ -22,7 +22,6 @@ lib/thingfish/metastore.rb
22
22
  lib/thingfish/metastore/memory.rb
23
23
  lib/thingfish/mixins.rb
24
24
  lib/thingfish/processor.rb
25
- lib/thingfish/processor/mp3.rb
26
25
  lib/thingfish/processor/sha256.rb
27
26
  lib/thingfish/processordaemon.rb
28
27
  lib/thingfish/spechelpers.rb
@@ -38,7 +37,6 @@ spec/thingfish/handler_spec.rb
38
37
  spec/thingfish/metastore/memory_spec.rb
39
38
  spec/thingfish/metastore_spec.rb
40
39
  spec/thingfish/mixins_spec.rb
41
- spec/thingfish/processor/mp3_spec.rb
42
40
  spec/thingfish/processor/sha256_spec.rb
43
41
  spec/thingfish/processor_spec.rb
44
42
  spec/thingfish_spec.rb
data/README.rdoc CHANGED
@@ -26,19 +26,23 @@ fetch it, all through a REST API.
26
26
 
27
27
  === Requirements
28
28
 
29
- Thingfish is written in ruby, and is tested using version 2.0.0.
30
-
31
- * Ruby (>= 2.0.0): http://www.ruby-lang.org/en/downloads/
32
-
33
- Other versions may work, but are not tested.
29
+ Thingfish is written in ruby, and is tested using {version 2.3}[http://www.ruby-lang.org/en/downloads/]. Other versions may work,
30
+ but are not tested.
34
31
 
35
32
  === Ruby Modules
36
33
 
37
34
  You can install Thingfish via the Rubygems package manager:
38
35
 
39
- $ sudo gem install thingfish
36
+ $ gem install thingfish
37
+
38
+ This will install the basic server and its dependencies. Additional functionality is available via separate gems in the following namespaces:
40
39
 
41
- This will install the basic server and its dependencies. You can also install a collection of useful plugins via the 'thingfish-plugins' gem.
40
+ [thingfish-metastore-*]
41
+ Storage backends for resource metadata
42
+ [thingfish-filestore-*]
43
+ Storage backends for resources themselves
44
+ [thingfish-processor-*]
45
+ Filters and extractors for resources
42
46
 
43
47
 
44
48
  == Contributing
@@ -61,7 +65,7 @@ You can submit bug reports, suggestions, and read more about future plans at
61
65
 
62
66
  == License
63
67
 
64
- Copyright (c) 2007-2014, Michael Granger and Mahlon E. Smith
68
+ Copyright (c) 2007-2016, Michael Granger and Mahlon E. Smith
65
69
  All rights reserved.
66
70
 
67
71
  Redistribution and use in source and binary forms, with or without
data/Rakefile CHANGED
@@ -26,12 +26,11 @@ hoespec = Hoe.spec 'thingfish' do
26
26
  self.developer 'Mahlon E. Smith', 'mahlon@martini.nu'
27
27
  self.license "BSD"
28
28
 
29
- self.dependency 'strelka', '~> 0.9'
30
- self.dependency 'mongrel2', '~> 0.43'
29
+ self.dependency 'strelka', '~> 0.9'
30
+ self.dependency 'mongrel2', '~> 0.46'
31
31
 
32
- self.dependency 'hoe-deveiate', '~> 0.3', :development
33
- self.dependency 'simplecov', '~> 0.7', :development
34
- self.dependency 'ruby-mp3info', '~> 0.8', :development
32
+ self.dependency 'hoe-deveiate', '~> 0.3', :development
33
+ self.dependency 'simplecov', '~> 0.7', :development
35
34
 
36
35
  self.require_ruby_version( '>=2.0.0' )
37
36
 
@@ -0,0 +1,57 @@
1
+ # -*- ruby -*-
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ #encoding: utf-8
4
+
5
+ #
6
+ # This script generates a Mongrel2 configuration database suitable for
7
+ # getting a Thingfish handler running.
8
+ #
9
+ # Load it with:
10
+ #
11
+ # $ m2sh.rb -c mongrel2.sqlite load examples/mongrel2-config.rb
12
+ #
13
+ # Afterwards, ensure the path to the mongrel2.sqlite file is in your
14
+ # thingfish.conf:
15
+ #
16
+ # mongrel2:
17
+ # configdb: /path/to/mongrel2.sqlite
18
+ #
19
+ # ... and start the mongrel2 daemon:
20
+ #
21
+ # $ mongrel2 /path/to/mongrel2.sqlite thingfish
22
+ #
23
+ # In production use, you'll likely want to mount the Thingfish handler
24
+ # within the URI space of an existing Mongrel2 environment.
25
+ #
26
+
27
+ require 'mongrel2'
28
+ require 'mongrel2/config/dsl'
29
+
30
+ server 'thingfish' do
31
+ name 'Thingfish'
32
+ default_host 'localhost'
33
+
34
+ access_log 'logs/access.log'
35
+ error_log 'logs/error.log'
36
+ chroot ''
37
+ pid_file 'run/mongrel2.pid'
38
+
39
+ bind_addr '0.0.0.0'
40
+ port 3474
41
+
42
+ xrequest '/usr/local/lib/mongrel2/filters/sendfile.so'
43
+
44
+ host 'localhost' do
45
+ route '/', handler( 'tcp://127.0.0.1:9900', 'thingfish' )
46
+ end
47
+ end
48
+
49
+ setting 'zeromq.threads', 1
50
+ setting 'limits.content_length', 250_000
51
+ setting 'server.daemonize', false
52
+ setting 'upload.temp_store', 'var/uploads/mongrel2.upload.XXXXXX'
53
+
54
+ mkdir_p 'var/uploads'
55
+ mkdir_p 'run'
56
+ mkdir_p 'logs'
57
+
data/lib/thingfish.rb CHANGED
@@ -2,18 +2,7 @@
2
2
 
3
3
  require 'loggability'
4
4
 
5
- #
6
5
  # Network-accessable datastore service
7
- #
8
- # == Version
9
- #
10
- # $Id: thingfish.rb,v ce172208b523 2013/11/20 02:21:12 ged $
11
- #
12
- # == Authors
13
- #
14
- # * Michael Granger <ged@FaerieMUD.org>
15
- # * Mahlon E. Smith <mahlon@martini.nu>
16
- #
17
6
  module Thingfish
18
7
  extend Loggability
19
8
 
@@ -26,7 +15,7 @@ module Thingfish
26
15
  VERSION = '0.5.0'
27
16
 
28
17
  # Version control revision
29
- REVISION = %q$Revision: ce172208b523 $
18
+ REVISION = %q$Revision: 3607a807e2bf $
30
19
 
31
20
 
32
21
  ### Get the library version. If +include_buildnum+ is true, the version string will
@@ -193,7 +193,7 @@ class Thingfish::Handler < Strelka::App
193
193
  "The name(s) of the fields to order results by."
194
194
  param :direction, /^(asc|desc)$/i, "The order direction (ascending or descending)"
195
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"
196
+ param :relationship, /^[\w\-]+$/, "The name of the relationship between two resources"
197
197
 
198
198
 
199
199
  #
@@ -316,6 +316,9 @@ class Thingfish::Handler < Strelka::App
316
316
 
317
317
  finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
318
318
 
319
+ primary_metadata = self.metastore.fetch( uuid )
320
+ self.check_resource_permissions( req, uuid, primary_metadata )
321
+
319
322
  criteria = {
320
323
  'relation' => uuid,
321
324
  'relationship' => rel,
@@ -328,6 +331,9 @@ class Thingfish::Handler < Strelka::App
328
331
  metadata = self.metastore.fetch( uuid )
329
332
 
330
333
  res = req.response
334
+ self.add_etag_headers( req, metadata )
335
+ self.add_content_disposition( res, metadata )
336
+
331
337
  res.body = object
332
338
  res.content_type = metadata['format']
333
339
 
@@ -349,6 +355,7 @@ class Thingfish::Handler < Strelka::App
349
355
  res.content_type = metadata['format']
350
356
 
351
357
  self.add_etag_headers( req, metadata )
358
+ self.add_content_disposition( res, metadata )
352
359
 
353
360
  if object.respond_to?( :path )
354
361
  path = Pathname( object.path )
@@ -411,9 +418,9 @@ class Thingfish::Handler < Strelka::App
411
418
 
412
419
  self.check_resource_permissions( req, uuid )
413
420
 
414
- self.datastore.remove( uuid ) or finish_with( HTTP::NOT_FOUND, "No such object." )
415
- metadata = self.metastore.remove( uuid )
416
421
  self.remove_related_resources( uuid )
422
+ metadata = self.metastore.remove( uuid )
423
+ self.datastore.remove( uuid ) or finish_with( HTTP::NOT_FOUND, "No such object." )
417
424
  self.send_event( :deleted, :uuid => uuid )
418
425
 
419
426
  res = req.response
@@ -441,8 +448,7 @@ class Thingfish::Handler < Strelka::App
441
448
 
442
449
  finish_with( HTTP::NOT_FOUND, "No such object." ) unless self.metastore.include?( uuid )
443
450
 
444
- metadata = self.metastore.fetch( uuid )
445
- metadata[ 'oid' ] = uuid
451
+ metadata = self.normalized_metadata_for( uuid )
446
452
  self.check_resource_permissions( req, uuid, metadata )
447
453
 
448
454
  res = req.response
@@ -538,13 +544,17 @@ class Thingfish::Handler < Strelka::App
538
544
  metadata = self.metastore.fetch( uuid )
539
545
  self.check_resource_permissions( req, uuid, metadata )
540
546
 
541
- op_metadata = self.metastore.fetch( uuid, *OPERATIONAL_METADATA_KEYS )
547
+ op_metadata = self.metastore.fetch( uuid, *OPERATIONAL_METADATA_KEYS )
542
548
  new_metadata = self.extract_metadata( req )
543
- self.metastore.save( uuid, new_metadata.merge(op_metadata) )
549
+ self.metastore.transaction do
550
+ self.metastore.remove_except( uuid, *OPERATIONAL_METADATA_KEYS )
551
+ self.metastore.merge( uuid, new_metadata.merge(op_metadata) )
552
+ end
544
553
  self.send_event( :metadata_replaced, :uuid => uuid )
545
554
 
546
555
  res = req.response
547
- res.status = HTTP::NO_CONTENT
556
+ res.status = HTTP::OK
557
+ res.for( :json, :yaml ) { self.normalized_metadata_for(uuid) }
548
558
 
549
559
  return res
550
560
  end
@@ -565,7 +575,8 @@ class Thingfish::Handler < Strelka::App
565
575
  self.send_event( :metadata_updated, :uuid => uuid )
566
576
 
567
577
  res = req.response
568
- res.status = HTTP::NO_CONTENT
578
+ res.status = HTTP::OK
579
+ res.for( :json, :yaml ) { self.normalized_metadata_for(uuid) }
569
580
 
570
581
  return res
571
582
  end
@@ -585,7 +596,8 @@ class Thingfish::Handler < Strelka::App
585
596
  self.send_event( :metadata_deleted, :uuid => uuid )
586
597
 
587
598
  res = req.response
588
- res.status = HTTP::NO_CONTENT
599
+ res.status = HTTP::OK
600
+ res.for( :json, :yaml ) { self.normalized_metadata_for(uuid) }
589
601
 
590
602
  return res
591
603
  end
@@ -626,8 +638,8 @@ class Thingfish::Handler < Strelka::App
626
638
  metadata.merge!( self.extract_header_metadata(request) )
627
639
  metadata.merge!( self.extract_default_metadata(request) )
628
640
 
629
- self.verify_operational_metadata( metadata )
630
641
  self.check_resource_permissions( request, uuid, metadata )
642
+ self.verify_operational_metadata( metadata )
631
643
 
632
644
  if uuid
633
645
  self.log.info "Replacing resource %s (encoding: %p)" %
@@ -722,15 +734,7 @@ class Thingfish::Handler < Strelka::App
722
734
  ### Extract and validate supplied metadata from the +request+.
723
735
  def extract_metadata( req )
724
736
  new_metadata = req.params.fields.dup
725
- new_metadata.delete( :uuid )
726
-
727
- protected_keys = OPERATIONAL_METADATA_KEYS & new_metadata.keys
728
-
729
- unless protected_keys.empty?
730
- finish_with HTTP::FORBIDDEN,
731
- "Unable to alter protected metadata. (%s)" % [ protected_keys.join(', ') ]
732
- end
733
-
737
+ new_metadata = self.remove_operational_metadata( new_metadata )
734
738
  return new_metadata
735
739
  end
736
740
 
@@ -750,6 +754,13 @@ class Thingfish::Handler < Strelka::App
750
754
  end
751
755
 
752
756
 
757
+ ### Fetch the current metadata for +uuid+, altering it for easier
758
+ ### round trips with REST.
759
+ def normalized_metadata_for( uuid )
760
+ return self.metastore.fetch(uuid).merge( 'uuid' => uuid )
761
+ end
762
+
763
+
753
764
  ### Check that the metadata provided contains valid values for
754
765
  ### the operational keys, before saving a resource to disk.
755
766
  def verify_operational_metadata( metadata )
@@ -759,6 +770,13 @@ class Thingfish::Handler < Strelka::App
759
770
  end
760
771
 
761
772
 
773
+ ### Prune operational +metadata+ from the provided hash.
774
+ def remove_operational_metadata( metadata )
775
+ operationals = OPERATIONAL_METADATA_KEYS + [ 'uuid' ]
776
+ return metadata.reject{|key, _| operationals.include?(key) }
777
+ end
778
+
779
+
762
780
  ### Send an event of +type+ with the given +msg+ over the zmq event socket.
763
781
  def send_event( type, msg )
764
782
  esock = self.event_socket or return
@@ -785,6 +803,15 @@ class Thingfish::Handler < Strelka::App
785
803
  end
786
804
 
787
805
 
806
+ ### Add a filename "hint" for browsers, if the resource being fetched
807
+ ### has a 'title' attribute.
808
+ def add_content_disposition( res, metadata )
809
+ return unless metadata[ 'title' ]
810
+ title = metadata[ 'title' ].encode( 'us-ascii', :undef => :replace )
811
+ res.headers[ :content_disposition ] = "filename=%p" % [ title ]
812
+ end
813
+
814
+
788
815
  ### Supply a method that child handlers can override. The regular auth
789
816
  ### plugin runs too early (but can also be used), this hook allows
790
817
  ### child handlers to make access decisions based on the +request+
@@ -0,0 +1,51 @@
1
+ # -*- ruby -*-
2
+ #encoding: utf-8
3
+
4
+ require 'digest/sha2'
5
+
6
+ require 'thingfish' unless defined?( Thingfish )
7
+ require 'thingfish/processor' unless defined?( Thingfish::Processor )
8
+
9
+
10
+ # Calculate and store a sha256 checksum for a resource.
11
+ class Thingfish::Processor::SHA256 < Thingfish::Processor
12
+ extend Loggability
13
+
14
+ # The chunk size to read
15
+ CHUNK_SIZE = 32 * 1024
16
+
17
+ # Loggability API -- log to the :thingfish logger
18
+ log_to :thingfish
19
+
20
+ # The list of handled types
21
+ handled_types '*/*'
22
+
23
+
24
+ ### Synchronous processor API -- generate a checksum during upload.
25
+ def on_request( request )
26
+ request.add_metadata( :checksum => self.checksum(request.body) )
27
+ request.related_resources.each_pair do |io, metadata|
28
+ metadata[ :checksum ] = self.checksum( io )
29
+ end
30
+ end
31
+
32
+
33
+ #########
34
+ protected
35
+ #########
36
+
37
+ ### Given an +io+, return a sha256 checksum of it's contents.
38
+ def checksum( io )
39
+ digest = Digest::SHA256.new
40
+ buf = ''
41
+
42
+ while io.read( CHUNK_SIZE, buf )
43
+ digest.update( buf )
44
+ end
45
+
46
+ io.rewind
47
+ return digest.hexdigest
48
+ end
49
+
50
+ end # class Thingfish::Processor::SHA256
51
+
@@ -277,7 +277,9 @@ describe Thingfish::Handler do
277
277
  'format' => 'image/png',
278
278
  'extent' => @png_io.string.bytesize,
279
279
  'relation' => main_uuid,
280
- 'relationship' => "twinsies"
280
+ 'relationship' => "twinsies",
281
+ 'title' => 'Make America Smart Again.png',
282
+ 'checksum' => '123456'
281
283
  })
282
284
 
283
285
  req = factory.get( "/#{main_uuid}/related/twinsies" )
@@ -285,6 +287,8 @@ describe Thingfish::Handler do
285
287
 
286
288
  expect( res.status_line ).to match( /200 ok/i )
287
289
  expect( res.headers.content_type ).to eq( 'image/png' )
290
+ expect( res.headers.etag ).to eq( '123456' )
291
+ expect( res.headers.content_disposition ).to eq( 'filename="Make America Smart Again.png"' )
288
292
  expect( res.body.read ).to eq( TEST_PNG_DATA )
289
293
  end
290
294
 
@@ -367,6 +371,20 @@ describe Thingfish::Handler do
367
371
  end
368
372
 
369
373
 
374
+ it "adds content disposition filename, if the resource has a title" do
375
+ uuid = @handler.datastore.save( @png_io )
376
+ @handler.metastore.save( uuid, {'format' => 'image/png', 'title' => 'spょler"py.txt'} )
377
+
378
+ req = factory.get( "/#{uuid}" )
379
+ result = @handler.handle( req )
380
+
381
+ expect( result.status_line ).to match( /200 ok/i )
382
+ expect( result.body.read ).to eq( @png_io.string )
383
+ expect( result.headers.content_type ).to eq( 'image/png' )
384
+ expect( result.headers.content_disposition ).to eq( 'filename="sp?ler\"py.txt"' )
385
+ end
386
+
387
+
370
388
  it "returns a 304 not modified for unchanged client cache requests" do
371
389
  uuid = @handler.datastore.save( @png_io )
372
390
  @handler.metastore.save( uuid, 'format' => 'image/png', 'checksum' => '123456' )
@@ -433,7 +451,7 @@ describe Thingfish::Handler do
433
451
  expect( result.status ).to eq( 200 )
434
452
  expect( result.headers.content_type ).to eq( 'application/json' )
435
453
  expect( content_hash ).to be_a( Hash )
436
- expect( content_hash['oid'] ).to eq( uuid )
454
+ expect( content_hash['uuid'] ).to eq( uuid )
437
455
  expect( content_hash['extent'] ).to eq( 288 )
438
456
  expect( content_hash['created'] ).to eq( Time.at(1378313840).to_s )
439
457
  end
@@ -496,19 +514,20 @@ describe Thingfish::Handler do
496
514
  uuid = @handler.datastore.save( @png_io )
497
515
  @handler.metastore.save( uuid, {
498
516
  'format' => 'image/png',
499
- 'extent' => 288,
517
+ 'extent' => 288
500
518
  })
501
519
 
502
- body_json = Yajl.dump({ 'comment' => 'Ignore me!' })
520
+ body_json = Yajl.dump({ 'comment' => 'Ignore me!', 'uuid' => 123 })
503
521
  req = factory.post( "/#{uuid}/metadata", body_json, 'Content-type' => 'application/json' )
504
522
  result = @handler.handle( req )
505
523
 
506
- expect( result.status ).to eq( HTTP::NO_CONTENT )
524
+ expect( result.status ).to eq( HTTP::OK )
507
525
  expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'Ignore me!' )
526
+ expect( @handler.metastore.fetch_value(uuid, 'uuid') ).to be_nil
508
527
  end
509
528
 
510
529
 
511
- it "returns FORBIDDEN when attempting to merge metadata with operational keys" do
530
+ it "ignores attempts to alter operational metadata when merging" do
512
531
  uuid = @handler.datastore.save( @png_io )
513
532
  @handler.metastore.save( uuid, {
514
533
  'format' => 'image/png',
@@ -519,10 +538,8 @@ describe Thingfish::Handler do
519
538
  req = factory.post( "/#{uuid}/metadata", body_json, 'Content-type' => 'application/json' )
520
539
  result = @handler.handle( req )
521
540
 
522
- expect( result.status ).to eq( HTTP::FORBIDDEN )
523
- expect( result.body.string ).to match( /unable to alter protected metadata/i )
524
- expect( result.body.string ).to match( /format/i )
525
- expect( @handler.metastore.fetch_value(uuid, 'comment') ).to be_nil
541
+ expect( result.status ).to eq( HTTP::OK )
542
+ expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'Ignore me!' )
526
543
  expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
527
544
  end
528
545
 
@@ -630,7 +647,7 @@ describe Thingfish::Handler do
630
647
  'Content-type' => 'application/json' )
631
648
  result = @handler.handle( req )
632
649
 
633
- expect( result.status ).to eq( HTTP::NO_CONTENT )
650
+ expect( result.status ).to eq( HTTP::OK )
634
651
  expect( @handler.metastore.fetch_value(uuid, 'comment') ).to eq( 'Yeah.' )
635
652
  expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
636
653
  expect( @handler.metastore ).to_not include( 'ephemeral' )
@@ -652,8 +669,8 @@ describe Thingfish::Handler do
652
669
  req = factory.delete( "/#{uuid}/metadata" )
653
670
  result = @handler.handle( req )
654
671
 
655
- expect( result.status ).to eq( HTTP::NO_CONTENT )
656
- expect( result.body.string ).to be_empty
672
+ expect( result.status ).to eq( HTTP::OK )
673
+ expect( result.body.string ).to_not be_empty
657
674
  expect( @handler.metastore.fetch_value(uuid, 'format') ).to eq( 'image/png' )
658
675
  expect( @handler.metastore.fetch_value(uuid, 'extent') ).to eq( 288 )
659
676
  expect( @handler.metastore.fetch_value(uuid, 'uploadaddress') ).to eq( '127.0.0.1' )