sequenceserver 3.1.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/sequenceserver/api_errors.rb +18 -0
- data/lib/sequenceserver/blast/hit.rb +1 -2
- data/lib/sequenceserver/blast/job.rb +15 -5
- data/lib/sequenceserver/blast/report.rb +5 -1
- data/lib/sequenceserver/database.rb +5 -1
- data/lib/sequenceserver/job.rb +26 -10
- data/lib/sequenceserver/makeblastdb.rb +2 -1
- data/lib/sequenceserver/routes.rb +37 -12
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver/zip_file_generator.rb +1 -1
- data/public/404.html +8 -16
- data/public/css/app.css +794 -111
- data/public/css/app.min.css +1 -1
- data/public/css/sequenceserver.min.css +1 -8
- data/public/js/circos.js +103 -96
- data/public/{packages/circosJS@1.7.0.js → js/circosjs.js} +156 -778
- data/public/js/cloud_share_modal.js +55 -58
- data/public/js/collapse_preferences.js +13 -15
- data/public/js/databases.js +4 -3
- data/public/js/databases_tree.js +51 -30
- data/public/js/error_modal.js +35 -22
- data/public/js/exporter.js +2 -0
- data/public/js/form.js +43 -7
- data/public/js/grapher.js +94 -92
- data/public/js/hit.js +21 -19
- data/public/js/hits.js +45 -38
- data/public/js/hits_overview.js +8 -27
- data/public/js/hsp.js +140 -144
- data/public/js/jquery_world.js +24 -15
- data/public/js/kablammo.js +14 -10
- data/public/js/length_distribution.js +23 -37
- data/public/js/null_plugins/report_plugins.js +3 -5
- data/public/js/options.js +17 -19
- data/public/js/query.js +36 -35
- data/public/js/report/alignment_results.js +18 -0
- data/public/js/report/graphical_overview.js +38 -0
- data/public/js/report/report_header.js +28 -0
- data/public/js/report/run_summary.js +43 -0
- data/public/js/report.js +62 -102
- data/public/js/report_root.js +3 -3
- data/public/js/search.js +5 -2
- data/public/js/search_button.js +85 -78
- data/public/js/sequence.js +11 -22
- data/public/js/sequence_modal.js +38 -37
- data/public/js/share_url.js +5 -5
- data/public/js/sidebar.js +148 -86
- data/public/js/svgExporter.js +8 -3
- data/public/js/tests/form.spec.js +1 -1
- data/public/js/tests/visualisation_helpers.spec.js +28 -0
- data/public/js/visualisation_helpers.js +24 -27
- data/public/sequenceserver-report.min.js +1 -1
- data/public/sequenceserver-report.min.js.LICENSE.txt +1 -88
- data/public/sequenceserver-report.min.js.map +1 -1
- data/public/sequenceserver-search.min.js +1 -1
- data/public/sequenceserver-search.min.js.LICENSE.txt +0 -30
- data/public/sequenceserver-search.min.js.map +1 -1
- data/views/error.erb +1 -1
- data/views/layout.erb +123 -130
- data/views/report.erb +3 -2
- data/views/search.erb +10 -14
- metadata +13 -26
- data/public/css/fonts.css +0 -31
- data/public/css/grapher.css +0 -670
- data/public/css/sequenceserver.css +0 -509
- data/public/vendor/github/mbostock/d3@3.5.6/d3.js +0 -9506
- data/public/vendor/github/mbostock/d3@3.5.6.js +0 -1
- data/views/search_layout.erb +0 -153
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f52305fc6b02d855f14730d480c99e940cf384c78b43f8791b789d4fd92b10bf
|
|
4
|
+
data.tar.gz: 2b8467a02de662d8a8e74f15916973000da735440a5651705fa3cce0df1c4b15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 53226eb96fce945402ea6889b188aff8431c15672b8fd57686143aaca48bacda86acfaa3c15e7843c91cdc9113545b0d042910ccbd36d32cb9d478e5b1b67da9
|
|
7
|
+
data.tar.gz: a2df8167ce52ce9461c0079f000b41f0508dcb36a1ab751a46012737dd9b9a34e953a327093457d1f72dcdc51079b478235cfc641b1fcbdd3085699c72a2f5ce
|
|
@@ -128,6 +128,24 @@ module SequenceServer
|
|
|
128
128
|
attr_reader :more_info
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
class XmlImportError < APIError
|
|
132
|
+
def http_status
|
|
133
|
+
400
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def title
|
|
137
|
+
'XML import failed'
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def message
|
|
141
|
+
'Please make sure the XML file is a valid BLAST output.'
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def more_info
|
|
145
|
+
'Make sure the XML you are importing is generated by blast with -outfmt 5 option.'
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
131
149
|
# Errors caused by everything other than invalid user input.
|
|
132
150
|
class SystemError < APIError
|
|
133
151
|
def initialize(more_info = nil)
|
|
@@ -38,8 +38,7 @@ module SequenceServer
|
|
|
38
38
|
# instance methods of the Links module.
|
|
39
39
|
def links
|
|
40
40
|
links = Links.instance_methods.map { |m| send m }
|
|
41
|
-
links.compact
|
|
42
|
-
links.sort_by { |link| [link[:order], link[:title]] }
|
|
41
|
+
links.flatten.compact.sort_by { |link| [link[:order], link[:title]] }
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
# Returns the database type (nucleotide or protein).
|
|
@@ -26,9 +26,9 @@ module SequenceServer
|
|
|
26
26
|
@qfile = store('query.fa', params[:sequence])
|
|
27
27
|
@databases = Database[params[:databases]]
|
|
28
28
|
@advanced = params[:advanced].to_s.strip
|
|
29
|
-
@
|
|
29
|
+
@num_threads = params[:num_threads] || config[:num_threads]
|
|
30
|
+
@options = @advanced + defaults
|
|
30
31
|
# The following params are for analytics only
|
|
31
|
-
@num_threads = config[:num_threads]
|
|
32
32
|
@query_length = calculate_query_size
|
|
33
33
|
@number_of_query_sequences = calculate_number_of_sequences
|
|
34
34
|
@databases_ncharacters_total = calculate_databases_ncharacters_total
|
|
@@ -62,8 +62,9 @@ module SequenceServer
|
|
|
62
62
|
# Returns the command that will be executed. Job super class takes care
|
|
63
63
|
# of actual execution.
|
|
64
64
|
def command
|
|
65
|
-
|
|
66
|
-
" -query '#{qfile}' #{options}"
|
|
65
|
+
"#{method} -db '#{databases.map(&:name).join(' ')}'" \
|
|
66
|
+
" -query '#{qfile}' #{options}" \
|
|
67
|
+
" -num_threads #{@num_threads || config[:num_threads]}"
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def raise!
|
|
@@ -114,10 +115,11 @@ module SequenceServer
|
|
|
114
115
|
validate_sequences params[:sequence]
|
|
115
116
|
validate_databases params[:databases]
|
|
116
117
|
validate_options params[:advanced]
|
|
118
|
+
validate_num_threads params[:num_threads]
|
|
117
119
|
end
|
|
118
120
|
|
|
119
121
|
def defaults
|
|
120
|
-
" -outfmt '11 qcovs qcovhsp'
|
|
122
|
+
" -outfmt '11 qcovs qcovhsp'"
|
|
121
123
|
end
|
|
122
124
|
|
|
123
125
|
def validate_method(method)
|
|
@@ -159,6 +161,14 @@ module SequenceServer
|
|
|
159
161
|
true
|
|
160
162
|
end
|
|
161
163
|
|
|
164
|
+
# num_threads is interpolated unquoted into the shell command, so it
|
|
165
|
+
# must be a positive integer. nil means: use the configured default.
|
|
166
|
+
def validate_num_threads(num_threads)
|
|
167
|
+
return true if num_threads.nil? || num_threads.to_s.match?(/\A[1-9][0-9]*\z/)
|
|
168
|
+
|
|
169
|
+
fail InputError, 'num_threads should be a positive integer.'
|
|
170
|
+
end
|
|
171
|
+
|
|
162
172
|
def allowed_chars
|
|
163
173
|
/\A[a-z0-9\-_. ',]*\Z/i
|
|
164
174
|
end
|
|
@@ -93,7 +93,11 @@ module SequenceServer
|
|
|
93
93
|
def xml_ir
|
|
94
94
|
@xml_ir ||=
|
|
95
95
|
if job.imported_xml_file
|
|
96
|
-
parse_xml
|
|
96
|
+
parse_xml(File.read(job.imported_xml_file)).tap do |xml_ir|
|
|
97
|
+
if xml_ir.size < 9 || !xml_ir[0].is_a?(String) || !xml_ir[1].is_a?(String) || !xml_ir[7].is_a?(Array) || !xml_ir[8].is_a?(Array)
|
|
98
|
+
raise SequenceServer::XmlImportError
|
|
99
|
+
end
|
|
100
|
+
end
|
|
97
101
|
else
|
|
98
102
|
job.raise!
|
|
99
103
|
parse_xml(xml_formatter.read_file)
|
|
@@ -45,10 +45,14 @@ module SequenceServer
|
|
|
45
45
|
cmd = "blastdbcmd -db #{name} -entry '#{accession}'"
|
|
46
46
|
|
|
47
47
|
if coords
|
|
48
|
+
# The regex MUST be anchored (\A ... \z). An unanchored match would
|
|
49
|
+
# accept any string that merely *contains* a numeric range, letting an
|
|
50
|
+
# attacker smuggle shell metacharacters after it (e.g. "1-1;id") into
|
|
51
|
+
# the unquoted `-range` argument below, which `sys` runs via /bin/sh.
|
|
48
52
|
fail(
|
|
49
53
|
InvalidParameterError,
|
|
50
54
|
"Invalid range coordinates: #{coords}"
|
|
51
|
-
) unless coords =~
|
|
55
|
+
) unless coords =~ /\A[0-9]+-[0-9]*\z/
|
|
52
56
|
|
|
53
57
|
cmd << " -range #{coords}"
|
|
54
58
|
end
|
data/lib/sequenceserver/job.rb
CHANGED
|
@@ -21,6 +21,14 @@ module SequenceServer
|
|
|
21
21
|
# Singleton methods provide the facility to create and queue a job,
|
|
22
22
|
# fetch a job or all jobs, and delete a job.
|
|
23
23
|
class Job
|
|
24
|
+
# Job ids are used to build paths under DOTDIR and end up in shell
|
|
25
|
+
# commands, so only allow characters that are safe in both. Ids may have
|
|
26
|
+
# several path segments (test fixtures do), but no segment may start with
|
|
27
|
+
# a dot, which rules out '.', '..' and hidden directories, and the id may
|
|
28
|
+
# not start with '/', so it always stays inside DOTDIR.
|
|
29
|
+
ID_SEGMENT = '[a-zA-Z0-9_-][a-zA-Z0-9_.-]*'.freeze
|
|
30
|
+
ID_PATTERN = %r{\A#{ID_SEGMENT}(/#{ID_SEGMENT})*\z}
|
|
31
|
+
|
|
24
32
|
class << self
|
|
25
33
|
# Creates and queues a job. Returns created job object.
|
|
26
34
|
def create(params)
|
|
@@ -40,6 +48,8 @@ module SequenceServer
|
|
|
40
48
|
|
|
41
49
|
# Fetches job with the given id.
|
|
42
50
|
def fetch(id)
|
|
51
|
+
return nil unless id.to_s.match?(ID_PATTERN)
|
|
52
|
+
|
|
43
53
|
job_file = File.join(DOTDIR, id, 'job.yaml')
|
|
44
54
|
return nil unless File.exist?(job_file)
|
|
45
55
|
|
|
@@ -86,17 +96,23 @@ module SequenceServer
|
|
|
86
96
|
# Subclasses should extend `initialize` as per requirement.
|
|
87
97
|
def initialize(params = {})
|
|
88
98
|
@id = params.fetch(:id, SecureRandom.uuid)
|
|
99
|
+
# Validate before anything touches the filesystem: the rescue below
|
|
100
|
+
# removes `dir`, which must never be derived from a bad id.
|
|
101
|
+
fail ArgumentError, "Invalid job id: #{@id}" unless @id.to_s.match?(ID_PATTERN)
|
|
102
|
+
|
|
89
103
|
@submitted_at = Time.now
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
begin
|
|
105
|
+
mkdir_p dir
|
|
106
|
+
yield if block_given?
|
|
107
|
+
save
|
|
108
|
+
rescue Errno::ENOSPC
|
|
109
|
+
raise SystemError, 'Not enough disk space to start a new job'
|
|
110
|
+
rescue Errno::EACCES
|
|
111
|
+
raise SystemError, "Permission denied to write to #{DOTDIR}"
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
rm_rf dir
|
|
114
|
+
raise e
|
|
115
|
+
end
|
|
100
116
|
end
|
|
101
117
|
|
|
102
118
|
attr_reader :id, :submitted_at, :completed_at, :exitstatus
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'find'
|
|
2
|
+
require 'shellwords'
|
|
2
3
|
require 'forwardable'
|
|
3
4
|
|
|
4
5
|
module SequenceServer
|
|
@@ -144,7 +145,7 @@ module SequenceServer
|
|
|
144
145
|
# directory. Returns the output of `blastdbcmd`. This method is called
|
|
145
146
|
# by `determine_formatted_fastas`.
|
|
146
147
|
def blastdbcmd
|
|
147
|
-
cmd = "blastdbcmd -recursive -list #{config[:database_dir]}" \
|
|
148
|
+
cmd = "blastdbcmd -recursive -list #{Shellwords.escape(config[:database_dir])}" \
|
|
148
149
|
' -list_outfmt "%f %t %p %n %l %d %v"'
|
|
149
150
|
out, err = sys(cmd, path: config[:bin])
|
|
150
151
|
errpat = /BLAST Database error/
|
|
@@ -35,7 +35,9 @@ module SequenceServer
|
|
|
35
35
|
# e.g. for example.org/our-sequenceserver set to '/our-sequenceserver'
|
|
36
36
|
set :root_path_prefix, ''
|
|
37
37
|
|
|
38
|
-
set :
|
|
38
|
+
set :asset_prefix, ''
|
|
39
|
+
|
|
40
|
+
set :layout, :'layout'
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# See
|
|
@@ -66,7 +68,7 @@ module SequenceServer
|
|
|
66
68
|
secret: ENV.fetch('SESSION_SECRET') { SecureRandom.alphanumeric(64) }
|
|
67
69
|
)
|
|
68
70
|
|
|
69
|
-
use Rack::Csrf, raise: true, skip: ['POST:/cloud_share']
|
|
71
|
+
use Rack::Csrf, raise: true, skip: ['POST:/cloud_share'] unless ENV['SKIP_CSRF_PROTECTION'] == 'true'
|
|
70
72
|
end
|
|
71
73
|
|
|
72
74
|
unless ENV['SEQUENCE_SERVER_COMPRESS_RESPONSES'] == 'false'
|
|
@@ -86,7 +88,7 @@ module SequenceServer
|
|
|
86
88
|
|
|
87
89
|
# Returns base HTML. Rest happens client-side: rendering the search form.
|
|
88
90
|
get '/' do
|
|
89
|
-
erb :search, layout: settings.
|
|
91
|
+
erb :search, layout: settings.layout
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
# Returns data that is used to render the search form client side. These
|
|
@@ -113,9 +115,9 @@ module SequenceServer
|
|
|
113
115
|
post '/' do
|
|
114
116
|
if params[:input_sequence]
|
|
115
117
|
@input_sequence = params[:input_sequence]
|
|
116
|
-
erb :search, layout: settings.
|
|
118
|
+
erb :search, layout: settings.layout
|
|
117
119
|
else
|
|
118
|
-
job = Job.create(
|
|
120
|
+
job = Job.create(job_params)
|
|
119
121
|
redirect to("/#{job.id}")
|
|
120
122
|
end
|
|
121
123
|
end
|
|
@@ -143,7 +145,7 @@ module SequenceServer
|
|
|
143
145
|
job = Job.fetch(jid)
|
|
144
146
|
halt 404, File.read(File.join(settings.root, 'public/404.html')) if job.nil?
|
|
145
147
|
|
|
146
|
-
erb :report, layout:
|
|
148
|
+
erb :report, layout: settings.layout
|
|
147
149
|
end
|
|
148
150
|
# @params sequence_ids: whitespace separated list of sequence ids to
|
|
149
151
|
# retrieve
|
|
@@ -156,7 +158,7 @@ module SequenceServer
|
|
|
156
158
|
# in identifiers) and retreival_databases (we don't allow whitespace in a
|
|
157
159
|
# database's name, so it's safe).
|
|
158
160
|
get '/get_sequence/' do
|
|
159
|
-
sequence_ids = params[:sequence_ids].to_s.split(',')
|
|
161
|
+
sequence_ids = params[:sequence_ids].to_s.split(',').uniq
|
|
160
162
|
database_ids = params[:database_ids].to_s.split(',')
|
|
161
163
|
if sequence_ids.empty?
|
|
162
164
|
status 422
|
|
@@ -172,7 +174,7 @@ module SequenceServer
|
|
|
172
174
|
end
|
|
173
175
|
|
|
174
176
|
post '/get_sequence' do
|
|
175
|
-
sequence_ids = params['sequence_ids'].to_s.split(',')
|
|
177
|
+
sequence_ids = params['sequence_ids'].to_s.split(',').uniq
|
|
176
178
|
database_ids = params['database_ids'].to_s.split(',')
|
|
177
179
|
|
|
178
180
|
if sequence_ids.empty?
|
|
@@ -289,11 +291,9 @@ module SequenceServer
|
|
|
289
291
|
error.class.name
|
|
290
292
|
end
|
|
291
293
|
|
|
292
|
-
|
|
293
|
-
# have more_info, use backtrace.join("\n") as more_info.
|
|
294
|
-
if error.respond_to? :more_info
|
|
294
|
+
if error.respond_to?(:more_info)
|
|
295
295
|
error_data[:more_info] = error.more_info
|
|
296
|
-
elsif error.respond_to?
|
|
296
|
+
elsif error.respond_to?(:backtrace) && !settings.production?
|
|
297
297
|
error_data[:more_info] = error.backtrace.join("\n")
|
|
298
298
|
end
|
|
299
299
|
|
|
@@ -358,6 +358,18 @@ module SequenceServer
|
|
|
358
358
|
def root_path_prefix
|
|
359
359
|
settings.root_path_prefix.to_s
|
|
360
360
|
end
|
|
361
|
+
|
|
362
|
+
def css_asset_path
|
|
363
|
+
File.join(settings.asset_prefix, 'css', 'app.min.css') + '?v=' + SequenceServer::VERSION
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def asset_path(file)
|
|
367
|
+
File.join(settings.asset_prefix, file)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def asset_prefix
|
|
371
|
+
settings.asset_prefix
|
|
372
|
+
end
|
|
361
373
|
end
|
|
362
374
|
|
|
363
375
|
private
|
|
@@ -365,5 +377,18 @@ module SequenceServer
|
|
|
365
377
|
def fetch_job(job_id)
|
|
366
378
|
Job.fetch(job_id)
|
|
367
379
|
end
|
|
380
|
+
|
|
381
|
+
# Only these request parameters may reach Job.create. Job#initialize and
|
|
382
|
+
# BLAST::Job#initialize also read :id, :xml and :num_threads, which end up
|
|
383
|
+
# in file paths and in the shell command, so they must never come from the
|
|
384
|
+
# request.
|
|
385
|
+
def job_params
|
|
386
|
+
{
|
|
387
|
+
method: params[:method],
|
|
388
|
+
sequence: params[:sequence],
|
|
389
|
+
databases: params[:databases],
|
|
390
|
+
advanced: params[:advanced]
|
|
391
|
+
}
|
|
392
|
+
end
|
|
368
393
|
end
|
|
369
394
|
end
|
|
@@ -23,7 +23,7 @@ class ZipFileGenerator
|
|
|
23
23
|
def write
|
|
24
24
|
entries = Dir.entries(@input_dir) - %w[. ..]
|
|
25
25
|
|
|
26
|
-
::Zip::File.open(@output_file,
|
|
26
|
+
::Zip::File.open(@output_file, create: true) do |zipfile|
|
|
27
27
|
write_entries entries, '', zipfile
|
|
28
28
|
end
|
|
29
29
|
end
|
data/public/404.html
CHANGED
|
@@ -4,24 +4,16 @@
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="utf-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
-
|
|
8
7
|
<title>SequenceServer: Not Found</title>
|
|
9
|
-
|
|
10
|
-
<!-- Bootstrap CSS -->
|
|
11
|
-
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet"
|
|
12
|
-
integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
|
8
|
+
<link rel="stylesheet" media="screen,print" type="text/css" href="css/app.min.css"/>
|
|
13
9
|
</head>
|
|
14
10
|
|
|
15
|
-
<body class="bg-
|
|
16
|
-
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
<p class="lead">Sorry, the page you are looking for could not be found.</p>
|
|
22
|
-
<a href="/" class="btn btn-primary mt-3">Start over</a>
|
|
11
|
+
<body class="bg-white text-black text-center">
|
|
12
|
+
<img src="/sequenceserver_logo.webp" alt="SequenceServer Logo" class="mt-12 mb-4 w-48">
|
|
13
|
+
<div class="container mt-12">
|
|
14
|
+
<h1 class="text-4xl font-light leading-tight">404 Not Found</h1>
|
|
15
|
+
<p class="text-xl font-light mb-4">Sorry, the page you are looking for could not be found.</p>
|
|
16
|
+
<a href="/" class="py-2 px-4 rounded-md shadow-sm text-white bg-seqblue">Start over</a>
|
|
23
17
|
</div>
|
|
24
|
-
|
|
25
18
|
</body>
|
|
26
|
-
|
|
27
|
-
</html>
|
|
19
|
+
</html>
|