Adding missing file.
[arvados.git] / apps / workbench / app / controllers / collections_controller.rb
1 class CollectionsController < ApplicationController
2   skip_around_filter :thread_with_mandatory_api_token, only: [:show_file]
3   skip_before_filter :find_object_by_uuid, only: [:provenance, :show_file]
4   skip_before_filter :check_user_agreements, only: [:show_file]
5
6   def show_pane_list
7     %w(Files Attributes Metadata Provenance_graph Used_by JSON API)
8   end
9
10   def set_persistent
11     case params[:value]
12     when 'persistent', 'cache'
13       persist_links = Link.filter([['owner_uuid', '=', current_user.uuid],
14                                    ['link_class', '=', 'resources'],
15                                    ['name', '=', 'wants'],
16                                    ['tail_uuid', '=', current_user.uuid],
17                                    ['head_uuid', '=', @object.uuid]])
18       logger.debug persist_links.inspect
19     else
20       return unprocessable "Invalid value #{value.inspect}"
21     end
22     if params[:value] == 'persistent'
23       if not persist_links.any?
24         Link.create(link_class: 'resources',
25                     name: 'wants',
26                     tail_uuid: current_user.uuid,
27                     head_uuid: @object.uuid)
28       end
29     else
30       persist_links.each do |link|
31         link.destroy || raise
32       end
33     end
34
35     respond_to do |f|
36       f.json { render json: @object }
37     end
38   end
39
40   def index
41     if params[:search].andand.length.andand > 0
42       tags = Link.where(any: ['contains', params[:search]])
43       @collections = (Collection.where(uuid: tags.collect(&:head_uuid)) |
44                       Collection.where(any: ['contains', params[:search]])).
45         uniq { |c| c.uuid }
46     else
47       if params[:limit]
48         limit = params[:limit].to_i
49       else
50         limit = 100
51       end
52
53       if params[:offset]
54         offset = params[:offset].to_i
55       else
56         offset = 0
57       end
58
59       @collections = Collection.limit(limit).offset(offset)
60     end
61     @links = Link.limit(1000).
62       where(head_uuid: @collections.collect(&:uuid))
63     @collection_info = {}
64     @collections.each do |c|
65       @collection_info[c.uuid] = {
66         tag_links: [],
67         wanted: false,
68         wanted_by_me: false,
69         provenance: [],
70         links: []
71       }
72     end
73     @links.each do |link|
74       @collection_info[link.head_uuid] ||= {}
75       info = @collection_info[link.head_uuid]
76       case link.link_class
77       when 'tag'
78         info[:tag_links] << link
79       when 'resources'
80         info[:wanted] = true
81         info[:wanted_by_me] ||= link.tail_uuid == current_user.uuid
82       when 'provenance'
83         info[:provenance] << link.name
84       end
85       info[:links] << link
86     end
87     @request_url = request.url
88   end
89
90   def show_file
91     # We pipe from arv-get to send the file to the user.  Before we start it,
92     # we ask the API server if the file actually exists.  This serves two
93     # purposes: it lets us return a useful status code for common errors, and
94     # helps us figure out which token to provide to arv-get.
95     coll = nil
96     usable_token = find_usable_token do
97       coll = Collection.find(params[:uuid])
98     end
99     if usable_token.nil?
100       return  # Response already rendered.
101     elsif params[:file].nil? or not file_in_collection?(coll, params[:file])
102       return render_not_found
103     end
104     opts = params.merge(arvados_api_token: usable_token)
105     ext = File.extname(params[:file])
106     self.response.headers['Content-Type'] =
107       Rack::Mime::MIME_TYPES[ext] || 'application/octet-stream'
108     self.response.headers['Content-Length'] = params[:size] if params[:size]
109     self.response.headers['Content-Disposition'] = params[:disposition] if params[:disposition]
110     self.response_body = file_enumerator opts
111   end
112
113   def search_scopes
114     ApiClientAuthorization.where(filters: [['scopes', '=', ["GET /arvados/v1/collections/#{@object.uuid}", "GET /arvados/v1/collections/#{@object.uuid}/"]]])
115   end
116
117   def show
118     return super if !@object
119     @provenance = []
120     @output2job = {}
121     @output2colorindex = {}
122     @sourcedata = {params[:uuid] => {uuid: params[:uuid]}}
123     @protected = {}
124     @search_sharing = search_scopes.select { |s| s.scopes != ['all'] }
125
126     colorindex = -1
127     any_hope_left = true
128     while any_hope_left
129       any_hope_left = false
130       Job.where(output: @sourcedata.keys).sort_by { |a| a.finished_at || a.created_at }.reverse.each do |job|
131         if !@output2colorindex[job.output]
132           any_hope_left = true
133           @output2colorindex[job.output] = (colorindex += 1) % 10
134           @provenance << {job: job, output: job.output}
135           @sourcedata.delete job.output
136           @output2job[job.output] = job
137           job.dependencies.each do |new_source_data|
138             unless @output2colorindex[new_source_data]
139               @sourcedata[new_source_data] = {uuid: new_source_data}
140             end
141           end
142         end
143       end
144     end
145
146     Link.where(head_uuid: @sourcedata.keys | @output2job.keys).each do |link|
147       if link.link_class == 'resources' and link.name == 'wants'
148         @protected[link.head_uuid] = true
149         if link.tail_uuid == current_user.uuid
150           @is_persistent = true
151         end
152       end
153     end
154     Link.where(tail_uuid: @sourcedata.keys).each do |link|
155       if link.link_class == 'data_origin'
156         @sourcedata[link.tail_uuid][:data_origins] ||= []
157         @sourcedata[link.tail_uuid][:data_origins] << [link.name, link.head_uuid]
158       end
159     end
160     Collection.where(uuid: @sourcedata.keys).each do |collection|
161       if @sourcedata[collection.uuid]
162         @sourcedata[collection.uuid][:collection] = collection
163       end
164     end
165
166     Collection.where(uuid: @object.uuid).each do |u|
167       @prov_svg = ProvenanceHelper::create_provenance_graph(u.provenance, "provenance_svg",
168                                                             {:request => request,
169                                                               :direction => :bottom_up,
170                                                               :combine_jobs => :script_only}) rescue nil
171       @used_by_svg = ProvenanceHelper::create_provenance_graph(u.used_by, "used_by_svg",
172                                                                {:request => request,
173                                                                  :direction => :top_down,
174                                                                  :combine_jobs => :script_only,
175                                                                  :pdata_only => true}) rescue nil
176     end
177   end
178
179   def sharing_popup
180     @search_sharing = search_scopes.select { |s| s.scopes != ['all'] }
181     respond_to do |format|
182       format.html
183       format.js
184     end
185   end
186
187   def share
188     a = ApiClientAuthorization.create(scopes: ["GET /arvados/v1/collections/#{@object.uuid}", "GET /arvados/v1/collections/#{@object.uuid}/"])
189     @search_sharing = search_scopes.select { |s| s.scopes != ['all'] }
190     render 'sharing_popup'
191   end
192
193   def unshare
194     @search_sharing = search_scopes.select { |s| s.scopes != ['all'] }
195     @search_sharing.each do |s|
196       s.destroy
197     end
198     @search_sharing = search_scopes.select { |s| s.scopes != ['all'] }
199     render 'sharing_popup'
200   end
201
202   protected
203
204   def find_usable_token
205     # Iterate over every token available to make it the current token and
206     # yield the given block.
207     # If the block succeeds, return the token it used.
208     # Otherwise, render an error response based on the most specific
209     # error we encounter, and return nil.
210     read_tokens = [Thread.current[:arvados_api_token]].compact
211     if params[:reader_tokens].is_a? Array
212       read_tokens += params[:reader_tokens]
213     end
214     most_specific_error = [401]
215     read_tokens.each do |api_token|
216       using_specific_api_token(api_token) do
217         begin
218           yield
219           return api_token
220         rescue ArvadosApiClient::NotLoggedInException => error
221           status = 401
222         rescue => error
223           status = (error.message =~ /\[API: (\d+)\]$/) ? $1.to_i : nil
224           raise unless [401, 403, 404].include?(status)
225         end
226         if status >= most_specific_error.first
227           most_specific_error = [status, error]
228         end
229       end
230     end
231     case most_specific_error.shift
232     when 401, 403
233       redirect_to_login
234     when 404
235       render_not_found(*most_specific_error)
236     end
237     return nil
238   end
239
240   def file_in_collection?(collection, filename)
241     def normalized_path(part_list)
242       File.join(part_list).sub(%r{^\./}, '')
243     end
244     target = normalized_path([filename])
245     collection.files.each do |file_spec|
246       return true if (normalized_path(file_spec[0, 2]) == target)
247     end
248     false
249   end
250
251   def file_enumerator(opts)
252     FileStreamer.new opts
253   end
254
255   class FileStreamer
256     def initialize(opts={})
257       @opts = opts
258     end
259     def each
260       return unless @opts[:uuid] && @opts[:file]
261       env = Hash[ENV].
262         merge({
263                 'ARVADOS_API_HOST' =>
264                 $arvados_api_client.arvados_v1_base.
265                 sub(/\/arvados\/v1/, '').
266                 sub(/^https?:\/\//, ''),
267                 'ARVADOS_API_TOKEN' =>
268                 @opts[:arvados_api_token],
269                 'ARVADOS_API_HOST_INSECURE' =>
270                 Rails.configuration.arvados_insecure_https ? 'true' : 'false'
271               })
272       IO.popen([env, 'arv-get', "#{@opts[:uuid]}/#{@opts[:file]}"],
273                'rb') do |io|
274         while buf = io.read(2**20)
275           yield buf
276         end
277       end
278       Rails.logger.warn("#{@opts[:uuid]}/#{@opts[:file]}: #{$?}") if $? != 0
279     end
280   end
281 end