Skip to content

Commit 6f1bbad

Browse files
authored
Merge pull request #249 from ncbo/refactor/ontology-download-endpoints
refactor: consolidate ontology download routes into a shared helper
2 parents 3ba9ed5 + 7ba6174 commit 6f1bbad

5 files changed

Lines changed: 115 additions & 67 deletions

File tree

controllers/ontologies_controller.rb

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -114,33 +114,10 @@ class OntologiesController < ApplicationController
114114
acronym = params["acronym"]
115115
ont = Ontology.find(acronym).include(Ontology.goo_attrs_to_load).first
116116
error 422, "You must provide an existing `acronym` to download" if ont.nil?
117-
ont.bring(:viewingRestriction) if ont.bring?(:viewingRestriction)
118-
check_access(ont)
119-
restricted_download = LinkedData::OntologiesAPI.settings.restrict_download.include?(acronym)
120-
error 403, "License restrictions on download for #{acronym}" if restricted_download && !current_user.admin?
121-
error 403, "Ontology #{acronym} is not accessible to your user" if ont.restricted? && !ont.accessible?(current_user)
122-
latest_submission = ont.latest_submission(status: :rdf) # Should resolve to latest successfully loaded submission
123-
error 404, "There is no latest submission loaded for download" if latest_submission.nil?
124-
latest_submission.bring(:uploadFilePath)
125-
126-
download_format = params["download_format"].to_s.downcase
127-
allowed_formats = ["csv", "rdf"]
128-
if download_format.empty?
129-
file_path = latest_submission.uploadFilePath
130-
elsif ([download_format] - allowed_formats).length > 0
131-
error 400, "Invalid download format: #{download_format}."
132-
elsif download_format.eql?("csv")
133-
latest_submission.bring(ontology: [:acronym])
134-
file_path = latest_submission.csv_path
135-
elsif download_format.eql?("rdf")
136-
file_path = latest_submission.rdf_path
137-
end
138-
139-
if File.readable? file_path
140-
send_file file_path, :filename => File.basename(file_path)
141-
else
142-
error 500, "Cannot read latest submission upload file: #{file_path}"
143-
end
117+
enforce_download_access(ont)
118+
submission = ont.latest_submission(status: :rdf) # Should resolve to latest successfully loaded submission
119+
error 404, "There is no latest submission loaded for download" if submission.nil?
120+
send_submission_download(ont, submission, params["download_format"])
144121
end
145122

146123
private

controllers/ontology_submissions_controller.rb

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -192,58 +192,24 @@ class OntologySubmissionsController < ApplicationController
192192
included = Ontology.goo_attrs_to_load.concat([submissions: submission_attributes])
193193
ont = Ontology.find(acronym).include(included).first
194194
error 422, "You must provide an existing `acronym` to download" if ont.nil?
195-
ont.bring(:viewingRestriction) if ont.bring?(:viewingRestriction)
196-
check_access(ont)
197-
ont_restrict_downloads = LinkedData::OntologiesAPI.settings.restrict_download
198-
error 403, "License restrictions on download for #{acronym}" if ont_restrict_downloads.include? acronym
195+
enforce_download_access(ont)
199196
submission = ont.submission(params['ontology_submission_id'].to_i)
200197
error 404, "There is no such submission for download" if submission.nil?
201-
file_path = submission.uploadFilePath
202-
# handle edge case where uploadFilePath is not set
203-
error 422, "Upload File Path is not set for this submission" if file_path.to_s.empty?
204-
download_format = params["download_format"].to_s.downcase
205-
allowed_formats = ["csv", "rdf"]
206-
if download_format.empty?
207-
file_path = submission.uploadFilePath
208-
elsif ([download_format] - allowed_formats).length > 0
209-
error 400, "Invalid download format: #{download_format}."
210-
elsif download_format.eql?("csv")
211-
if ont.latest_submission.id != submission.id
212-
error 400, "Invalid download format: #{download_format}."
213-
else
214-
latest_submission.bring(ontology: [:acronym])
215-
file_path = submission.csv_path
216-
end
217-
elsif download_format.eql?("rdf")
218-
file_path = submission.rdf_path
219-
end
220-
221-
if File.readable? file_path
222-
send_file file_path, :filename => File.basename(file_path)
223-
else
224-
error 500, "Cannot read submission upload file: #{file_path}"
225-
end
198+
send_submission_download(ont, submission, params["download_format"])
226199
end
227200

