sequenceserver 0.8.9 → 1.0.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (213) hide show
  1. checksums.yaml +4 -4
  2. data/{README.txt → README.md} +2 -0
  3. data/bin/sequenceserver +255 -55
  4. data/config.ru +2 -4
  5. data/lib/sequenceserver.rb +293 -447
  6. data/lib/sequenceserver/blast.rb +464 -64
  7. data/lib/sequenceserver/database.rb +185 -19
  8. data/lib/sequenceserver/links.rb +114 -0
  9. data/lib/sequenceserver/logger.rb +27 -0
  10. data/lib/sequenceserver/sequence.rb +141 -0
  11. data/public/css/bootstrap.min.css +8 -413
  12. data/public/css/custom.css +363 -122
  13. data/public/css/font-awesome.min.css +4 -0
  14. data/public/fonts/FontAwesome.otf +0 -0
  15. data/public/fonts/fontawesome-webfont.eot +0 -0
  16. data/public/fonts/fontawesome-webfont.svg +565 -0
  17. data/public/fonts/fontawesome-webfont.ttf +0 -0
  18. data/public/fonts/fontawesome-webfont.woff +0 -0
  19. data/public/fonts/fontawesome-webfont.woff2 +0 -0
  20. data/public/js/bootstrap.min.js +11 -0
  21. data/public/js/d3.v3.min.js +5 -0
  22. data/public/js/html5shiv.min.js +4 -0
  23. data/public/js/jquery.scrollspy.js +74 -0
  24. data/public/js/jquery.t.js +353 -0
  25. data/public/js/sequence.js +2419 -0
  26. data/public/js/sequenceserver.blast.js +29 -30
  27. data/public/js/sequenceserver.js +544 -120
  28. data/public/js/underscore.min.js +6 -0
  29. data/public/js/webshims/polyfiller.js +1 -0
  30. data/public/js/webshims/shims/FlashCanvas/canvas2png.js +1 -0
  31. data/public/js/webshims/shims/FlashCanvas/flashcanvas.js +1 -0
  32. data/public/js/webshims/shims/FlashCanvas/flashcanvas.swf +0 -0
  33. data/public/js/webshims/shims/FlashCanvasPro/canvas2png.js +1 -0
  34. data/public/js/webshims/shims/FlashCanvasPro/flash10canvas.swf +0 -0
  35. data/public/js/webshims/shims/FlashCanvasPro/flash9canvas.swf +0 -0
  36. data/public/js/webshims/shims/FlashCanvasPro/flashcanvas.js +1 -0
  37. data/public/js/webshims/shims/canvas-blob.js +1 -0
  38. data/public/js/webshims/shims/color-picker.js +2 -0
  39. data/public/js/webshims/shims/combos/1.js +6 -0
  40. data/public/js/webshims/shims/combos/10.js +2 -0
  41. data/public/js/webshims/shims/combos/11.js +2 -0
  42. data/public/js/webshims/shims/combos/12.js +6 -0
  43. data/public/js/webshims/shims/combos/13.js +1 -0
  44. data/public/js/webshims/shims/combos/14.js +1 -0
  45. data/public/js/webshims/shims/combos/15.js +2 -0
  46. data/public/js/webshims/shims/combos/16.js +7 -0
  47. data/public/js/webshims/shims/combos/17.js +2 -0
  48. data/public/js/webshims/shims/combos/18.js +3 -0
  49. data/public/js/webshims/shims/combos/2.js +7 -0
  50. data/public/js/webshims/shims/combos/21.js +2 -0
  51. data/public/js/webshims/shims/combos/22.js +1 -0
  52. data/public/js/webshims/shims/combos/23.js +6 -0
  53. data/public/js/webshims/shims/combos/25.js +2 -0
  54. data/public/js/webshims/shims/combos/27.js +1 -0
  55. data/public/js/webshims/shims/combos/28.js +1 -0
  56. data/public/js/webshims/shims/combos/29.js +1 -0
  57. data/public/js/webshims/shims/combos/3.js +1 -0
  58. data/public/js/webshims/shims/combos/30.js +2 -0
  59. data/public/js/webshims/shims/combos/31.js +1 -0
  60. data/public/js/webshims/shims/combos/33.js +1 -0
  61. data/public/js/webshims/shims/combos/34.js +1 -0
  62. data/public/js/webshims/shims/combos/4.js +1 -0
  63. data/public/js/webshims/shims/combos/5.js +2 -0
  64. data/public/js/webshims/shims/combos/6.js +2 -0
  65. data/public/js/webshims/shims/combos/7.js +7 -0
  66. data/public/js/webshims/shims/combos/8.js +7 -0
  67. data/public/js/webshims/shims/combos/9.js +2 -0
  68. data/public/js/webshims/shims/combos/97.js +1 -0
  69. data/public/js/webshims/shims/combos/98.js +1 -0
  70. data/public/js/webshims/shims/combos/99.js +1 -0
  71. data/public/js/webshims/shims/details.js +1 -0
  72. data/public/js/webshims/shims/dom-extend.js +1 -0
  73. data/public/js/webshims/shims/es5.js +1 -0
  74. data/public/js/webshims/shims/es6.js +1 -0
  75. data/public/js/webshims/shims/excanvas.js +1 -0
  76. data/public/js/webshims/shims/filereader-xhr.js +1 -0
  77. data/public/js/webshims/shims/form-combat.js +1 -0
  78. data/public/js/webshims/shims/form-core.js +1 -0
  79. data/public/js/webshims/shims/form-datalist-lazy.js +1 -0
  80. data/public/js/webshims/shims/form-datalist.js +1 -0
  81. data/public/js/webshims/shims/form-fixrangechange.js +1 -0
  82. data/public/js/webshims/shims/form-inputmode.js +1 -0
  83. data/public/js/webshims/shims/form-message.js +1 -0
  84. data/public/js/webshims/shims/form-native-extend.js +1 -0
  85. data/public/js/webshims/shims/form-number-date-api.js +1 -0
  86. data/public/js/webshims/shims/form-number-date-ui.js +1 -0
  87. data/public/js/webshims/shims/form-shim-extend.js +1 -0
  88. data/public/js/webshims/shims/form-shim-extend2.js +1 -0
  89. data/public/js/webshims/shims/form-validation.js +1 -0
  90. data/public/js/webshims/shims/form-validators.js +1 -0
  91. data/public/js/webshims/shims/forms-picker.js +1 -0
  92. data/public/js/webshims/shims/geolocation.js +1 -0
  93. data/public/js/webshims/shims/i18n/formcfg-ar.js +1 -0
  94. data/public/js/webshims/shims/i18n/formcfg-ch-CN.js +1 -0
  95. data/public/js/webshims/shims/i18n/formcfg-cs.js +1 -0
  96. data/public/js/webshims/shims/i18n/formcfg-de.js +1 -0
  97. data/public/js/webshims/shims/i18n/formcfg-el.js +1 -0
  98. data/public/js/webshims/shims/i18n/formcfg-en.js +1 -0
  99. data/public/js/webshims/shims/i18n/formcfg-es.js +1 -0
  100. data/public/js/webshims/shims/i18n/formcfg-fa.js +1 -0
  101. data/public/js/webshims/shims/i18n/formcfg-fr.js +1 -0
  102. data/public/js/webshims/shims/i18n/formcfg-he.js +1 -0
  103. data/public/js/webshims/shims/i18n/formcfg-hi.js +1 -0
  104. data/public/js/webshims/shims/i18n/formcfg-hu.js +1 -0
  105. data/public/js/webshims/shims/i18n/formcfg-it.js +1 -0
  106. data/public/js/webshims/shims/i18n/formcfg-ja.js +1 -0
  107. data/public/js/webshims/shims/i18n/formcfg-lt.js +1 -0
  108. data/public/js/webshims/shims/i18n/formcfg-nl.js +1 -0
  109. data/public/js/webshims/shims/i18n/formcfg-pl.js +1 -0
  110. data/public/js/webshims/shims/i18n/formcfg-pt-BR.js +1 -0
  111. data/public/js/webshims/shims/i18n/formcfg-pt-PT.js +1 -0
  112. data/public/js/webshims/shims/i18n/formcfg-pt.js +1 -0
  113. data/public/js/webshims/shims/i18n/formcfg-ru.js +1 -0
  114. data/public/js/webshims/shims/i18n/formcfg-sv.js +1 -0
  115. data/public/js/webshims/shims/i18n/formcfg-zh-CN.js +1 -0
  116. data/public/js/webshims/shims/i18n/formcfg-zh-TW.js +1 -0
  117. data/public/js/webshims/shims/jme/alternate-media.js +1 -0
  118. data/public/js/webshims/shims/jme/base.js +1 -0
  119. data/public/js/webshims/shims/jme/controls.css +1 -0
  120. data/public/js/webshims/shims/jme/jme.eot +0 -0
  121. data/public/js/webshims/shims/jme/jme.svg +36 -0
  122. data/public/js/webshims/shims/jme/jme.ttf +0 -0
  123. data/public/js/webshims/shims/jme/jme.woff +0 -0
  124. data/public/js/webshims/shims/jme/mediacontrols-lazy.js +1 -0
  125. data/public/js/webshims/shims/jme/mediacontrols.js +1 -0
  126. data/public/js/webshims/shims/jme/playlist.js +1 -0
  127. data/public/js/webshims/shims/jpicker/images/AlphaBar.png +0 -0
  128. data/public/js/webshims/shims/jpicker/images/Bars.png +0 -0
  129. data/public/js/webshims/shims/jpicker/images/Maps.png +0 -0
  130. data/public/js/webshims/shims/jpicker/images/NoColor.png +0 -0
  131. data/public/js/webshims/shims/jpicker/images/bar-opacity.png +0 -0
  132. data/public/js/webshims/shims/jpicker/images/map-opacity.png +0 -0
  133. data/public/js/webshims/shims/jpicker/images/mappoint.gif +0 -0
  134. data/public/js/webshims/shims/jpicker/images/picker.gif +0 -0
  135. data/public/js/webshims/shims/jpicker/images/preview-opacity.png +0 -0
  136. data/public/js/webshims/shims/jpicker/images/rangearrows.gif +0 -0
  137. data/public/js/webshims/shims/jpicker/jpicker.css +1 -0
  138. data/public/js/webshims/shims/matchMedia.js +3 -0
  139. data/public/js/webshims/shims/mediacapture-picker.js +1 -0
  140. data/public/js/webshims/shims/mediacapture.js +1 -0
  141. data/public/js/webshims/shims/mediaelement-core.js +1 -0
  142. data/public/js/webshims/shims/mediaelement-debug.js +1 -0
  143. data/public/js/webshims/shims/mediaelement-jaris.js +1 -0
  144. data/public/js/webshims/shims/mediaelement-native-fix.js +1 -0
  145. data/public/js/webshims/shims/mediaelement-yt.js +1 -0
  146. data/public/js/webshims/shims/moxie/flash/Moxie.cdn.swf +0 -0
  147. data/public/js/webshims/shims/moxie/flash/Moxie.min.swf +0 -0
  148. data/public/js/webshims/shims/moxie/js/moxie-html4.js +3 -0
  149. data/public/js/webshims/shims/moxie/js/moxie-swf.js +2 -0
  150. data/public/js/webshims/shims/picture.js +1 -0
  151. data/public/js/webshims/shims/plugins/jquery.ui.position.js +11 -0
  152. data/public/js/webshims/shims/range-ui.js +1 -0
  153. data/public/js/webshims/shims/sizzle.js +11 -0
  154. data/public/js/webshims/shims/sticky.js +1 -0
  155. data/public/js/webshims/shims/styles/color-picker.png +0 -0
  156. data/public/js/webshims/shims/styles/forms-ext.css +1 -0
  157. data/public/js/webshims/shims/styles/forms-picker.css +1 -0
  158. data/public/js/webshims/shims/styles/progress.gif +0 -0
  159. data/public/js/webshims/shims/styles/progress.png +0 -0
  160. data/public/js/webshims/shims/styles/shim-ext.css +1 -0
  161. data/public/js/webshims/shims/styles/shim.css +1 -0
  162. data/public/js/webshims/shims/styles/transparent.png +0 -0
  163. data/public/js/webshims/shims/styles/widget.eot +0 -0
  164. data/public/js/webshims/shims/styles/widget.svg +12 -0
  165. data/public/js/webshims/shims/styles/widget.ttf +0 -0
  166. data/public/js/webshims/shims/styles/widget.woff +0 -0
  167. data/public/js/webshims/shims/swf/JarisFLVPlayer.swf +0 -0
  168. data/public/js/webshims/shims/swfmini-embed.js +1 -0
  169. data/public/js/webshims/shims/swfmini.js +6 -0
  170. data/public/js/webshims/shims/track-ui.js +1 -0
  171. data/public/js/webshims/shims/track.js +1 -0
  172. data/public/js/webshims/shims/url.js +1 -0
  173. data/public/js/webshims/shims/usermedia-core.js +1 -0
  174. data/public/js/webshims/shims/usermedia-shim.js +1 -0
  175. data/sequenceserver.gemspec +16 -13
  176. data/views/400.erb +28 -0
  177. data/views/500.erb +35 -19
  178. data/views/_options.erb +6 -15
  179. data/views/result.erb +218 -0
  180. data/views/search.erb +354 -151
  181. metadata +254 -62
  182. data/example.config.yml +0 -39
  183. data/lib/sequenceserver/customisation.rb +0 -60
  184. data/lib/sequenceserver/database_formatter.rb +0 -190
  185. data/lib/sequenceserver/helpers.rb +0 -136
  186. data/lib/sequenceserver/sequencehelpers.rb +0 -93
  187. data/lib/sequenceserver/sinatralikeloggerformatter.rb +0 -12
  188. data/lib/sequenceserver/version.rb +0 -9
  189. data/public/css/beige.css.css +0 -254
  190. data/public/css/bootstrap.dropdown.css +0 -29
  191. data/public/css/bootstrap.icons.css +0 -155
  192. data/public/css/bootstrap.modal.css +0 -28
  193. data/public/js/bootstrap.dropdown.js +0 -92
  194. data/public/js/bootstrap.modal.js +0 -7
  195. data/public/js/bootstrap.transition.js +0 -7
  196. data/public/js/jquery-scrollspy.js +0 -98
  197. data/public/js/jquery.activity.js +0 -10
  198. data/public/js/jquery.enablePlaceholder.min.js +0 -10
  199. data/public/js/store.min.js +0 -2
  200. data/public/sequence.html +0 -28
  201. data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta +0 -5486
  202. data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nhr +0 -0
  203. data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nin +0 -0
  204. data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nsq +0 -0
  205. data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta +0 -6449
  206. data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.phr +0 -0
  207. data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.pin +0 -0
  208. data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.psq +0 -0
  209. data/tests/run +0 -26
  210. data/tests/test_sequencehelpers.rb +0 -77
  211. data/tests/test_sequenceserver_blast.rb +0 -60
  212. data/tests/test_ui.rb +0 -104
  213. data/tests/ui.specs.todo +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 631b0671719d8700180eb7987e01f0a951b68cfa
