5037: Merge branch 'master' into 5037-nonocache
[arvados.git] / services / api / app / controllers / arvados / v1 / collections_controller.rb
1 require "arvados/keep"
2
3 class Arvados::V1::CollectionsController < ApplicationController
4   def create
5     if resource_attrs[:uuid] and (loc = Keep::Locator.parse(resource_attrs[:uuid]))
6       resource_attrs[:portable_data_hash] = loc.to_s
7       resource_attrs.delete :uuid
8     end
9     super
10   end
11
12   def find_object_by_uuid
13     if loc = Keep::Locator.parse(params[:id])
14       loc.strip_hints!
15       if c = Collection.readable_by(*@read_users).where({ portable_data_hash: loc.to_s }).limit(1).first
16         @object = {
17           uuid: c.portable_data_hash,
18           portable_data_hash: c.portable_data_hash,
19           manifest_text: c.signed_manifest_text,
20         }
21       end
22     else
23       super
24     end
25     true
26   end
27
28   def show
29     if @object.is_a? Collection
30       super
31     else
32       send_json @object
33     end
34   end
35
36   def find_collections(visited, sp, &b)
37     case sp
38     when ArvadosModel
39       sp.class.columns.each do |c|
40         find_collections(visited, sp[c.name.to_sym], &b) if c.name != "log"
41       end
42     when Hash
43       sp.each do |k, v|
44         find_collections(visited, v, &b)
45       end
46     when Array
47       sp.each do |v|
48         find_collections(visited, v, &b)
49       end
50     when String
51       if m = /[a-f0-9]{32}\+\d+/.match(sp)
52         yield m[0], nil
53       elsif m = Collection.uuid_regex.match(sp)
54         yield nil, m[0]
55       end
56     end
57   end
58
59   def search_edges(visited, uuid, direction)
60     if uuid.nil? or uuid.empty? or visited[uuid]
61       return
62     end
63
64     if loc = Keep::Locator.parse(uuid)
65       loc.strip_hints!
66       return if visited[loc.to_s]
67     end
68
69     logger.debug "visiting #{uuid}"
70
71     if loc
72       # uuid is a portable_data_hash
73       collections = Collection.readable_by(*@read_users).where(portable_data_hash: loc.to_s)
74       c = collections.limit(2).all
75       if c.size == 1
76         visited[loc.to_s] = c[0]
77       elsif c.size > 1
78         name = collections.limit(1).where("name <> ''").first
79         if name
80           visited[loc.to_s] = {
81             portable_data_hash: c[0].portable_data_hash,
82             name: "#{name.name} + #{collections.count-1} more"
83           }
84         else
85           visited[loc.to_s] = {
86             portable_data_hash: c[0].portable_data_hash,
87             name: loc.to_s
88           }
89         end
90       end
91
92       if direction == :search_up
93         # Search upstream for jobs where this locator is the output of some job
94         Job.readable_by(*@read_users).where(output: loc.to_s).each do |job|
95           search_edges(visited, job.uuid, :search_up)
96         end
97
98         Job.readable_by(*@read_users).where(log: loc.to_s).each do |job|
99           search_edges(visited, job.uuid, :search_up)
100         end
101       elsif direction == :search_down
102         if loc.to_s == "d41d8cd98f00b204e9800998ecf8427e+0"
103           # Special case, don't follow the empty collection.
104           return
105         end
106
107         # Search downstream for jobs where this locator is in script_parameters
108         Job.readable_by(*@read_users).where(["jobs.script_parameters like ?", "%#{loc.to_s}%"]).each do |job|
109           search_edges(visited, job.uuid, :search_down)
110         end
111
112         Job.readable_by(*@read_users).where(["jobs.docker_image_locator = ?", "#{loc.to_s}"]).each do |job|
113           search_edges(visited, job.uuid, :search_down)
114         end
115       end
116     else
117       # uuid is a regular Arvados UUID
118       rsc = ArvadosModel::resource_class_for_uuid uuid
119       if rsc == Job
120         Job.readable_by(*@read_users).where(uuid: uuid).each do |job|
121           visited[uuid] = job.as_api_response
122           if direction == :search_up
123             # Follow upstream collections referenced in the script parameters
124             find_collections(visited, job) do |hash, uuid|
125               search_edges(visited, hash, :search_up) if hash
126               search_edges(visited, uuid, :search_up) if uuid
127             end
128           elsif direction == :search_down
129             # Follow downstream job output
130             search_edges(visited, job.output, direction)
131           end
132         end
133       elsif rsc == Collection
134         if c = Collection.readable_by(*@read_users).where(uuid: uuid).limit(1).first
135           search_edges(visited, c.portable_data_hash, direction)
136           visited[c.portable_data_hash] = c.as_api_response
137         end
138       elsif rsc != nil
139         rsc.where(uuid: uuid).each do |r|
140           visited[uuid] = r.as_api_response
141         end
142       end
143     end
144
145     if direction == :search_up
146       # Search for provenance links pointing to the current uuid
147       Link.readable_by(*@read_users).
148         where(head_uuid: uuid, link_class: "provenance").
149         each do |link|
150         visited[link.uuid] = link.as_api_response
151         search_edges(visited, link.tail_uuid, direction)
152       end
153     elsif direction == :search_down
154       # Search for provenance links emanating from the current uuid
155       Link.readable_by(current_user).
156         where(tail_uuid: uuid, link_class: "provenance").
157         each do |link|
158         visited[link.uuid] = link.as_api_response
159         search_edges(visited, link.head_uuid, direction)
160       end
161     end
162   end
163
164   def provenance
165     visited = {}
166     search_edges(visited, @object[:portable_data_hash], :search_up)
167     search_edges(visited, @object[:uuid], :search_up)
168     send_json visited
169   end
170
171   def used_by
172     visited = {}
173     search_edges(visited, @object[:uuid], :search_down)
174     search_edges(visited, @object[:portable_data_hash], :search_down)
175     send_json visited
176   end
177
178   protected
179
180   def load_limit_offset_order_params *args
181     if action_name == 'index'
182       # Omit manifest_text from index results unless expressly selected.
183       @select ||= model_class.selectable_attributes - ["manifest_text"]
184     end
185     super
186   end
187 end