8784: Fix test for latest firefox.
[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.update_attributes!(trash_at: nil)
79     else
80       raise InvalidStateTransitionError
81     end
82     show
83   end
84
85   def find_collections(visited, sp, &b)
86     case sp
87     when ArvadosModel
88       sp.class.columns.each do |c|
89         find_collections(visited, sp[c.name.to_sym], &b) if c.name != "log"
90       end
91     when Hash
92       sp.each do |k, v|
93         find_collections(visited, v, &b)
94       end
95     when Array
96       sp.each do |v|
97         find_collections(visited, v, &b)
98       end
99     when String
100       if m = /[a-f0-9]{32}\+\d+/.match(sp)
101         yield m[0], nil
102       elsif m = Collection.uuid_regex.match(sp)
103         yield nil, m[0]
104       end
105     end
106   end
107
108   def search_edges(visited, uuid, direction)
109     if uuid.nil? or uuid.empty? or visited[uuid]
110       return
111     end
112
113     if loc = Keep::Locator.parse(uuid)
114       loc.strip_hints!
115       return if visited[loc.to_s]
116     end
117
118     logger.debug "visiting #{uuid}"
119
120     if loc
121       # uuid is a portable_data_hash
122       collections = Collection.readable_by(*@read_users).where(portable_data_hash: loc.to_s)
123       c = collections.limit(2).all
124       if c.size == 1
125         visited[loc.to_s] = c[0]
126       elsif c.size > 1
127         name = collections.limit(1).where("name <> ''").first
128         if name
129           visited[loc.to_s] = {
130             portable_data_hash: c[0].portable_data_hash,
131             name: "#{name.name} + #{collections.count-1} more"
132           }
133         else
134           visited[loc.to_s] = {
135             portable_data_hash: c[0].portable_data_hash,
136             name: loc.to_s
137           }
138         end
139       end
140
141       if direction == :search_up
142         # Search upstream for jobs where this locator is the output of some job
143         Job.readable_by(*@read_users).where(output: loc.to_s).each do |job|
144           search_edges(visited, job.uuid, :search_up)
145         end
146
147         Job.readable_by(*@read_users).where(log: loc.to_s).each do |job|
148           search_edges(visited, job.uuid, :search_up)
149         end
150       elsif direction == :search_down
151         if loc.to_s == "d41d8cd98f00b204e9800998ecf8427e+0"
152           # Special case, don't follow the empty collection.
153           return
154         end
155
156         # Search downstream for jobs where this locator is in script_parameters
157         Job.readable_by(*@read_users).where(["jobs.script_parameters like ?", "%#{loc.to_s}%"]).each do |job|
158           search_edges(visited, job.uuid, :search_down)
159         end
160
161         Job.readable_by(*@read_users).where(["jobs.docker_image_locator = ?", "#{loc.to_s}"]).each do |job|
162           search_edges(visited, job.uuid, :search_down)
163         end
164       end
165     else
166       # uuid is a regular Arvados UUID
167       rsc = ArvadosModel::resource_class_for_uuid uuid
168       if rsc == Job
169         Job.readable_by(*@read_users).where(uuid: uuid).each do |job|
170           visited[uuid] = job.as_api_response
171           if direction == :search_up
172             # Follow upstream collections referenced in the script parameters
173             find_collections(visited, job) do |hash, col_uuid|
174               search_edges(visited, hash, :search_up) if hash
175               search_edges(visited, col_uuid, :search_up) if col_uuid
176             end
177           elsif direction == :search_down
178             # Follow downstream job output
179             search_edges(visited, job.output, direction)
180           end
181         end
182       elsif rsc == Collection
183         if c = Collection.readable_by(*@read_users).where(uuid: uuid).limit(1).first
184           search_edges(visited, c.portable_data_hash, direction)
185           visited[c.portable_data_hash] = c.as_api_response
186         end
187       elsif rsc != nil
188         rsc.where(uuid: uuid).each do |r|
189           visited[uuid] = r.as_api_response
190         end
191       end
192     end
193
194     if direction == :search_up
195       # Search for provenance links pointing to the current uuid
196       Link.readable_by(*@read_users).
197         where(head_uuid: uuid, link_class: "provenance").
198         each do |link|
199         visited[link.uuid] = link.as_api_response
200         search_edges(visited, link.tail_uuid, direction)
201       end
202     elsif direction == :search_down
203       # Search for provenance links emanating from the current uuid
204       Link.readable_by(current_user).
205         where(tail_uuid: uuid, link_class: "provenance").
206         each do |link|
207         visited[link.uuid] = link.as_api_response
208         search_edges(visited, link.head_uuid, direction)
209       end
210     end
211   end
212
213   def provenance
214     visited = {}
215     search_edges(visited, @object[:portable_data_hash], :search_up)
216     search_edges(visited, @object[:uuid], :search_up)
217     send_json visited
218   end
219
220   def used_by
221     visited = {}
222     search_edges(visited, @object[:uuid], :search_down)
223     search_edges(visited, @object[:portable_data_hash], :search_down)
224     send_json visited
225   end
226
227   protected
228
229   def load_limit_offset_order_params *args
230     super
231     if action_name == 'index'
232       # Omit manifest_text and unsigned_manifest_text from index results unless expressly selected.
233       @select ||= model_class.selectable_attributes - ["manifest_text", "unsigned_manifest_text"]
234     end
235   end
236 end