11870: minor update
[arvados.git] / services / api / app / controllers / arvados / v1 / collections_controller.rb
1 require "arvados/keep"
2
3 class Arvados::V1::CollectionsController < ApplicationController
4   include DbCurrentTime
5
6   def self._index_requires_parameters
7     (super rescue {}).
8       merge({
9         include_trash: {
10           type: 'boolean', required: false, description: "Include collections whose is_trashed attribute is true."
11         },
12       })
13   end
14
15
16   def create
17     if resource_attrs[:uuid] and (loc = Keep::Locator.parse(resource_attrs[:uuid]))
18       resource_attrs[:portable_data_hash] = loc.to_s
19       resource_attrs.delete :uuid
20     end
21     super
22   end
23
24   def find_objects_for_index
25     if params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name)
26       @objects = Collection.unscoped.readable_by(*@read_users)
27     end
28     super
29   end
30
31   def find_object_by_uuid
32     if loc = Keep::Locator.parse(params[:id])
33       loc.strip_hints!
34       if c = Collection.readable_by(*@read_users).where({ portable_data_hash: loc.to_s }).limit(1).first
35         @object = {
36           uuid: c.portable_data_hash,
37           portable_data_hash: c.portable_data_hash,
38           manifest_text: c.signed_manifest_text,
39         }
40       end
41       true
42     else
43       super
44     end
45   end
46
47   def show
48     if @object.is_a? Collection
49       # Omit unsigned_manifest_text
50       @select ||= model_class.selectable_attributes - ["unsigned_manifest_text"]
51       super
52     else
53       send_json @object
54     end
55   end
56
57   def destroy
58     if !@object.is_trashed
59       @object.update_attributes!(trash_at: db_current_time)
60     end
61     earliest_delete = (@object.trash_at +
62                        Rails.configuration.blob_signature_ttl.seconds)
63     if @object.delete_at > earliest_delete
64       @object.update_attributes!(delete_at: earliest_delete)
65     end
66     show
67   end
68
69   def trash
70     if !@object.is_trashed
71       @object.update_attributes!(trash_at: db_current_time)
72     end
73     show
74   end
75
76   def untrash
77     if @object.is_trashed
78       @object.trash_at = nil
79
80       if params[:ensure_unique_name]
81         @object.save_with_unique_name!
82       else
83         @object.save!
84       end
85     else
86       raise InvalidStateTransitionError
87     end
88     show
89   end
90
91   def find_collections(visited, sp, &b)
92     case sp
93     when ArvadosModel
94       sp.class.columns.each do |c|
95         find_collections(visited, sp[c.name.to_sym], &b) if c.name != "log"
96       end
97     when Hash
98       sp.each do |k, v|
99         find_collections(visited, v, &b)
100       end
101     when Array
102       sp.each do |v|
103         find_collections(visited, v, &b)
104       end
105     when String
106       if m = /[a-f0-9]{32}\+\d+/.match(sp)
107         yield m[0], nil
108       elsif m = Collection.uuid_regex.match(sp)
109         yield nil, m[0]
110       end
111     end
112   end
113
114   def search_edges(visited, uuid, direction)
115     if uuid.nil? or uuid.empty? or visited[uuid]
116       return
117     end
118
119     if loc = Keep::Locator.parse(uuid)
120       loc.strip_hints!
121       return if visited[loc.to_s]
122     end
123
124     logger.debug "visiting #{uuid}"
125
126     if loc
127       # uuid is a portable_data_hash
128       collections = Collection.readable_by(*@read_users).where(portable_data_hash: loc.to_s)
129       c = collections.limit(2).all
130       if c.size == 1
131         visited[loc.to_s] = c[0]
132       elsif c.size > 1
133         name = collections.limit(1).where("name <> ''").first
134         if name
135           visited[loc.to_s] = {
136             portable_data_hash: c[0].portable_data_hash,
137             name: "#{name.name} + #{collections.count-1} more"
138           }
139         else
140           visited[loc.to_s] = {
141             portable_data_hash: c[0].portable_data_hash,
142             name: loc.to_s
143           }
144         end
145       end
146
147       if direction == :search_up
148         # Search upstream for jobs where this locator is the output of some job
149         Job.readable_by(*@read_users).where(output: loc.to_s).each do |job|
150           search_edges(visited, job.uuid, :search_up)
151         end
152
153         Job.readable_by(*@read_users).where(log: loc.to_s).each do |job|
154           search_edges(visited, job.uuid, :search_up)
155         end
156       elsif direction == :search_down
157         if loc.to_s == "d41d8cd98f00b204e9800998ecf8427e+0"
158           # Special case, don't follow the empty collection.
159           return
160         end
161
162         # Search downstream for jobs where this locator is in script_parameters
163         Job.readable_by(*@read_users).where(["jobs.script_parameters like ?", "%#{loc.to_s}%"]).each do |job|
164           search_edges(visited, job.uuid, :search_down)
165         end
166
167         Job.readable_by(*@read_users).where(["jobs.docker_image_locator = ?", "#{loc.to_s}"]).each do |job|
168           search_edges(visited, job.uuid, :search_down)
169         end
170       end
171     else
172       # uuid is a regular Arvados UUID
173       rsc = ArvadosModel::resource_class_for_uuid uuid
174       if rsc == Job
175         Job.readable_by(*@read_users).where(uuid: uuid).each do |job|
176           visited[uuid] = job.as_api_response
177           if direction == :search_up
178             # Follow upstream collections referenced in the script parameters
179             find_collections(visited, job) do |hash, col_uuid|
180               search_edges(visited, hash, :search_up) if hash
181               search_edges(visited, col_uuid, :search_up) if col_uuid
182             end
183           elsif direction == :search_down
184             # Follow downstream job output
185             search_edges(visited, job.output, direction)
186           end
187         end
188       elsif rsc == Collection
189         if c = Collection.readable_by(*@read_users).where(uuid: uuid).limit(1).first
190           search_edges(visited, c.portable_data_hash, direction)
191           visited[c.portable_data_hash] = c.as_api_response
192         end
193       elsif rsc != nil
194         rsc.where(uuid: uuid).each do |r|
195           visited[uuid] = r.as_api_response
196         end
197       end
198     end
199
200     if direction == :search_up
201       # Search for provenance links pointing to the current uuid
202       Link.readable_by(*@read_users).
203         where(head_uuid: uuid, link_class: "provenance").
204         each do |link|
205         visited[link.uuid] = link.as_api_response
206         search_edges(visited, link.tail_uuid, direction)
207       end
208     elsif direction == :search_down
209       # Search for provenance links emanating from the current uuid
210       Link.readable_by(current_user).
211         where(tail_uuid: uuid, link_class: "provenance").
212         each do |link|
213         visited[link.uuid] = link.as_api_response
214         search_edges(visited, link.head_uuid, direction)
215       end
216     end
217   end
218
219   def provenance
220     visited = {}
221     search_edges(visited, @object[:portable_data_hash], :search_up)
222     search_edges(visited, @object[:uuid], :search_up)
223     send_json visited
224   end
225
226   def used_by
227     visited = {}
228     search_edges(visited, @object[:uuid], :search_down)
229     search_edges(visited, @object[:portable_data_hash], :search_down)
230     send_json visited
231   end
232
233   protected
234
235   def load_limit_offset_order_params *args
236     super
237     if action_name == 'index'
238       # Omit manifest_text and unsigned_manifest_text from index results unless expressly selected.
239       @select ||= model_class.selectable_attributes - ["manifest_text", "unsigned_manifest_text"]
240     end
241   end
242 end