4
- data.tar.gz: 028caff7b7edae94848eaba84dfd48ac5c6f5f81
3
+ metadata.gz: b4b6422d0e86b47690118cf32a0633609fd8e545
4
+ data.tar.gz: 01dda58ef4ab7bac5c889508361be848482cfc75
5
5
  SHA512:
6
- metadata.gz: c1064444047ef36dc19d357bb7ad639a7dd4d4786ff4dee1da8d631dbdacd5bf00530a530d8bb9ba2887616535978f13a1ff1ad7a20974594a3e9dec3bcb99b5
7
- data.tar.gz: a1379fe0d72402dc1c76ee8197c7ce1faa2a01e4e5c899cff5679c641b2788f419cbdbe4cbcaa305387ea6c292abb8d5bf285f0244551fc2f8ec64e98959271e
6
+ metadata.gz: c469b959e1fa91763766a7584b2b8ba6950c057d68319cb5ffc7e7dffe8b903bdff88037264ff9a6e71a6b2914a8c733e061b0c065c6934af6f41e633f01e889
7
+ data.tar.gz: 6cc6dcf46bbf6e4d93437e284102083de62fb30e35e18f0a6f63461dfbaa27f1fe9e7a019b2bcab48ca84d67b787598dc7fde277dcb9ffaad9c507f64e76b89c
@@ -2,4 +2,6 @@ Thanks for downloading SequenceServer!
2
2
 
