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