228201
##
229202
# Download a submission diff file
230203
get '/:ontology_submission_id/download_diff' do
231204
acronym = params["acronym"]
232205
submission_attributes = [:submissionId, :submissionStatus, :diffFilePath]
233-
ont = Ontology.find(acronym).include(:submissions => submission_attributes).first
206+
included = Ontology.goo_attrs_to_load.concat([submissions: submission_attributes])
207+
ont = Ontology.find(acronym).include(included).first
234208
error 422, "You must provide an existing `acronym` to download" if ont.nil?
235-
ont.bring(:viewingRestriction)
236-
check_access(ont)
237-
ont_restrict_downloads = LinkedData::OntologiesAPI.settings.restrict_download
238-
error 403, "License restrictions on download for #{acronym}" if ont_restrict_downloads.include? acronym
209+
enforce_download_access(ont)
239210
submission = ont.submission(params['ontology_submission_id'].to_i)
240211
error 404, "There is no such submission for download" if submission.nil?
241-
file_path = submission.diffFilePath
242-
if File.readable? file_path
243-
send_file file_path, :filename => File.basename(file_path)
244-
else
245-
error 500, "Cannot read submission diff file: #{file_path}"
246-
end
212+
send_submission_download(ont, submission, "diff")
247213
end
248214

249215
def delete_submissions(startId, endId)

helpers/download_helper.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
require 'sinatra/base'
2+
3+
module Sinatra
4+
module Helpers
5+
module DownloadHelper
6+
7+
# Formats that can be requested via `download_format`. An empty/absent
8+
# value streams the original uploaded source file.
9+
ALLOWED_DOWNLOAD_FORMATS = %w[csv rdf diff].freeze
10+
11+
##
12+
# Single access gate shared by every download endpoint. Enforces, in order:
13+
# * read ACL (check_access / read_restricted?)
14+
# * license-based download restriction (admins bypass)
15+
# * private-ontology accessibility
16+
def enforce_download_access(ont)
17+
ont.bring(:viewingRestriction) if ont.bring?(:viewingRestriction)
18+
check_access(ont)
19+
restricted = LinkedData::OntologiesAPI.settings.restrict_download.include?(ont.acronym)
20+
error 403, "License restrictions on download for #{ont.acronym}" if restricted && !current_user.admin?
21+
error 403, "Ontology #{ont.acronym} is not accessible to your user" if ont.restricted? && !ont.accessible?(current_user)
22+
end
23+
24+
##
25+
# Resolve the file for a submission + requested format and stream it.
26+
# Shared by the ontology-latest, specific-submission, and diff routes so
27+
# format handling, missing-file guards, and streaming live in one place.
28+
def send_submission_download(ont, submission, download_format)
29+
download_format = download_format.to_s.downcase
30+
31+
unless download_format.empty? || ALLOWED_DOWNLOAD_FORMATS.include?(download_format)
32+
error 400, "Invalid download format: #{download_format}."
33+
end
34+
35+
file_path = resolve_download_path(ont, submission, download_format)
36+
37+
error 404, "No #{download_format.empty? ? 'source' : download_format} file is available for this submission" if file_path.to_s.empty?
38+
error 404, "Download file is not readable: #{File.basename(file_path)}" unless File.readable?(file_path)
39+
40+
# Downloads are large (often hundreds of MB) ontology files streamed from
41+
# disk. Mark them no-store so the global Rack::Cache (Redis entitystore)
42+
# does not buffer the body into Redis — doing so defeats send_file's
43+
# streaming and makes serving a cached hit a multi-second Redis GET.
44+
cache_control :no_store
45+
46+
send_file file_path, filename: File.basename(file_path)
47+
end
48+
49+
private
50+
51+
def resolve_download_path(ont, submission, download_format)
52+
case download_format
53+
when ""
54+
submission.bring(:uploadFilePath) if submission.bring?(:uploadFilePath)
55+
submission.uploadFilePath
56+
when "rdf"
57+
submission.rdf_path
58+
when "diff"
59+
submission.bring(:diffFilePath) if submission.bring?(:diffFilePath)
60+
submission.diffFilePath
61+
when "csv"
62+
# The CSV is an index artifact that only survives for the current
63+
# (highest-id) submission; the archiver deletes it for older ones.
64+
unless ont.latest_submission&.id == submission.id
65+
error 400, "CSV download is only available for the latest submission of #{ont.acronym}."
66+
end
67+
submission.bring(ontology: [:acronym])
68+
submission.csv_path
69+
end
70+
end
71+
end
72+
end
73+
end
74+
75+
helpers Sinatra::Helpers::DownloadHelper

