1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: AGPL-3.0
8 class Arvados::V1::CollectionsController < ApplicationController
10 include TrashableController
12 def self._index_requires_parameters
16 type: 'boolean', required: false, description: "Include collections whose is_trashed attribute is true."
18 include_old_versions: {
19 type: 'boolean', required: false, description: "Include past collection versions."
24 def self._show_requires_parameters
28 type: 'boolean', required: false, description: "Show collection even if its is_trashed attribute is true."
30 include_old_versions: {
31 type: 'boolean', required: false, default: true, description: "Include past collection versions.",
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
41 resource_attrs.delete :version
42 resource_attrs.delete :current_version_uuid
46 def find_objects_for_index
48 include_trash: params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name),
49 include_old_versions: params[:include_old_versions] || false,
51 @objects = Collection.readable_by(*@read_users, opts) if !opts.empty?
55 def find_object_by_uuid
56 if loc = Keep::Locator.parse(params[:id])
60 include_trash: params[:include_trash],
61 include_old_versions: params[:include_old_versions],
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.
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."
71 # "trash_at desc" sorts null first, then latest to earliest, so
72 # it will select the Collection object with the longest
75 if c = Collection.readable_by(*@read_users, opts).where({ portable_data_hash: loc.to_s }).order("trash_at desc").limit(1).first
77 uuid: c.portable_data_hash,
78 portable_data_hash: c.portable_data_hash,
79 manifest_text: c.signed_manifest_text,
88 if @object.is_a? Collection
89 # Omit unsigned_manifest_text
90 @select ||= model_class.selectable_attributes - ["unsigned_manifest_text"]
98 def find_collections(visited, sp, ignore_columns=[], &b)
101 sp.class.columns.each do |c|
102 find_collections(visited, sp[c.name.to_sym], &b) if !ignore_columns.include?(c.name)
106 find_collections(visited, v, &b)
110 find_collections(visited, v, &b)
113 if m = /[a-f0-9]{32}\+\d+/.match(sp)
115 elsif m = Collection.uuid_regex.match(sp)
121 def search_edges(visited, uuid, direction)
122 if uuid.nil? or uuid.empty? or visited[uuid]
126 if loc = Keep::Locator.parse(uuid)
128 return if visited[loc.to_s]
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
136 visited[loc.to_s] = c[0]
138 name = collections.limit(1).where("name <> ''").first
140 visited[loc.to_s] = {
141 portable_data_hash: c[0].portable_data_hash,
142 name: "#{name.name} + #{collections.count-1} more"
145 visited[loc.to_s] = {
146 portable_data_hash: c[0].portable_data_hash,
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)
159 Job.readable_by(*@read_users).where(log: loc.to_s).each do |job|
160 search_edges(visited, job.uuid, :search_up)
164 Container.readable_by(*@read_users).where(output: loc.to_s).each do |c|
165 search_edges(visited, c.uuid, :search_up)
168 Container.readable_by(*@read_users).where(log: loc.to_s).each do |c|
169 search_edges(visited, c.uuid, :search_up)
171 elsif direction == :search_down
172 if loc.to_s == "d41d8cd98f00b204e9800998ecf8427e+0"
173 # Special case, don't follow the empty collection.
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)
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)
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)
195 # uuid is a regular Arvados UUID
196 rsc = ArvadosModel::resource_class_for_uuid uuid
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
206 elsif direction == :search_down
207 # Follow downstream job output
208 search_edges(visited, job.output, direction)
211 elsif rsc == Container
212 c = Container.readable_by(*@read_users).where(uuid: uuid).limit(1).first
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
221 elsif direction == :search_down
222 # Follow downstream job output
223 search_edges(visited, c.output, :search_down)
226 elsif rsc == ContainerRequest
227 c = ContainerRequest.readable_by(*@read_users).where(uuid: uuid).limit(1).first
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
236 elsif direction == :search_down
237 # Follow downstream job output
238 search_edges(visited, c.output_uuid, :search_down)
241 elsif rsc == Collection
242 c = Collection.readable_by(*@read_users).where(uuid: uuid).limit(1).first
244 if direction == :search_up
245 visited[c.uuid] = c.as_api_response
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)
252 Job.readable_by(*@read_users).where(log: c.portable_data_hash).each do |job|
253 search_edges(visited, job.uuid, :search_up)
257 ContainerRequest.readable_by(*@read_users).where(output_uuid: uuid).each do |cr|
258 search_edges(visited, cr.uuid, :search_up)
261 ContainerRequest.readable_by(*@read_users).where(log_uuid: uuid).each do |cr|
262 search_edges(visited, cr.uuid, :search_up)
264 elsif direction == :search_down
265 search_edges(visited, c.portable_data_hash, :search_down)
269 rsc.where(uuid: uuid).each do |r|
270 visited[uuid] = r.as_api_response
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").
280 visited[link.uuid] = link.as_api_response
281 search_edges(visited, link.tail_uuid, direction)
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").
288 visited[link.uuid] = link.as_api_response
289 search_edges(visited, link.head_uuid, direction)
297 search_edges(visited, @object[:uuid], :search_up)
299 search_edges(visited, @object[:portable_data_hash], :search_up)
307 search_edges(visited, @object[:uuid], :search_down)
309 search_edges(visited, @object[:portable_data_hash], :search_down)
316 def load_limit_offset_order_params *args
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"]