3
3
  Documentation available at http://www.sequenceserver.com
4
4
 
5
+ [![build status](https://secure.travis-ci.org/yannickwurm/sequenceserver.png?branch=master)](https://travis-ci.org/yannickwurm/sequenceserver)
6
+
5
7
  -- Yannick Wurm, Ben Woodcroft, Anurag Priyam
data/bin/sequenceserver CHANGED
@@ -1,82 +1,282 @@
1
1
  #!/usr/bin/env ruby
2
+ require 'readline'
3
+ require 'slop'
2
4
 
3
- require 'rubygems'
4
- require 'bundler/setup'
5
- require 'optparse'
6
- require 'sequenceserver'
5
+ ENV['RACK_ENV'] ||= 'production'
7
6
 
8
- # e.g:
9
- # sequenceserver --config .sequenceserver.conf format_database
10
- begin
11
- # parse command line till first non-option, removing parsed options from ARGV
12
- OptionParser.new do |opts|
13
- opts.banner =<<BANNER
7
+ # display name for tools like `ps`
8
+ $PROGRAM_NAME = 'sequenceserver'
14
9
 
10
+ begin
11
+ Slop.parse!(:strict => true, :help => true) do
12
+ banner <<BANNER
15
13
  SUMMARY
16
14
 
17
- custom, local, BLAST server
15
+ custom, local, BLAST server
18
16
 
19
17
  USAGE
20
18
 
21
- sequenceserver [options] [subcommand] [subcommand's options]
19
+ sequenceserver [options]
22
20
 
23
- Example:
21
+ Example:
24
22
 
25
- # launch SequenceServer with the given config file
26
- $ sequenceserver --config ~/.sequenceserver.ants.conf
23
+ # launch SequenceServer with the given config file
24
+ $ sequenceserver -c ~/.sequenceserver.ants.conf
27
25
 
28
- # use the bundled database formatter utility to prepare databases for use
29
- # with SequenceServer
30
- $ sequenceserver format-databases
26
+ # use the bundled database formatter utility to prepare databases for use
27
+ # with SequenceServer
28
+ $ sequenceserver -m
31
29
 
32
30
  DESCRIPTION
33
31
 
34
- SequenceServer lets you rapidly set up a BLAST+ server with an intuitive user
35
- interface for use locally or over the web.
32
+ SequenceServer lets you rapidly set up a BLAST+ server with an intuitive user
33
+ interface for use locally or over the web.
36
34
 
37
- SUB-COMMANDS
35
+ If BLAST+ is not installed on your system, SequenceServer will offer to install
36
+ BLAST+ for you.
38
37
 
39
- format-databases:
40
- prepare BLAST databases for use with SequenceServer
38
+ You should only ever have to point it to a directory of FASTA files.
41
39
 
42
- Run '#{$0} format-databases -h' for help.
40
+ In a given directory, SequenceServer is able to tell FASTA files that are yet
41
+ to be formatted for use with BLAST+ and format them, and FASTA files that are
42
+ already formatted for use with BLAST+, heuristically skipping all other files
43
+ in the directory. Directories are scanned recursively. Type of sequences in a
44
+ FASTA file is detected automagically. `parse_seqids` option of `makeblastdb` is
45
+ used to create BLAST+ databases.
46
+ BANNER
43
47
 
44
- OPTIONS
48
+ on 'c', 'config_file=',
49
+ 'Use the given configuration file',
50
+ :argument => true
45
51
 
46
- BANNER
47
- opts.on('-c', '--config CONFIG_FILE', 'Use the given configuration file') do |config_file|
48
- SequenceServer::App.config_file = File.expand_path(config_file)
49
- end
52
+ on 'b', 'bin=',
53
+ 'Load BLAST+ binaries from this directory',
54
+ :argument => true
50
55
 
51
- opts.on('-v', '--version', 'Print version number of SequenceServer that will be loaded.' ) do |config_file|
52
- puts SequenceServer.version
53
- exit
54
- end
55
- end.order!
56
-
57
- # of the remaining items in ARGV, the first one must be a subcommand
58
- subcommand = ARGV.shift
59
-
60
- if subcommand
61
- # process subcommands now
62
-
63
- case subcommand
64
- when 'format-databases'
65
- require 'sequenceserver/database_formatter'
66
- exit
67
- else
68
- puts "invalid subcommand: #{subcommand}"
69
- puts "Run '#{$0} -h' for help with command line options."
70
- exit
56
+ on 'd', 'database_dir=',
57
+ 'Read FASTA and BLAST database from this directory',
58
+ :argument => true
59
+
60
+ on 'n', 'num_threads=',
61
+ 'Number of threads to use to run a BLAST search',
62
+ :argument => true
63
+ #:as => Integer
64
+
65
+ on 'r', 'require=',
66
+ 'Load extension from this file',
67
+ :argument => true
68
+
69
+ on 'h', 'host=',
70
+ 'Host to run SequenceServer on',
71
+ :argument => true
72
+
73
+ on 'p', 'port=',
74
+ 'Port to run SequenceServer on',
75
+ :argument => true
76
+
77
+ on 's', 'set',
78
+ 'Set configuration value in default or given config file'
79
+
80
+ on 'm', 'make-blast-databases',
81
+ 'Create BLAST databases'
82
+
83
+ on 'l', 'list_databases',
84
+ 'List BLAST databases'
85
+
86
+ on 'u', 'list-unformatted-fastas',
87
+ 'List unformatted FASTA files'
88
+
89
+ on 'i', 'interactive',
90
+ 'Run SequenceServer in interactive mode'
91
+
92
+ on 'D', 'devel',
93
+ 'Start SequenceServer in development mode'
94
+
95
+ on '-v', '--version',
96
+ 'Print version number of SequenceServer that will be loaded'
97
+
98
+ on '-h', '--help',
99
+ 'Display this help message'
100
+
101
+ run do
102
+ if version?
103
+ gemspec_file = File.join(File.dirname(__FILE__), '..', 'sequenceserver.gemspec')
104
+ gemspec = eval File.read gemspec_file
105
+ puts gemspec.version
106
+ exit
107
+ end
108
+
109
+ ENV['RACK_ENV'] = 'development' if devel?
110
+ # Save the state of user terminal
111
+ stty_save = `stty -g`.chomp
112
+ # Exit gracefully on SIGINT
113
+ trap('INT') do
114
+ puts ''
115
+ puts 'Aborted.'
116
+ system('stty', stty_save)
117
+ exit
118
+ end
119
+
120
+ require 'sequenceserver'
121
+ begin
122
+ opts = to_h.delete_if{|k, v| v.nil?}
123
+ opts.delete :set
124
+ SequenceServer.init opts
125
+ rescue SystemExit => e
126
+ # The aim of following error recovery scenarios is to guide user to a
127
+ # working SequenceServer installation. We expect to land following
128
+ # error scenarios either when creating a new SequenceServer (first
129
+ # time or later), or updating config values using -s CLI option.
130
+
131
+ # Try error recovery now.
132
+ case e.status
133
+ when SequenceServer::EXIT_CONFIG_FILE_NOT_FOUND
134
+ # Create an empty configuration file to make SequenceServer happy.
135
+ File.open(SequenceServer.config_file, 'w') {}
136
+ puts " Created."
137
+ redo
138
+ when SequenceServer::EXIT_BLAST_NOT_INSTALLED,
139
+ SequenceServer::EXIT_BLAST_NOT_COMPATIBLE
140
+
141
+ # Set a flag so that if we recovered from error resulting config can be
142
+ # saved. Config will be saved unless invoked with -b option.
143
+ fetch_option(:set).value = !bin?
144
+
145
+ # Ask user if she already has BLAST+ downloaded or offer to download
146
+ # BLAST+ for her.
147
+ puts
148
+ print 'Do you have BLAST+ downloaded already? [y/n] (Default: n): '
149
+ response = STDIN.gets.to_s.strip
150
+ if response.match(/^[y]$/i)
151
+ response = Readline.readline('Where? ').to_s.strip
152
+ unless File.basename(response) == 'bin'
153
+ fetch_option(:bin).value = File.join(response, 'bin')
154
+ end
155
+ redo
156
+ end
157
+
158
+ puts
159
+ print "Install BLAST+? [y/n] (Default: y): "
160
+ response = STDIN.gets.to_s.strip
161
+ unless response.match(/^[n]$/i)
162
+ puts
163
+ puts "*** Installing BLAST+."
164
+ puts "RUBY_PLATFORM #{RUBY_PLATFORM}"
165
+
166
+ version = SequenceServer::MINIMUM_BLAST_VERSION
167
+ url = case RUBY_PLATFORM
168
+ when /i686-linux/ # 32 bit Linux
169
+ "ftp://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/#{version.chop}/ncbi-blast-#{version}-ia32-linux.tar.gz"
170
+ when /x86_64-linux/ # 64 bit Linux
171
+ "ftp://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/#{version.chop}/ncbi-blast-#{version}-x64-linux.tar.gz"
172
+ when /darwin/ # Mac
173
+ "ftp://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/#{version.chop}/ncbi-blast-#{version}-universal-macosx.tar.gz"
174
+ else
175
+ puts <<ERR
176
+ ------------------------------------------------------------------------
177
+ FAILED!! to install NCBI BLAST+.
178
+
179
+ We currently support Linux and Mac only (both 32 and 64 bit). If you
180
+ believe you are running a supported platform, please open a support
181
+ ticket titled "#{RUBY_PLATFORM}" at:
182
+
183
+ https://github.com/yannickwurm/sequenceserver/issues
184
+ ------------------------------------------------------------------------
185
+
186
+ ERR
187
+ end
188
+
189
+ archive = File.join('/tmp', File.basename(url))
190
+ system "wget -c #{url} -O #{archive} && mkdir -p ~/.sequenceserver && tar xvf #{archive} -C ~/.sequenceserver"
191
+ unless $?.success?
192
+ puts "*** Failed to install BLAST+."
193
+ puts " You may need to download BLAST+ from - "
194
+ puts " http://www.ncbi.nlm.nih.gov/blast/Blast.cgi?CMD=Web&PAGE_TYPE=BlastDocs&DOC_TYPE=Download"
195
+ puts " Please ensure that you download BLAST+ version
196
+ #{SequenceServer::MINIMUM_BLAST_VERSION} or higher."
197
+ exit SequenceServer::EXIT_BLAST_INSTALLATION_FAILED
198
+ end
199
+ fetch_option(:bin).value = "~/.sequenceserver/ncbi-blast-#{version}/bin/"
200
+ redo
201
+ else
202
+ exit e.status unless set?
203
+ end
204
+ when SequenceServer::EXIT_NO_SEQUENCE_DIR
205
+ # Ask user for the directory containing sequences or BLAST+
206
+ # databases.
207
+
208
+ # Set a flag so that if we recovered from error resulting config can be
209
+ # saved. Config will be saved unless invoked with -d option.
210
+ fetch_option(:set).value = !database_dir?
211
+
212
+ puts
213
+ prompt = 'Where are the sequences you would like to search?' \
214
+ ' (Default: current dir) '
215
+ response = Readline.readline(prompt).to_s.strip
216
+ fetch_option(:database_dir).value = response
217
+ redo
218
+ when SequenceServer::EXIT_NO_BLAST_DATABASE
219
+ unless list_databases? or list_unformatted_fastas? or make_blast_databases?
220
+ # Offer user to format the FASTA files.
221
+ puts
222
+ print 'Create databases? [y/n] (Default: y): '
223
+ response = STDIN.gets.to_s.strip
224
+ unless response.match(/^[n]$/i)
225
+ puts
226
+ puts "*** Creating databases."
227
+ SequenceServer::Database.make_blast_databases
228
+ redo
229
+ else
230
+ exit e.status unless set?
231
+ end
232
+ end
233
+ else
234
+ exit e.status
235
+ end
236
+ end
237
+
238
+ if interactive?
239
+ SequenceServer.irb
240
+ exit
241
+ end
242
+
243
+ if list_databases?
244
+ puts SequenceServer::Database.all
245
+ exit
246
+ end
247
+
248
+ if list_unformatted_fastas? or make_blast_databases?
249
+ unformatted_fastas = SequenceServer::Database.unformatted_fastas
250
+ if unformatted_fastas.empty?
251
+ puts "All FASTA files in #{SequenceServer[:database_dir]} are formatted."
252
+ exit
253
+ end
254
+ end
255
+
256
+ if list_unformatted_fastas?
257
+ puts unformatted_fastas
258
+ exit
259
+ end
260
+
261
+ if make_blast_databases?
262
+ SequenceServer::Database.make_blast_databases
263
+ exit
264
+ end
265
+
266
+ if set?
267
+ SequenceServer.send :write_config_file
268
+ exit
269
+ end
270
+
271
+ if fetch_option(:set).value
272
+ SequenceServer.send :write_config_file
273
+ end
274
+
275
+ SequenceServer.run
71
276
  end
72
277
  end
73
- rescue OptionParser::InvalidOption =>e
278
+ rescue Slop::Error => e
74
279
  puts e
75
280
  puts "Run '#{$0} -h' for help with command line options."
76
281
  exit
77
282
  end
78
-
79
- # display name for tools like `ps`
80
- $PROGRAM_NAME = 'sequenceserver'
81
-
82
- SequenceServer::App.run!
data/config.ru CHANGED
@@ -1,6 +1,4 @@
1
- require 'rubygems'
2
- require 'bundler/setup'
3
1
  require 'sequenceserver'
4
2
 
5
- SequenceServer::App.init
6
- run SequenceServer::App
3
+ SequenceServer.init
4
+ run SequenceServer
@@ -1,527 +1,373 @@
1
- # sequenceserver.rb
2
-
3
- require 'sinatra/base'
4
1
  require 'yaml'
5
- require 'logger'
6
2
  require 'fileutils'
7
- require 'sequenceserver/helpers'
3
+ require 'sinatra/base'
4
+ require 'thin'
5
+ require 'json'
6
+
7
+ require 'sequenceserver/logger'
8
+ require 'sequenceserver/sequence'
9
+ require 'sequenceserver/database'
8
10
  require 'sequenceserver/blast'
9
- require 'sequenceserver/sequencehelpers'
10
- require 'sequenceserver/sinatralikeloggerformatter'
11
- require 'sequenceserver/customisation'
12
- require 'sequenceserver/version'
13
11
 
14
- # Helper module - initialize the blast server.
15
12
  module SequenceServer
16
- class App < Sinatra::Base
17
- include Helpers
18
- include SequenceHelpers
19
- include SequenceServer::Customisation
20
-
21
- # Basic configuration settings for app.
22
- configure do
23
- # enable some builtin goodies
24
- enable :session, :logging
25
13
 
26
- # main application file
27
- set :app_file, File.expand_path(__FILE__)
14
+ # Use a fixed minimum version of BLAST+
15
+ MINIMUM_BLAST_VERSION = '2.2.30+'
28
16
 
29
- # app root is SequenceServer's installation directory
30
- #
31
- # SequenceServer figures out different settings, location of static
32
- # assets or templates for example, based on app root.
33
- set :root, File.dirname(File.dirname(app_file))
17
+ # Use the following exit codes, or 1.
18
+ EXIT_BLAST_NOT_INSTALLED = 2
19
+ EXIT_BLAST_NOT_COMPATIBLE = 3
20
+ EXIT_NO_BLAST_DATABASE = 4
21
+ EXIT_BLAST_INSTALLATION_FAILED = 5
22
+ EXIT_CONFIG_FILE_NOT_FOUND = 6
23
+ EXIT_NO_SEQUENCE_DIR = 7
34
24
 
35
- # path to test database
36
- #
37
- # SequenceServer ships with test database (fire ant genome) so users can
38
- # launch and preview SequenceServer without any configuration, and/or run
39
- # test suite.
40
- set :test_database, File.join(root, 'tests', 'database')
25
+ class << self
26
+ def environment
27
+ ENV['RACK_ENV']
28
+ end
41
29
 
42
- # path to example configuration file
43
- #
44
- # SequenceServer ships with a dummy configuration file. Users can simply
45
- # copy it and make necessary changes to get started.
46
- set :example_config_file, File.join(root, 'example.config.yml')
30
+ def verbose?
31
+ @verbose ||= (environment == 'development')
32
+ end
47
33
 
48
- # path to SequenceServer's configuration file
49
- #
50
- # The configuration file is a simple, YAML data store.
51
- set :config_file, Proc.new{ File.expand_path('~/.sequenceserver.conf') }
34
+ def root
35
+ File.dirname(File.dirname(__FILE__))
36
+ end
52
37
 
53
- set :log, Proc.new { Logger.new(STDERR) }
54
- log.formatter = SinatraLikeLogFormatter.new()
38
+ def logger
39
+ @logger ||= Logger.new(STDERR, verbose?)
55
40
  end
56
41
 
57
- # Local, app configuration settings derived from config.yml.
58
- #
59
- # A config.yml should contain the settings described in the following
60
- # configure block as key, value pairs. See example.config.yml in the
61
- # installation directory.
62
- configure do
63
- # store the settings hash from config.yml; further configuration values
64
- # are derived from it
65
- set :config, {}
42
+ def init(config = {})
43
+ @config_file = config.delete(:config_file) || '~/.sequenceserver.conf'
44
+ @config_file = File.expand_path(config_file)
45
+ assert_file_present('config file', config_file, EXIT_CONFIG_FILE_NOT_FOUND)
46
+
47
+ @config = {
48
+ :num_threads => 1,
49
+ :port => 4567,
50
+ :host => 'localhost'
51
+ }.update(parse_config_file.merge(config))
52
+
53
+ if @config[:bin]
54
+ @config[:bin] = File.expand_path @config[:bin]
55
+ assert_dir_present 'bin dir', @config[:bin]
56
+ export_bin_dir
57
+ end
66
58
 
67
- # absolute path to the blast binaries
68
- #
69
- # A default of 'nil' is indicative of blast binaries being present in
70
- # system PATH.
71
- set :bin, Proc.new{ File.expand_path(config['bin']) rescue nil }
59
+ assert_blast_installed_and_compatible
72
60
 
73
- # absolute path to the database directory
74
- #
75
- # As a default use 'database' directory relative to current working
76
- # directory of the running app.
77
- set :database, Proc.new{ File.expand_path(config['database']) rescue test_database }
61
+ assert_dir_present 'database dir', @config[:database_dir], EXIT_NO_SEQUENCE_DIR
62
+ @config[:database_dir] = File.expand_path(@config[:database_dir])
63
+ assert_blast_databases_present_in_database_dir
78
64
 
79
- # the port number to run Sequence Server standalone
80
- set :port, Proc.new{ (config['port'] or 4567).to_i }
65
+ Database.scan_databases_dir
81
66
 
82
- # number of threads to be used during blasting
83
- #
84
- # This option is passed directly to BLAST+. We use a default value of 1
85
- # as a higher value may cause BLAST+ to crash if it was not compiled with
86
- # threading support.
87
- set :num_threads, Proc.new{ (config['num_threads'] or 1).to_i }
88
- end
67
+ @config[:num_threads] = Integer(@config[:num_threads])
68
+ assert_num_threads_valid @config[:num_threads]
69
+ logger.debug("Will use #{@config[:num_threads]} threads to run BLAST.")
89
70
 
90
- # Lookup tables used by Sequence Server to pick up the right blast binary,
91
- # or database. These tables should be populated during app initialization
92
- # by scanning bin, and database directories.
93
- configure do
94
- # blast methods (executables) and their corresponding absolute path
95
- set :binaries, {}
71
+ if @config[:require]
72
+ @config[:require] = File.expand_path @config[:require]
73
+ assert_file_present 'extension file', @config[:require]
74
+ require @config[:require]
75
+ end
76
+
77
+ # We don't validate port and host settings. If SequenceServer is run
78
+ # self-hosted, bind will fail on incorrect values. If SequenceServer
79
+ # is run via Apache+Passenger, we don't need to worry.
96
80
 
97
- # list of blast databases indexed by their hash value
98
- set :databases, {}
81
+ self
99
82
  end
100
83
 
101
- configure :development do
102
- log.level = Logger::DEBUG
84
+ attr_reader :config_file, :config
85
+
86
+ def [](key)
87
+ config[key]
103
88
  end
104
89
 
105
- configure(:production) do
106
- log.level = Logger::INFO
107
- error do
108
- erb :'500'
90
+ # Run SequenceServer as a self-hosted server using Thin webserver.
91
+ def run
92
+ url = "http://#{config[:host]}:#{config[:port]}"
93
+ server = Thin::Server.new(config[:host], config[:port], :signals => false) do
94
+ use Rack::CommonLogger
95
+ run SequenceServer
109
96
  end
110
- not_found do
111
- erb :'500'
97
+ server.silent = true
98
+ server.backend.start do
99
+ puts "** SequenceServer is ready."
100
+ puts " Go to #{url} in your browser and start BLASTing!"
101
+ puts " Press CTRL+C to quit."
102
+ [:INT, :TERM].each do |sig|
103
+ trap sig do
104
+ server.stop!
105
+ puts
106
+ puts "** Thank you for using SequenceServer :)."
107
+ puts " Please cite: "
108
+ puts " Priyam, Woodcroft, Rai & Wurm,"
109
+ puts " SequenceServer (in prep)."
110
+ end
111
+ end
112
112
  end
113
+ rescue
114
+ puts "** Oops! There was an error."
115
+ puts " Is SequenceServer already accessible at #{url}?"
116
+ puts " Try running SequenceServer on another port, like so: sequenceserver -p 4570."
113
117
  end
114
118
 
115
- class << self
116
- # Run SequenceServer as a self-hosted server.
117
- #
118
- # By default SequenceServer uses Thin, Mongrel or WEBrick (in that
119
- # order). This can be configured by setting the 'server' option.
120
- def run!(options={})
121
- set options
119
+ # Rack-interface.
120
+ #
121
+ # Inject our logger in the env and dispatch request to our
122
+ # controller.
123
+ def call(env)
124
+ env['rack.logger'] = logger
125
+ App.call(env)
126
+ end
122
127
 
123
- # perform SequenceServer initializations
124
- puts "\n== Initializing SequenceServer..."
125
- init
128
+ # Run SequenceServer interactively.
129
+ def irb
130
+ ARGV.clear
131
+ require 'irb'
132
+ IRB.setup nil
133
+ IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
134
+ require 'irb/ext/multi-irb'
135
+ IRB.irb nil, self
136
+ end
126
137
 
127
- # find out the what server to host SequenceServer with
128
- handler = detect_rack_handler
129
- handler_name = handler.name.gsub(/.*::/, '')
138
+ private
130
139
 
131
- puts
132
- log.info("Using #{handler_name} web server.")
140
+ def parse_config_file
141
+ logger.debug("Reading configuration file: #{config_file}.")
142
+ config = YAML.load_file(config_file) || {}
133
143
 
134
- if handler_name == 'WEBrick'
135
- puts "\n== We recommend using Thin web server for better performance."
136
- puts "== To install Thin: [sudo] gem install thin"
137
- end
144
+ # Symbolize hash keys
145
+ config = config.inject({}){|c, e| c[e.first.to_sym] = e.last; c}
138
146
 
139
- url = "http://#{bind}:#{port}"
140
- puts "\n== Launched SequenceServer at: #{url}"
141
- puts "== Press CTRL + C to quit."
142
- handler.run(self, :Host => bind, :Port => port, :Logger => Logger.new('/dev/null')) do |server|
143
- [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler) } }
144
- set :running, true
147
+ # The newer config file version replaces the older database key with
148
+ # database_dir. If an older version is found, we auto-migrate it to
149
+ # newer one.
150
+ config[:database_dir] ||= config.delete(:database)
151
+ config
152
+ rescue ArgumentError => error
153
+ puts "*** Error in config file: #{error}."
154
+ puts " YAML is white space sensitive. Is your config file properly indented?"
155
+ exit 1
156
+ end
145
157
 
146
- # for Thin
147
- server.silent = true if handler_name == 'Thin'
148
- end
149
- rescue Errno::EADDRINUSE, RuntimeError => e
150
- puts "\n== Failed to start SequenceServer."
151
- puts "== Is SequenceServer already running at: #{url}"
158
+ def write_config_file
159
+ File.open(SequenceServer.config_file, 'w') do |f|
160
+ f.puts(config.delete_if{|k, v| v.nil?}.to_yaml)
152
161
  end
162
+ end
153
163
 
154
- # Stop SequenceServer.
155
- def quit!(server, handler_name)
156
- # Use Thin's hard #stop! if available, otherwise just #stop.
157
- server.respond_to?(:stop!) ? server.stop! : server.stop
158
- puts "\n== Thank you for using SequenceServer :)." +
159
- "\n== Please cite: " +
160
- "\n== Priyam A., Woodcroft B.J., Wurm Y (in prep)." +
161
- "\n== Sequenceserver: BLAST searching made easy." unless handler_name =~/cgi/i
164
+ # Export NCBI BLAST+ bin dir to PATH environment variable.
165
+ def export_bin_dir
166
+ bin_dir = config[:bin]
167
+ if bin_dir
168
+ unless ENV['PATH'].split(':').include? bin_dir
169
+ ENV['PATH'] = "#{bin_dir}:#{ENV['PATH']}"
170
+ end
162
171
  end
172
+ end
163
173
 
164
- # Initializes the blast server : executables, database. Exit if blast
165
- # executables, and databses can not be found. Logs the result if logging
166
- # has been enabled.
167
- def init
168
- # first read the user supplied configuration options
169
- self.config = parse_config
170
-
171
- # empty config file
172
- unless config
173
- log.warn("Empty configuration file: #{config_file} - will assume default settings")
174
- self.config = {}
175
- end
174
+ def assert_file_present desc, file, exit_code = 1
175
+ unless file and File.exists? File.expand_path file
176
+ puts "*** Couldn't find #{desc}: #{file}."
177
+ exit exit_code
178
+ end
179
+ end
176
180
 
177
- # scan for blast binaries
178
- self.binaries = scan_blast_executables(bin).freeze
181
+ alias assert_dir_present assert_file_present
179
182
 
180
- # Log the discovery of binaries.
181
- binaries.each do |command, path|
182
- log.info("Found #{command} at #{path}")
183
- end
184
-
185
- # scan for blast database
186
- self.databases = scan_blast_db(database, binaries['blastdbcmd']).freeze
183
+ def assert_blast_installed_and_compatible
184
+ unless command? 'blastdbcmd'
185
+ puts "*** Could not find BLAST+ binaries."
186
+ exit EXIT_BLAST_NOT_INSTALLED
187
+ end
188
+ version = %x|blastdbcmd -version|.split[1]
189
+ unless version >= MINIMUM_BLAST_VERSION
190
+ puts "*** Your BLAST+ version #{version} is outdated."
191
+ puts " SequenceServer needs NCBI BLAST+ version #{MINIMUM_BLAST_VERSION} or higher."
192
+ exit EXIT_BLAST_NOT_COMPATIBLE
193
+ end
194
+ end
187
195
 
188
- # Log the discovery of databases.
189
- databases.each do |id, database|
190
- log.info("Found #{database.type} database: #{database.title} at #{database.name}")
196
+ def assert_blast_databases_present_in_database_dir
197
+ database_dir = config[:database_dir]
198
+ out = %x|blastdbcmd -recursive -list #{database_dir}|
199
+ if out.empty?
200
+ puts "*** Could not find BLAST databases in '#{database_dir}'."
201
+ exit EXIT_NO_BLAST_DATABASE
202
+ elsif out.match(/BLAST Database error/) or not $?.success?
203
+ puts "*** Error obtaining BLAST databases."
204
+ puts " Tried: #{find_dbs_command}"
205
+ puts " Error:"
206
+ out.strip.split("\n").each do |l|
207
+ puts " #{l}"
191
208
  end
192
- rescue IOError => error
193
- log.fatal("Fail: #{error}")
194
- exit
195
- rescue ArgumentError => error
196
- log.fatal("Error in config.yml: #{error}")
197
- puts "YAML is white space sensitive. Is your config.yml properly indented?"
198
- exit
199
- rescue Errno::ENOENT # config file not found
200
- log.info('Configuration file not found')
201
- FileUtils.cp(example_config_file, config_file)
202
- log.info("Generated a dummy configuration file: #{config_file}")
203
- puts "\nPlease edit #{config_file} to indicate the location of your BLAST databases and run SequenceServer again."
204
- exit
209
+ puts " Please could you report this to 'https://groups.google.com/forum/#!forum/sequenceserver'?"
210
+ exit EXIT_BLAST_DATABASE_ERROR
205
211
  end
212
+ end
206
213
 
207
- # Parse config.yml, and return the resulting hash.
208
- #
209
- # This method uses YAML.load_file to read config.yml. Absence of a
210
- # config.yml is safely ignored as the app should then fall back on
211
- # default configuration values. Any other error raised by YAML.load_file
212
- # is not rescued.
213
- def parse_config
214
- YAML.load_file( config_file )
214
+ def assert_num_threads_valid num_threads
215
+ unless num_threads > 0
216
+ puts "*** Can't use #{num_threads} number of threads."
217
+ puts " Number of threads should be greater than or equal to 1."
218
+ exit 1
219
+ end
220
+ if num_threads > 256
221
+ logger.warn "*** Number of threads set at #{num_threads} is unusually high."
215
222
  end
223
+ rescue
224
+ puts "*** Number of threads should be a number."
225
+ exit 1
216
226
  end
217
227
 
218
- get '/' do
219
- erb :search
228
+
229
+ # Return `true` if the given command exists and is executable.
230
+ def command?(command)
231
+ system("which #{command} > /dev/null 2>&1")
220
232
  end
221
233
 
222
- before '/' do
223
- pass if params.empty?
234
+ end
224
235
 
225
- # ensure required params present
226
- #
227
- # If a required parameter is missing, SequnceServer returns 'Bad Request
228
- # (400)' error.
229
- #
230
- # See Twitter's [Error Codes & Responses][1] page for reference.
231
- #
232
- # [1]: https://dev.twitter.com/docs/error-codes-responses
236
+ # Controller.
237
+ class App < Sinatra::Base
233
238
 
234
- if params[:method].nil? or params[:method].empty?
235
- halt 400, "No BLAST method provided."
236
- end
239
+ # See
240
+ # http://www.sinatrarb.com/configuration.html
241
+ configure do
242
+ # We don't need Rack::MethodOverride. Let's avoid the overhead.
243
+ disable :method_override
244
+
245
+ # Ensure exceptions never leak out of the app. Exceptions raised within
246
+ # the app must be handled by the app. We do this by attaching error
247
+ # blocks to exceptions we know how to handle and attaching to Exception
248
+ # as fallback.
249
+ disable :show_exceptions, :raise_errors
250
+
251
+ # Make it a policy to dump to 'rack.errors' any exception raised by the
252
+ # app so that error handlers don't have to do it themselves. But for it
253
+ # to always work, Exceptions defined by us should not respond to `code`
254
+ # or http_status` methods. Error blocks errors must explicitly set http
255
+ # status, if needed, by calling `status` method.
256
+ # method.
257
+ enable :dump_errors
258
+
259
+ # We don't want Sinatra do setup any loggers for us. We will use our own.
260
+ set :logging, nil
261
+
262
+ # Public, and views directory will be found here.
263
+ set :root, lambda { SequenceServer.root }
264
+ end
237
265
 
238
- if params[:sequence].nil? or params[:sequence].empty?
239
- halt 400, "No input sequence provided."
240
- end
266
+ # See
267
+ # http://www.sinatrarb.com/intro.html#Mime%20Types
268
+ configure do
269
+ mime_type :fasta, 'text/fasta'
270
+ end
241
271
 
242
- if params[:databases].nil?
243
- halt 400, "No BLAST database provided."
272
+ helpers do
273
+ # Render an anchor element from the given Hash.
274
+ #
275
+ # See links.rb for example of a Hash object that will be rendered.
276
+ def a(link)
277
+ return unless link[:title] and link[:url]
278
+ _a = ["<a"]
279
+ _a << "href=#{link[:url]}"
280
+ _a << "class=\"#{link[:class]}\"" if link[:class]
281
+ _a << "target=\"_blank\"" if absolute? link[:url]
282
+ _a << '>'
283
+ _a << "<i class=\"fa #{link[:icon]}\"></i>" if link[:icon]
284
+ _a << link[:title]
285
+ _a << "</a>"
286
+ _a.join("\n")
244
287
  end
245
288
 
246
- # ensure params are valid #
247
-
248
- # only allowed blast methods should be used
249
- blast_methods = %w|blastn blastp blastx tblastn tblastx|
250
- unless blast_methods.include?(params[:method])
251
- halt 400, "Unknown BLAST method: #{params[:method]}."
289
+ # Is the given URI absolute? (or relative?)
290
+ def absolute?(uri)
291
+ URI.parse(uri).absolute?
252
292
  end
253
293
 
254
- # check the advanced options are sensible
255
- begin #FIXME
256
- validate_advanced_parameters(params[:advanced])
257
- rescue ArgumentError => error
258
- halt 400, "Advanced parameters invalid: #{error}"
294
+ # Formats score (a float) to two decimal places.
295
+ def prettify_score(score)
296
+ '%.2f' % score
259
297
  end
260
298
 
261
- # log params
262
- settings.log.debug('method: ' + params[:method])
263
- settings.log.debug('sequence: ' + params[:sequence])
264
- settings.log.debug('database: ' + params[:databases].inspect)
265
- settings.log.debug('advanced: ' + params[:advanced])
266
- end
267
-
268
- post '/' do
269
- method = params['method']
270
- databases = params[:databases]
271
- sequence = params[:sequence]
272
- advanced_opts = params['advanced']
273
-
274
- # evaluate empty sequence as nil, otherwise as fasta
275
- sequence = sequence.empty? ? nil : to_fasta(sequence)
276
-
277
- # blastn implies blastn, not megablast; but let's not interfere if a user
278
- # specifies `task` herself
279
- if method == 'blastn' and not advanced_opts =~ /task/
280
- advanced_opts << ' -task blastn '
299
+ # Formats evalue (a float expressed in scientific notation) to "a x b^c".
300
+ def prettify_evalue(evalue)
301
+ evalue.to_s.sub(/(\d*\.\d*)e?([+-]\d*)?/) do
302
+ s = '%.3f' % Regexp.last_match[1]
303
+ s << " &times; 10<sup>#{Regexp.last_match[2]}</sup>" if Regexp.last_match[2]
304
+ s
305
+ end
281
306
  end
307
+ end
282
308
 
283
- method = settings.binaries[ method ]
284
- databases = params[:databases].map{|index|
285
- settings.databases[index].name
286
- }
287
- advanced_opts << " -num_threads #{settings.num_threads}"
288
-
289
- # run blast and log
290
- blast = Blast.new(method, sequence, databases.join(' '), advanced_opts)
291
- blast.run!
292
- settings.log.info('Ran: ' + blast.command)
309
+ # For any request that hits the app in development mode, log incoming
310
+ # params.
311
+ before do
312
+ logger.debug params
313
+ end
293
314
 
294
- unless blast.success?
295
- halt *blast.error
296
- end
315
+ # Render the search form.
316
+ get '/' do
317
+ erb :search, :locals => {:databases => Database.group_by(&:type)}
318
+ end
297
319
 
298
- format_blast_results(blast.result, databases)
320
+ # BLAST search!
321
+ post '/' do
322
+ erb :result, :locals => {:report => BLAST.run(params)}
299
323
  end
300
324
 
301
- # get '/get_sequence/?id=sequence_ids&db=retreival_databases'
325
+ # get '/get_sequence/?sequence_ids=sequence_ids&database_ids=retreival_databases[&download=fasta]'
302
326
  #
303
327
  # Use whitespace to separate entries in sequence_ids (all other chars exist
304
328
  # in identifiers) and retreival_databases (we don't allow whitespace in a
305
329
  # database's name, so it's safe).
306
330
  get '/get_sequence/' do
307
- sequenceids = params[:id].split(/\s/).uniq # in a multi-blast
308
- # query some may have been found multiply
309
- retrieval_databases = params[:db].split(/\s/)
310
-
311
- settings.log.info("Looking for: '#{sequenceids.join(', ')}' in '#{retrieval_databases.join(', ')}'")
312
-
313
- # the results do not indicate which database a hit is from.
314
- # Thus if several databases were used for blasting, we must check them all
315
- # if it works, refactor with "inject" or "collect"?
316
- found_sequences = ''
317
-
318
- retrieval_databases.each do |database| # we need to populate this session variable from the erb.
319
- sequence = sequence_from_blastdb(sequenceids, database)
320
- if sequence.empty?
321
- settings.log.debug("'#{sequenceids.join(', ')}' not found in #{database}")
322
- else
323
- found_sequences += sequence
324
- end
325
- end
326
-
327
- found_sequences_count = found_sequences.count('>')
328
-
329
- out = ''
330
- # just in case, checking we found right number of sequences
331
- if found_sequences_count != sequenceids.length
332
- out <<<<HEADER
333
- <h1>ERROR: incorrect number of sequences found.</h1>
334
- <p>Dear user,</p>
331
+ sequence_ids = params[:sequence_ids].split(/\s/)
332
+ database_ids = params[:database_ids].split(/\s/)
335
333
 
336
- <p><strong>We have found
337
- <em>#{found_sequences_count > sequenceids.length ? 'more' : 'less'}</em>
338
- sequence than expected.</strong></p>
334
+ sequences = Sequence.from_blastdb(sequence_ids, database_ids)
339
335
 
340
- <p>This is likely due to a problem with how databases are formatted.
341
- <strong>Please share this text with the person managing this website so
342
- they can resolve the issue.</strong></p>
343
-
344
- <p> You requested #{sequenceids.length} sequence#{sequenceids.length > 1 ? 's' : ''}
345
- with the following identifiers: <code>#{sequenceids.join(', ')}</code>,
346
- from the following databases: <code>#{retrieval_databases.join(', ')}</code>.
347
- But we found #{found_sequences_count} sequence#{found_sequences_count> 1 ? 's' : ''}.
348
- </p>
349
-
350
- <p>If sequences were retrieved, you can find them below (but some may be incorrect, so be careful!).</p>
351
- <hr/>
352
- HEADER
353
- end
354
-
355
- out << "<pre><code>#{found_sequences}</pre></code>"
356
- out
357
- end
358
-
359
- # Ensure a '>sequence_identifier\n' at the start of a sequence.
360
- #
361
- # An empty query line appears in the blast report if the leading
362
- # '>sequence_identifier\n' in the sequence is missing. We prepend
363
- # the input sequence with user info in such case.
364
- #
365
- # > to_fasta("acgt")
366
- # => 'Submitted_By_127.0.0.1_at_110214-15:33:34\nacgt'
367
- def to_fasta(sequence)
368
- sequence.lstrip!
369
- if sequence[0,1] != '>'
370
- ip = request.ip.to_s
371
- time = Time.now.strftime("%y%m%d-%H:%M:%S")
372
- sequence.insert(0, ">Submitted_By_#{ip}_at_#{time}\n")
373
- end
374
- return sequence
375
- end
376
-
377
- def format_blast_results(result, databases)
378
- # Constructing the result in an Array and then calling Array#join is much faster than
379
- # building up a String and using +=, as older versions of SeqServ did.
380
- formatted_results = []
381
-
382
- @all_retrievable_ids = []
383
- string_of_used_databases = databases.join(' ')
384
- blast_database_number = 0
385
- line_number = 0
386
- started_query = false
387
- finished_database_summary = false
388
- finished_alignments = false
389
- reference_string = ''
390
- database_summary_string = ''
391
- result.each do |line|
392
- line_number += 1
393
- next if line_number <= 5 #skip the first 5 lines
394
-
395
- # Add the reference to the end, not the start, of the blast result
396
- if line_number >= 7 and line_number <= 15
397
- reference_string += line
398
- next
399
- end
400
-
401
- if !finished_database_summary and line_number > 15
402
- database_summary_string += line
403
- finished_database_summary = true if line.match(/total letters/)
404
- next
405
- end
406
-
407
- # Remove certain lines from the output
408
- skipped_lines = [/^<\/BODY>/,/^<\/HTML>/,/^<\/PRE>/]
409
- skip = false
410
- skipped_lines.each do |skippy|
411
- # $stderr.puts "`#{line}' matches #{skippy}?"
412
- if skippy.match(line)
413
- skip = true
414
- # $stderr.puts 'yes'
415
- else
416
- # $stderr.puts 'no'
417
- end
336
+ if params[:download]
337
+ file_name = "sequenceserver_#{sequence_ids.first}.fasta"
338
+ file = Tempfile.new file_name
339
+ sequences.each do |sequence|
340
+ file.puts sequence.fasta
418
341
  end
419
- next if skip
420
-
421
- # Remove the javascript inclusion
422
- line.gsub!(/^<script src=\"blastResult.js\"><\/script>/, '')
423
-
424
- if line.match(/^>/) # If line to possibly replace
425
- # Reposition the anchor to the end of the line, so that it both still works and
426
- # doesn't interfere with the diagnostic space at the beginning of the line.
427
- #
428
- # There are two cases:
429
- #
430
- # database formatted _with_ -parse_seqids
431
- line.gsub!(/^>(.+)(<a.*><\/a>)(.*)/, '>\1\3\2')
432
- #
433
- # database formatted _without_ -parse_seqids
434
- line.gsub!(/^>(<a.*><\/a>)(.*)/, '>\2\1')
435
-
436
- # get hit coordinates -- useful for linking to genome browsers
437
- hit_length = result[line_number..-1].index{|l| l =~ />lcl|Lambda/}
438
- hit_coordinates = result[line_number, hit_length].grep(/Sbjct/).
439
- map(&:split).map{|l| [l[1], l[-1]]}.flatten.map(&:to_i).minmax
440
-
441
- # Create the hyperlink (if required)
442
- formatted_results << construct_sequence_hyperlink_line(line, databases, hit_coordinates)
443
- else
444
- # Surround each query's result in <div> tags so they can be coloured by CSS
445
- if matches = line.match(/^<b>Query=<\/b> (.*)/) # If starting a new query, then surround in new <div> tag, and finish the last one off
446
- line = "<div class=\"resultn\" id=\"#{matches[1]}\">\n<h3>Query= #{matches[1]}</h3><pre>"
447
- unless blast_database_number == 0
448
- line = "</pre></div>\n#{line}"
449
- end
450
- blast_database_number += 1
451
- elsif line.match(/^ Database: /) and !finished_alignments
452
- formatted_results << "</div>\n<pre>#{database_summary_string}\n\n"
453
- finished_alignments = true
454
- end
455
- formatted_results << line
456
- end
457
- end
458
- formatted_results << "</pre>"
459
-
460
- link_to_fasta_of_all = "/get_sequence/?id=#{@all_retrievable_ids.join(' ')}&db=#{string_of_used_databases}"
461
- # #dbs must be sep by ' '
462
- retrieval_text = @all_retrievable_ids.empty? ? '' : "<a href='#{url(link_to_fasta_of_all)}'>FASTA of #{@all_retrievable_ids.length} retrievable hit(s)</a>"
463
-
464
- "<h2>Results</h2>"+
465
- retrieval_text +
466
- "<br/><br/>" +
467
- formatted_results.join +
468
- "<br/>" +
469
- "<pre>#{reference_string.strip}</pre>"
470
- end
471
-
472
- def construct_sequence_hyperlink_line(line, databases, hit_coordinates)
473
- matches = line.match(/^>(.+)/)
474
- sequence_id = matches[1]
475
-
476
- link = nil
477
-
478
- # If a custom sequence hyperlink method has been defined,
479
- # use that.
480
- options = {
481
- :sequence_id => sequence_id,
482
- :databases => databases,
483
- :hit_coordinates => hit_coordinates
484
- }
485
-
486
- # First precedence: construct the whole line to be customised
487
- if self.respond_to?(:construct_custom_sequence_hyperlinking_line)
488
- settings.log.debug("Using custom hyperlinking line creator with sequence #{options.inspect}")
489
- link_line = construct_custom_sequence_hyperlinking_line(options)
490
- unless link_line.nil?
491
- return link_line
492
- end
493
- end
494
-
495
- # If we have reached here, custom construction of the
496
- # whole line either wasn't defined, or returned nil
497
- # (indicating failure)
498
- if self.respond_to?(:construct_custom_sequence_hyperlink)
499
- settings.log.debug("Using custom hyperlink creator with sequence #{options.inspect}")
500
- link = construct_custom_sequence_hyperlink(options)
501
- else
502
- settings.log.debug("Using standard hyperlink creator with sequence `#{options.inspect}'")
503
- link = construct_standard_sequence_hyperlink(options)
504
- end
505
-
506
- # Return the BLAST output line with the link in it
507
- if link.nil?
508
- settings.log.debug('No link added link for: `'+ sequence_id +'\'')
509
- return line
342
+ file.close
343
+ send_file file.path, :type => :fasta, :filename => file_name
510
344
  else
511
- settings.log.debug('Added link for: `'+ sequence_id +'\''+ link)
512
- return "><a href='#{url(link)}' target='_blank'>#{sequence_id}</a> \n"
345
+ {
346
+ :sequence_ids => sequence_ids,
347
+ :databases => Database[database_ids].map(&:title),
348
+ :sequences => sequences.map {|s| s.info}
349
+ }.to_json
513
350
  end
351
+ end
514
352
 
353
+ # This error block will only ever be hit if the user gives us a funny
354
+ # sequence or incorrect advanced parameter. Well, we could hit this block
355
+ # if someone is playing around with our HTTP API too.
356
+ error BLAST::ArgumentError do
357
+ status 400
358
+ error = env['sinatra.error']
359
+ erb :'400', :locals => {:error => error}
515
360
  end
516
361
 
517
- # Advanced options are specified by the user. Here they are checked for interference with SequenceServer operations.
518
- # raise ArgumentError if an error has occurred, otherwise return without value
519
- def validate_advanced_parameters(advanced_options)
520
- raise ArgumentError, "Invalid characters detected in the advanced options" unless advanced_options =~ /\A[a-z0-9\-_\. ']*\Z/i
521
- disallowed_options = %w(-out -html -outfmt -db -query)
522
- disallowed_options.each do |o|
523
- raise ArgumentError, "The advanced BLAST option \"#{o}\" is used internally by SequenceServer and so cannot be specified by the you" if advanced_options =~ /#{o}/i
524
- end
362
+ # This will catch any unhandled error and some very special errors. Ideally
363
+ # we will never hit this block. If we do, there's a bug in SequenceServer
364
+ # or something really weird going on. If we hit this error block we show
365
+ # the stacktrace to the user requesting them to post the same to our Google
366
+ # Group.
367
+ error Exception, BLAST::RuntimeError do
368
+ status 500
369
+ error = env['sinatra.error']
370
+ erb :'500', :locals => {:error => error}
525
371
  end
526
372
  end
527
373
  end