test/controllers/test_ontologies_controller.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ def test_download_ontology
182182
# see also test_ontologies_submissions_controller::test_download_submission
183183
end
184184

185+
def test_download_ontology_sets_no_store
186+
# Downloads must be marked no-store so the global Rack::Cache does not
187+
# buffer large ontology file bodies into Redis (see helpers/download_helper.rb).
188+
acronym = create_ontologies_and_submissions(ont_count: 1, submission_count: 1, process_submission: true)[1].first
189+
get "/ontologies/#{acronym}/download"
190+
assert_equal(200, last_response.status, msg='failed download for ontology : ' + get_errors(last_response))
191+
assert_includes(last_response.headers['Cache-Control'].to_s, 'no-store',
192+
msg="download response must set Cache-Control: no-store to bypass Rack::Cache")
193+
end
194+
185195
def test_download_ontology_csv
186196
num_onts_created, created_ont_acronyms, onts = create_ontologies_and_submissions(ont_count: 1, submission_count: 1,
187197
process_submission: true,

test/controllers/test_ontology_submissions_controller.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,26 @@ def test_download_ontology_submission_rdf
233233
assert_equal(400, last_response.status, "Download failure for '#{acronym}' ontology: " + get_errors(last_response))
234234
end
235235

236+
def test_download_ontology_submission_csv
237+
# Regression: CSV download of the *latest* submission previously raised a
238+
# NameError (undefined `latest_submission`) -> 500. It must stream the CSV,
239+
# while a non-latest submission (whose CSV the archiver deletes) returns 400.
240+
_, created_ont_acronyms, onts = create_ontologies_and_submissions(ont_count: 1, submission_count: 2,
241+
process_submission: true,
242+
process_options: { process_rdf: true, extract_metadata: true, index_search: true })
243+
acronym = created_ont_acronyms.first
244+
ont = onts.first
245+
ont.bring(:submissions)
246+
subs = ont.submissions.each { |s| s.bring(:submissionId) }.sort_by(&:submissionId)
247+
older, latest = subs.first, subs.last
248+
249+
get "/ontologies/#{acronym}/submissions/#{latest.submissionId}/download?download_format=csv"
250+
assert_equal(200, last_response.status, "CSV download failed for latest submission: " + get_errors(last_response))
251+
252+
get "/ontologies/#{acronym}/submissions/#{older.submissionId}/download?download_format=csv"
253+
assert_equal(400, last_response.status, "Expected 400 for CSV of non-latest submission: " + get_errors(last_response))
254+
end
255+
236256
def test_download_acl_only
237257
_, created_ont_acronyms, onts = create_ontologies_and_submissions(ont_count: 1, submission_count: 1,
238258
process_submission: false)

0 commit comments

Comments
 (0)