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