b174879d0954681e58030d99bc39daf3c6adf7e9
[arvados.git] / services / api / app / controllers / arvados / v1 / collections_controller.rb
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: AGPL-3.0
4
5 require "arvados/keep"
6 require "trashable"
7
8 class Arvados::V1::CollectionsController < ApplicationController
9   include DbCurrentTime
10   include TrashableController
11
12   def self._index_requires_parameters
13     (super rescue {}).
14       merge({
15         include_trash: {
16           type: 'boolean', required: false, description: "Include collections whose is_trashed attribute is true."
17         },
18         include_old_versions: {
19           type: 'boolean', required: false, description: "Include past collection versions."
20         },
21       })
22   end
23
24   def self._show_requires_parameters
25     (super rescue {}).
26       merge({
27         include_trash: {
28           type: 'boolean', required: false, description: "Show collection even if its is_trashed attribute is true."
29         },
30         include_old_versions: {
31           type: 'boolean', required: false, default: true, description: "Include past collection versions.",
32         },
33       })
34   end
35
36   def create
37     if resource_attrs[:uuid] and (loc = Keep::Locator.parse(resource_attrs[:uuid]))
38       resource_attrs[:portable_data_hash] = loc.to_s
39       resource_attrs.delete :uuid
40     end
41     resource_attrs.delete :version
42     resource_attrs.delete :current_version_uuid
43     super
44   end
45
46   def find_objects_for_index
47     opts = {
48       include_trash: params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name),
49       include_old_versions: params[:include_old_versions] || false,
50     }
51     @objects = Collection.readable_by(*@read_users, opts) if !opts.empty?
52     super
53   end
54
55   def find_object_by_uuid
56     if loc = Keep::Locator.parse(params[:id])
57       loc.strip_hints!
58
59       opts = {
60         include_trash: params[:include_trash],
61         include_old_versions: params[:include_old_versions],
62       }
63
64       # It matters which Collection object we pick because we use it to get signed_manifest_text,
65       # the value of which is affected by the value of trash_at.
66       #
67       # From postgres doc: "By default, null values sort as if larger than any non-null
68       # value; that is, NULLS FIRST is the default for DESC order, and
69       # NULLS LAST otherwise."
70       #
71       # "trash_at desc" sorts null first, then latest to earliest, so
72       # it will select the Collection object with the longest
73       # available lifetime.
74
75       if c = Collection.readable_by(*@read_users, opts).where({ portable_data_hash: loc.to_s }).order("trash_at desc").limit(1).first
76         @object = {
77           uuid: c.portable_data_hash,
78           portable_data_hash: c.portable_data_hash,
79           manifest_text: c.signed_manifest_text,
80         }
81       end
82     else
83       super
84     end
85   end
86
87   def show
88     if @object.is_a? Collection
89       # Omit unsigned_manifest_text
90       @select ||= model_class.selectable_attributes - ["unsigned_manifest_text"]
91       super
92     else
93       send_json @object
94     end
95   end
96
97
98   def find_collections(visited, sp, ignore_columns=[], &b)
99     case sp
100     when ArvadosModel
101       sp.class.columns.each do |c|
102         find_collections(visited, sp[c.name.to_sym], &b) if !ignore_columns.include?(c.name)
103       end
104     when Hash
105       sp.each do |k, v|
106         find_collections(visited, v, &b)
107       end
108     when Array
109       sp.each do |v|
110         find_collections(visited, v, &b)
111       end
112     when String
113       if m = /[a-f0-9]{32}\+\d+/.match(sp)
114         yield m[0], nil
115       elsif m = Collection.uuid_regex.match(sp)
116         yield nil, m[0]
117       end
118     end
119   end
120
121   def search_edges(visited, uuid, direction)
122     if uuid.nil? or uuid.empty? or visited[uuid]
123       return
124     end
125
126     if loc = Keep::Locator.parse(uuid)
127       loc.strip_hints!
128       return if visited[loc.to_s]
129     end
130
131     if loc
132       # uuid is a portable_data_hash
133       collections = Collection.readable_by(*@read_users).where(portable_data_hash: loc.to_s)
134       c = collections.limit(2).all
135       if c.size == 1
136         visited[loc.to_s] = c[0]
137       elsif c.size > 1
138         name = collections.limit(1).where("name <> ''").first
139         if name
140           visited[loc.to_s] = {
141             portable_data_hash: c[0].portable_data_hash,
142             name: "#{name.name} + #{collections.count-1} more"
143           }
144         else
145           visited[loc.to_s] = {
146             portable_data_hash: c[0].portable_data_hash,
147             name: loc.to_s
148           }
149         end
150       end
151
152       if direction == :search_up
153         # Search upstream for jobs where this locator is the output of some job
154         if !Rails.configuration.API.DisabledAPIs["jobs.list"]
155           Job.readable_by(*@read_users).where(output: loc.to_s).each do |job|
156             search_edges(visited, job.uuid, :search_up)
157           end
158
159           Job.readable_by(*@read_users).where(log: loc.to_s).each do |job|
160             search_edges(visited, job.uuid, :search_up)
161           end
162         end
163
164         Container.readable_by(*@read_users).where(output: loc.to_s).each do |c|
165           search_edges(visited, c.uuid, :search_up)
166         end
167
168         Container.readable_by(*@read_users).where(log: loc.to_s).each do |c|
169           search_edges(visited, c.uuid, :search_up)
170         end
171       elsif direction == :search_down
172         if loc.to_s == "d41d8cd98f00b204e9800998ecf8427e+0"
173           # Special case, don't follow the empty collection.
174           return
175         end
176
177         # Search downstream for jobs where this locator is in script_parameters
178         if !Rails.configuration.API.DisabledAPIs["jobs.list"]
179           Job.readable_by(*@read_users).where(["jobs.script_parameters like ?", "%#{loc.to_s}%"]).each do |job|
180             search_edges(visited, job.uuid, :search_down)
181           end
182
183           Job.readable_by(*@read_users).where(["jobs.docker_image_locator = ?", "#{loc.to_s}"]).each do |job|
184             search_edges(visited, job.uuid, :search_down)
185           end
186         end
187
188         Container.readable_by(*@read_users).where([Container.full_text_trgm + " like ?", "%#{loc.to_s}%"]).each do |c|
189           if c.output != loc.to_s && c.log != loc.to_s
190             search_edges(visited, c.uuid, :search_down)
191           end
192         end
193       end
194     else
195       # uuid is a regular Arvados UUID
196       rsc = ArvadosModel::resource_class_for_uuid uuid
197       if rsc == Job
198         Job.readable_by(*@read_users).where(uuid: uuid).each do |job|
199           visited[uuid] = job.as_api_response
200           if direction == :search_up
201             # Follow upstream collections referenced in the script parameters
202             find_collections(visited, job) do |hash, col_uuid|
203               search_edges(visited, hash, :search_up) if hash
204               search_edges(visited, col_uuid, :search_up) if col_uuid
205             end
206           elsif direction == :search_down
207             # Follow downstream job output
208             search_edges(visited, job.output, direction)
209           end
210         end
211       elsif rsc == Container
212         c = Container.readable_by(*@read_users).where(uuid: uuid).limit(1).first
213         if c
214           visited[uuid] = c.as_api_response
215           if direction == :search_up
216             # Follow upstream collections referenced in the script parameters
217             find_collections(visited, c, ignore_columns=["log", "output"]) do |hash, col_uuid|
218               search_edges(visited, hash, :search_up) if hash
219               search_edges(visited, col_uuid, :search_up) if col_uuid
220             end
221           elsif direction == :search_down
222             # Follow downstream job output
223             search_edges(visited, c.output, :search_down)
224           end
225         end
226       elsif rsc == ContainerRequest
227         c = ContainerRequest.readable_by(*@read_users).where(uuid: uuid).limit(1).first
228         if c
229           visited[uuid] = c.as_api_response
230           if direction == :search_up
231             # Follow upstream collections
232             find_collections(visited, c, ignore_columns=["log_uuid", "output_uuid"]) do |hash, col_uuid|
233               search_edges(visited, hash, :search_up) if hash
234               search_edges(visited, col_uuid, :search_up) if col_uuid
235             end
236           elsif direction == :search_down
237             # Follow downstream job output
238             search_edges(visited, c.output_uuid, :search_down)
239           end
240         end
241       elsif rsc == Collection
242         c = Collection.readable_by(*@read_users).where(uuid: uuid).limit(1).first
243         if c
244           if direction == :search_up
245             visited[c.uuid] = c.as_api_response
246
247             if !Rails.configuration.API.DisabledAPIs["jobs.list"]
248               Job.readable_by(*@read_users).where(output: c.portable_data_hash).each do |job|
249                 search_edges(visited, job.uuid, :search_up)
250               end
251
252               Job.readable_by(*@read_users).where(log: c.portable_data_hash).each do |job|
253                 search_edges(visited, job.uuid, :search_up)
254               end
255             end
256
257             ContainerRequest.readable_by(*@read_users).where(output_uuid: uuid).each do |cr|
258               search_edges(visited, cr.uuid, :search_up)
259             end
260
261             ContainerRequest.readable_by(*@read_users).where(log_uuid: uuid).each do |cr|
262               search_edges(visited, cr.uuid, :search_up)
263             end
264           elsif direction == :search_down
265             search_edges(visited, c.portable_data_hash, :search_down)
266           end
267         end
268       elsif rsc != nil
269         rsc.where(uuid: uuid).each do |r|
270           visited[uuid] = r.as_api_response
271         end
272       end
273     end
274
275     if direction == :search_up
276       # Search for provenance links pointing to the current uuid
277       Link.readable_by(*@read_users).
278         where(head_uuid: uuid, link_class: "provenance").
279         each do |link|
280         visited[link.uuid] = link.as_api_response
281         search_edges(visited, link.tail_uuid, direction)
282       end
283     elsif direction == :search_down
284       # Search for provenance links emanating from the current uuid
285       Link.readable_by(current_user).
286         where(tail_uuid: uuid, link_class: "provenance").
287         each do |link|
288         visited[link.uuid] = link.as_api_response
289         search_edges(visited, link.head_uuid, direction)
290       end
291     end
292   end
293
294   def provenance
295     visited = {}
296     if @object[:uuid]
297       search_edges(visited, @object[:uuid], :search_up)
298     else
299       search_edges(visited, @object[:portable_data_hash], :search_up)
300     end
301     send_json visited
302   end
303
304   def used_by
305     visited = {}
306     if @object[:uuid]
307       search_edges(visited, @object[:uuid], :search_down)
308     else
309       search_edges(visited, @object[:portable_data_hash], :search_down)
310     end
311     send_json visited
312   end
313
314   protected
315
316   def load_limit_offset_order_params *args
317     super
318     if action_name == 'index'
319       # Omit manifest_text and unsigned_manifest_text from index results unless expressly selected.
320       @select ||= model_class.selectable_attributes - ["manifest_text", "unsigned_manifest_text"]
321     end
322   end
323 end