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.

Potentially problematic release.


This version of sequenceserver might be problematic. Click here for more details.

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