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.
- checksums.yaml +4 -4
- data/{README.txt → README.md} +2 -0
- data/bin/sequenceserver +255 -55
- data/config.ru +2 -4
- data/lib/sequenceserver.rb +293 -447
- data/lib/sequenceserver/blast.rb +464 -64
- data/lib/sequenceserver/database.rb +185 -19
- data/lib/sequenceserver/links.rb +114 -0
- data/lib/sequenceserver/logger.rb +27 -0
- data/lib/sequenceserver/sequence.rb +141 -0
- data/public/css/bootstrap.min.css +8 -413
- data/public/css/custom.css +363 -122
- data/public/css/font-awesome.min.css +4 -0
- data/public/fonts/FontAwesome.otf +0 -0
- data/public/fonts/fontawesome-webfont.eot +0 -0
- data/public/fonts/fontawesome-webfont.svg +565 -0
- data/public/fonts/fontawesome-webfont.ttf +0 -0
- data/public/fonts/fontawesome-webfont.woff +0 -0
- data/public/fonts/fontawesome-webfont.woff2 +0 -0
- data/public/js/bootstrap.min.js +11 -0
- data/public/js/d3.v3.min.js +5 -0
- data/public/js/html5shiv.min.js +4 -0
- data/public/js/jquery.scrollspy.js +74 -0
- data/public/js/jquery.t.js +353 -0
- data/public/js/sequence.js +2419 -0
- data/public/js/sequenceserver.blast.js +29 -30
- data/public/js/sequenceserver.js +544 -120
- data/public/js/underscore.min.js +6 -0
- data/public/js/webshims/polyfiller.js +1 -0
- data/public/js/webshims/shims/FlashCanvas/canvas2png.js +1 -0
- data/public/js/webshims/shims/FlashCanvas/flashcanvas.js +1 -0
- data/public/js/webshims/shims/FlashCanvas/flashcanvas.swf +0 -0
- data/public/js/webshims/shims/FlashCanvasPro/canvas2png.js +1 -0
- data/public/js/webshims/shims/FlashCanvasPro/flash10canvas.swf +0 -0
- data/public/js/webshims/shims/FlashCanvasPro/flash9canvas.swf +0 -0
- data/public/js/webshims/shims/FlashCanvasPro/flashcanvas.js +1 -0
- data/public/js/webshims/shims/canvas-blob.js +1 -0
- data/public/js/webshims/shims/color-picker.js +2 -0
- data/public/js/webshims/shims/combos/1.js +6 -0
- data/public/js/webshims/shims/combos/10.js +2 -0
- data/public/js/webshims/shims/combos/11.js +2 -0
- data/public/js/webshims/shims/combos/12.js +6 -0
- data/public/js/webshims/shims/combos/13.js +1 -0
- data/public/js/webshims/shims/combos/14.js +1 -0
- data/public/js/webshims/shims/combos/15.js +2 -0
- data/public/js/webshims/shims/combos/16.js +7 -0
- data/public/js/webshims/shims/combos/17.js +2 -0
- data/public/js/webshims/shims/combos/18.js +3 -0
- data/public/js/webshims/shims/combos/2.js +7 -0
- data/public/js/webshims/shims/combos/21.js +2 -0
- data/public/js/webshims/shims/combos/22.js +1 -0
- data/public/js/webshims/shims/combos/23.js +6 -0
- data/public/js/webshims/shims/combos/25.js +2 -0
- data/public/js/webshims/shims/combos/27.js +1 -0
- data/public/js/webshims/shims/combos/28.js +1 -0
- data/public/js/webshims/shims/combos/29.js +1 -0
- data/public/js/webshims/shims/combos/3.js +1 -0
- data/public/js/webshims/shims/combos/30.js +2 -0
- data/public/js/webshims/shims/combos/31.js +1 -0
- data/public/js/webshims/shims/combos/33.js +1 -0
- data/public/js/webshims/shims/combos/34.js +1 -0
- data/public/js/webshims/shims/combos/4.js +1 -0
- data/public/js/webshims/shims/combos/5.js +2 -0
- data/public/js/webshims/shims/combos/6.js +2 -0
- data/public/js/webshims/shims/combos/7.js +7 -0
- data/public/js/webshims/shims/combos/8.js +7 -0
- data/public/js/webshims/shims/combos/9.js +2 -0
- data/public/js/webshims/shims/combos/97.js +1 -0
- data/public/js/webshims/shims/combos/98.js +1 -0
- data/public/js/webshims/shims/combos/99.js +1 -0
- data/public/js/webshims/shims/details.js +1 -0
- data/public/js/webshims/shims/dom-extend.js +1 -0
- data/public/js/webshims/shims/es5.js +1 -0
- data/public/js/webshims/shims/es6.js +1 -0
- data/public/js/webshims/shims/excanvas.js +1 -0
- data/public/js/webshims/shims/filereader-xhr.js +1 -0
- data/public/js/webshims/shims/form-combat.js +1 -0
- data/public/js/webshims/shims/form-core.js +1 -0
- data/public/js/webshims/shims/form-datalist-lazy.js +1 -0
- data/public/js/webshims/shims/form-datalist.js +1 -0
- data/public/js/webshims/shims/form-fixrangechange.js +1 -0
- data/public/js/webshims/shims/form-inputmode.js +1 -0
- data/public/js/webshims/shims/form-message.js +1 -0
- data/public/js/webshims/shims/form-native-extend.js +1 -0
- data/public/js/webshims/shims/form-number-date-api.js +1 -0
- data/public/js/webshims/shims/form-number-date-ui.js +1 -0
- data/public/js/webshims/shims/form-shim-extend.js +1 -0
- data/public/js/webshims/shims/form-shim-extend2.js +1 -0
- data/public/js/webshims/shims/form-validation.js +1 -0
- data/public/js/webshims/shims/form-validators.js +1 -0
- data/public/js/webshims/shims/forms-picker.js +1 -0
- data/public/js/webshims/shims/geolocation.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-ar.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-ch-CN.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-cs.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-de.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-el.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-en.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-es.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-fa.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-fr.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-he.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-hi.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-hu.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-it.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-ja.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-lt.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-nl.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-pl.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-pt-BR.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-pt-PT.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-pt.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-ru.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-sv.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-zh-CN.js +1 -0
- data/public/js/webshims/shims/i18n/formcfg-zh-TW.js +1 -0
- data/public/js/webshims/shims/jme/alternate-media.js +1 -0
- data/public/js/webshims/shims/jme/base.js +1 -0
- data/public/js/webshims/shims/jme/controls.css +1 -0
- data/public/js/webshims/shims/jme/jme.eot +0 -0
- data/public/js/webshims/shims/jme/jme.svg +36 -0
- data/public/js/webshims/shims/jme/jme.ttf +0 -0
- data/public/js/webshims/shims/jme/jme.woff +0 -0
- data/public/js/webshims/shims/jme/mediacontrols-lazy.js +1 -0
- data/public/js/webshims/shims/jme/mediacontrols.js +1 -0
- data/public/js/webshims/shims/jme/playlist.js +1 -0
- data/public/js/webshims/shims/jpicker/images/AlphaBar.png +0 -0
- data/public/js/webshims/shims/jpicker/images/Bars.png +0 -0
- data/public/js/webshims/shims/jpicker/images/Maps.png +0 -0
- data/public/js/webshims/shims/jpicker/images/NoColor.png +0 -0
- data/public/js/webshims/shims/jpicker/images/bar-opacity.png +0 -0
- data/public/js/webshims/shims/jpicker/images/map-opacity.png +0 -0
- data/public/js/webshims/shims/jpicker/images/mappoint.gif +0 -0
- data/public/js/webshims/shims/jpicker/images/picker.gif +0 -0
- data/public/js/webshims/shims/jpicker/images/preview-opacity.png +0 -0
- data/public/js/webshims/shims/jpicker/images/rangearrows.gif +0 -0
- data/public/js/webshims/shims/jpicker/jpicker.css +1 -0
- data/public/js/webshims/shims/matchMedia.js +3 -0
- data/public/js/webshims/shims/mediacapture-picker.js +1 -0
- data/public/js/webshims/shims/mediacapture.js +1 -0
- data/public/js/webshims/shims/mediaelement-core.js +1 -0
- data/public/js/webshims/shims/mediaelement-debug.js +1 -0
- data/public/js/webshims/shims/mediaelement-jaris.js +1 -0
- data/public/js/webshims/shims/mediaelement-native-fix.js +1 -0
- data/public/js/webshims/shims/mediaelement-yt.js +1 -0
- data/public/js/webshims/shims/moxie/flash/Moxie.cdn.swf +0 -0
- data/public/js/webshims/shims/moxie/flash/Moxie.min.swf +0 -0
- data/public/js/webshims/shims/moxie/js/moxie-html4.js +3 -0
- data/public/js/webshims/shims/moxie/js/moxie-swf.js +2 -0
- data/public/js/webshims/shims/picture.js +1 -0
- data/public/js/webshims/shims/plugins/jquery.ui.position.js +11 -0
- data/public/js/webshims/shims/range-ui.js +1 -0
- data/public/js/webshims/shims/sizzle.js +11 -0
- data/public/js/webshims/shims/sticky.js +1 -0
- data/public/js/webshims/shims/styles/color-picker.png +0 -0
- data/public/js/webshims/shims/styles/forms-ext.css +1 -0
- data/public/js/webshims/shims/styles/forms-picker.css +1 -0
- data/public/js/webshims/shims/styles/progress.gif +0 -0
- data/public/js/webshims/shims/styles/progress.png +0 -0
- data/public/js/webshims/shims/styles/shim-ext.css +1 -0
- data/public/js/webshims/shims/styles/shim.css +1 -0
- data/public/js/webshims/shims/styles/transparent.png +0 -0
- data/public/js/webshims/shims/styles/widget.eot +0 -0
- data/public/js/webshims/shims/styles/widget.svg +12 -0
- data/public/js/webshims/shims/styles/widget.ttf +0 -0
- data/public/js/webshims/shims/styles/widget.woff +0 -0
- data/public/js/webshims/shims/swf/JarisFLVPlayer.swf +0 -0
- data/public/js/webshims/shims/swfmini-embed.js +1 -0
- data/public/js/webshims/shims/swfmini.js +6 -0
- data/public/js/webshims/shims/track-ui.js +1 -0
- data/public/js/webshims/shims/track.js +1 -0
- data/public/js/webshims/shims/url.js +1 -0
- data/public/js/webshims/shims/usermedia-core.js +1 -0
- data/public/js/webshims/shims/usermedia-shim.js +1 -0
- data/sequenceserver.gemspec +16 -13
- data/views/400.erb +28 -0
- data/views/500.erb +35 -19
- data/views/_options.erb +6 -15
- data/views/result.erb +218 -0
- data/views/search.erb +354 -151
- metadata +254 -62
- data/example.config.yml +0 -39
- data/lib/sequenceserver/customisation.rb +0 -60
- data/lib/sequenceserver/database_formatter.rb +0 -190
- data/lib/sequenceserver/helpers.rb +0 -136
- data/lib/sequenceserver/sequencehelpers.rb +0 -93
- data/lib/sequenceserver/sinatralikeloggerformatter.rb +0 -12
- data/lib/sequenceserver/version.rb +0 -9
- data/public/css/beige.css.css +0 -254
- data/public/css/bootstrap.dropdown.css +0 -29
- data/public/css/bootstrap.icons.css +0 -155
- data/public/css/bootstrap.modal.css +0 -28
- data/public/js/bootstrap.dropdown.js +0 -92
- data/public/js/bootstrap.modal.js +0 -7
- data/public/js/bootstrap.transition.js +0 -7
- data/public/js/jquery-scrollspy.js +0 -98
- data/public/js/jquery.activity.js +0 -10
- data/public/js/jquery.enablePlaceholder.min.js +0 -10
- data/public/js/store.min.js +0 -2
- data/public/sequence.html +0 -28
- data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta +0 -5486
- data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nhr +0 -0
- data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nin +0 -0
- data/tests/database/nucleotide/Sinvicta2-2-3.cdna.subset.fasta.nsq +0 -0
- data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta +0 -6449
- data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.phr +0 -0
- data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.pin +0 -0
- data/tests/database/protein/Sinvicta2-2-3.prot.subset.fasta.psq +0 -0
- data/tests/run +0 -26
- data/tests/test_sequencehelpers.rb +0 -77
- data/tests/test_sequenceserver_blast.rb +0 -60
- data/tests/test_ui.rb +0 -104
- data/tests/ui.specs.todo +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b4b6422d0e86b47690118cf32a0633609fd8e545
|
4
|
+
data.tar.gz: 01dda58ef4ab7bac5c889508361be848482cfc75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c469b959e1fa91763766a7584b2b8ba6950c057d68319cb5ffc7e7dffe8b903bdff88037264ff9a6e71a6b2914a8c733e061b0c065c6934af6f41e633f01e889
|
7
|
+
data.tar.gz: 6cc6dcf46bbf6e4d93437e284102083de62fb30e35e18f0a6f63461dfbaa27f1fe9e7a019b2bcab48ca84d67b787598dc7fde277dcb9ffaad9c507f64e76b89c
|
data/{README.txt → README.md}
RENAMED
@@ -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
|
-
|
4
|
-
require 'bundler/setup'
|
5
|
-
require 'optparse'
|
6
|
-
require 'sequenceserver'
|
5
|
+
ENV['RACK_ENV'] ||= 'production'
|
7
6
|
|
8
|
-
#
|
9
|
-
|
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
|
-
|
15
|
+
custom, local, BLAST server
|
18
16
|
|
19
17
|
USAGE
|
20
18
|
|
21
|
-
|
19
|
+
sequenceserver [options]
|
22
20
|
|
23
|
-
|
21
|
+
Example:
|
24
22
|
|
25
|
-
|
26
|
-
|
23
|
+
# launch SequenceServer with the given config file
|
24
|
+
$ sequenceserver -c ~/.sequenceserver.ants.conf
|
27
25
|
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
35
|
+
If BLAST+ is not installed on your system, SequenceServer will offer to install
|
36
|
+
BLAST+ for you.
|
38
37
|
|
39
|
-
|
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
|
-
|
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
|
-
|
48
|
+
on 'c', 'config_file=',
|
49
|
+
'Use the given configuration file',
|
50
|
+
:argument => true
|
45
51
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
end
|
52
|
+
on 'b', 'bin=',
|
53
|
+
'Load BLAST+ binaries from this directory',
|
54
|
+
:argument => true
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
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
data/lib/sequenceserver.rb
CHANGED
@@ -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 '
|
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
|
-
|
27
|
-
|
14
|
+
# Use a fixed minimum version of BLAST+
|
15
|
+
MINIMUM_BLAST_VERSION = '2.2.30+'
|
28
16
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
54
|
-
|
38
|
+
def logger
|
39
|
+
@logger ||= Logger.new(STDERR, verbose?)
|
55
40
|
end
|
56
41
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
80
|
-
set :port, Proc.new{ (config['port'] or 4567).to_i }
|
65
|
+
Database.scan_databases_dir
|
81
66
|
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
98
|
-
set :databases, {}
|
81
|
+
self
|
99
82
|
end
|
100
83
|
|
101
|
-
|
102
|
-
|
84
|
+
attr_reader :config_file, :config
|
85
|
+
|
86
|
+
def [](key)
|
87
|
+
config[key]
|
103
88
|
end
|
104
89
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
111
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
128
|
-
handler = detect_rack_handler
|
129
|
-
handler_name = handler.name.gsub(/.*::/, '')
|
138
|
+
private
|
130
139
|
|
131
|
-
|
132
|
-
|
140
|
+
def parse_config_file
|
141
|
+
logger.debug("Reading configuration file: #{config_file}.")
|
142
|
+
config = YAML.load_file(config_file) || {}
|
133
143
|
|
134
|
-
|
135
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
-
|
178
|
-
self.binaries = scan_blast_executables(bin).freeze
|
181
|
+
alias assert_dir_present assert_file_present
|
179
182
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
193
|
-
|
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
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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
|
-
|
219
|
-
|
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
|
-
|
223
|
-
pass if params.empty?
|
234
|
+
end
|
224
235
|
|
225
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
243
|
-
|
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
|
-
#
|
247
|
-
|
248
|
-
|
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
|
-
#
|
255
|
-
|
256
|
-
|
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
|
-
#
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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 << " × 10<sup>#{Regexp.last_match[2]}</sup>" if Regexp.last_match[2]
|
304
|
+
s
|
305
|
+
end
|
281
306
|
end
|
307
|
+
end
|
282
308
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
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
|
-
|
295
|
-
|
296
|
-
|
315
|
+
# Render the search form.
|
316
|
+
get '/' do
|
317
|
+
erb :search, :locals => {:databases => Database.group_by(&:type)}
|
318
|
+
end
|
297
319
|
|
298
|
-
|
320
|
+
# BLAST search!
|
321
|
+
post '/' do
|
322
|
+
erb :result, :locals => {:report => BLAST.run(params)}
|
299
323
|
end
|
300
324
|
|
301
|
-
# get '/get_sequence/?
|
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
|
-
|
308
|
-
|
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
|
-
|
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
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
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
|
-
|
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
|
-
|
512
|
-
|
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
|
-
#
|
518
|
-
#
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
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
|