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