thingfish 0.5.0.pre20161103181816 → 0.5.0

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.
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' )