Add 'sdk/java-v2/' from commit '55f103e336ca9fb8bf1720d2ef4ee8dd4e221118'
[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 create
25     if resource_attrs[:uuid] and (loc = Keep::Locator.parse(resource_attrs[:uuid]))
26       resource_attrs[:portable_data_hash] = loc.to_s
27       resource_attrs.delete :uuid
28     end
29     resource_attrs.delete :version
30     resource_attrs.delete :current_version_uuid
31     super
32   end
33
34   def find_objects_for_index
35     opts = {}
36     if params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name)
37       opts.update({include_trash: true})
38     end
39     if params[:include_old_versions] || @include_old_versions
40       opts.update({include_old_versions: true})
41     end
42     @objects = Collection.readable_by(*@read_users, opts) if !opts.empty?
43     super
44   end
45
46   def find_object_by_uuid
47     @include_old_versions = true
48
49     if loc = Keep::Locator.parse(params[:id])
50       loc.strip_hints!
51
52       # It matters which Collection object we pick because we use it to get signed_manifest_text,
53       # the value of which is affected by the value of trash_at.
54       #
55       # From postgres doc: "By default, null values sort as if larger than any non-null
56       # value; that is, NULLS FIRST is the default for DESC order, and
57       # NULLS LAST otherwise."
58       #
59       # "trash_at desc" sorts null first, then latest to earliest, so
60       # it will select the Collection object with the longest
61       # available lifetime.
62
63       if c = Collection.readable_by(*@read_users).where({ portable_data_hash: loc.to_s }).order("trash_at desc").limit(1).first
64         @object = {
65           uuid: c.portable_data_hash,
66           portable_data_hash: c.portable_data_hash,
67           manifest_text: c.signed_manifest_text,
68         }
69       end
70       true
71     else
72       super
73     end
74   end
75
76   def show
77     if @object.is_a? Collection
78       # Omit unsigned_manifest_text
79       @select ||= model_class.selectable_attributes - ["unsigned_manifest_text"]
80       super
81     else
82       send_json @object
83     end
84   end
85
86
87   def find_collections(visited, sp, &b)
88     case sp
89     when ArvadosModel
90       sp.class.columns.each do |c|
91         find_collections(visited, sp[c.name.to_sym], &b) if c.name != "log"
92       end
93     when Hash
94       sp.each do |k, v|
95         find_collections(visited, v, &b)
96       end
97     when Array
98       sp.each do |v|
99         find_collections(visited, v, &b)
100       end
101     when String
102       if m = /[a-f0-9]{32}\+\d+/.match(sp)
103         yield m[0], nil
104       elsif m = Collection.uuid_regex.match(sp)
105         yield nil, m[0]
106       end
107     end
108   end
109
110   def search_edges(visited, uuid, direction)
111     if uuid.nil? or uuid.empty? or visited[uuid]
112       return
113     end
114
115     if loc = Keep::Locator.parse(uuid)
116       loc.strip_hints!
117       return if visited[loc.to_s]
118     end
119
120     logger.debug "visiting #{uuid}"
121
122     if loc
123       # uuid is a portable_data_hash
124       collections = Collection.readable_by(*@read_users).where(portable_data_hash: loc.to_s)
125       c = collections.limit(2).all
126       if c.size == 1
127         visited[loc.to_s] = c[0]
128       elsif c.size > 1
129         name = collections.limit(1).where("name <> ''").first
130         if name
131           visited[loc.to_s] = {
132             portable_data_hash: c[0].portable_data_hash,
133             name: "#{name.name} + #{collections.count-1} more"
134           }
135         else
136           visited[loc.to_s] = {
137             portable_data_hash: c[0].portable_data_hash,
138             name: loc.to_s
139           }
140         end
141       end
142
143       if direction == :search_up
144         # Search upstream for jobs where this locator is the output of some job
145         Job.readable_by(*@read_users).where(output: loc.to_s).each do |job|
146           search_edges(visited, job.uuid, :search_up)
147         end
148
149         Job.readable_by(*@read_users).where(log: loc.to_s).each do |job|
150           search_edges(visited, job.uuid, :search_up)
151         end
152       elsif direction == :search_down
153         if loc.to_s == "d41d8cd98f00b204e9800998ecf8427e+0"
154           # Special case, don't follow the empty collection.
155           return
156         end
157
158         # Search downstream for jobs where this locator is in script_parameters
159         Job.readable_by(*@read_users).where(["jobs.script_parameters like ?", "%#{loc.to_s}%"]).each do |job|
160           search_edges(visited, job.uuid, :search_down)
161         end
162
163         Job.readable_by(*@read_users).where(["jobs.docker_image_locator = ?", "#{loc.to_s}"]).each do |job|
164           search_edges(visited, job.uuid, :search_down)
165         end
166       end
167     else
168       # uuid is a regular Arvados UUID
169       rsc = ArvadosModel::resource_class_for_uuid uuid
170       if rsc == Job
171         Job.readable_by(*@read_users).where(uuid: uuid).each do |job|
172           visited[uuid] = job.as_api_response
173           if direction == :search_up
174             # Follow upstream collections referenced in the script parameters
175             find_collections(visited, job) do |hash, col_uuid|
176               search_edges(visited, hash, :search_up) if hash
177               search_edges(visited, col_uuid, :search_up) if col_uuid
178             end
179           elsif direction == :search_down
180             # Follow downstream job output
181             search_edges(visited, job.output, direction)
182           end
183         end
184       elsif rsc == Collection
185         if c = Collection.readable_by(*@read_users).where(uuid: uuid).limit(1).first
186           search_edges(visited, c.portable_data_hash, direction)
187           visited[c.portable_data_hash] = c.as_api_response
188         end
189       elsif rsc != nil
190         rsc.where(uuid: uuid).each do |r|
191           visited[uuid] = r.as_api_response
192         end
193       end
194     end
195
196     if direction == :search_up
197       # Search for provenance links pointing to the current uuid
198       Link.readable_by(*@read_users).
199         where(head_uuid: uuid, link_class: "provenance").
200         each do |link|
201         visited[link.uuid] = link.as_api_response
202         search_edges(visited, link.tail_uuid, direction)
203       end
204     elsif direction == :search_down
205       # Search for provenance links emanating from the current uuid
206       Link.readable_by(current_user).
207         where(tail_uuid: uuid, link_class: "provenance").
208         each do |link|
209         visited[link.uuid] = link.as_api_response
210         search_edges(visited, link.head_uuid, direction)
211       end
212     end
213   end
214
215   def provenance
216     visited = {}
217     search_edges(visited, @object[:portable_data_hash], :search_up)
218     search_edges(visited, @object[:uuid], :search_up)
219     send_json visited
220   end
221
222   def used_by
223     visited = {}
224     search_edges(visited, @object[:uuid], :search_down)
225     search_edges(visited, @object[:portable_data_hash], :search_down)
226     send_json visited
227   end
228
229   protected
230
231   def load_limit_offset_order_params *args
232     super
233     if action_name == 'index'
234       # Omit manifest_text and unsigned_manifest_text from index results unless expressly selected.
235       @select ||= model_class.selectable_attributes - ["manifest_text", "unsigned_manifest_text"]
236     end
237   end
238 end