3036: Merge branch 'master' into 3036-mutable-collections
[arvados.git] / services / api / app / models / collection.rb
1 class Collection < ArvadosModel
2   include HasUuid
3   include KindAndEtag
4   include CommonApiTemplate
5
6   api_accessible :user, extend: :common do |t|
7     t.add :data_size
8     t.add :files
9   end
10
11   api_accessible :with_data, extend: :user do |t|
12     t.add :manifest_text
13   end
14
15   def redundancy_status
16     if redundancy_confirmed_as.nil?
17       'unconfirmed'
18     elsif redundancy_confirmed_as < redundancy
19       'degraded'
20     else
21       if redundancy_confirmed_at.nil?
22         'unconfirmed'
23       elsif Time.now - redundancy_confirmed_at < 7.days
24         'OK'
25       else
26         'stale'
27       end
28     end
29   end
30
31   def assign_uuid
32     if not self.manifest_text
33       errors.add :manifest_text, 'not supplied'
34       return false
35     end
36     expect_uuid = Digest::MD5.hexdigest(self.manifest_text)
37     if self.uuid
38       self.uuid.gsub! /\+.*/, ''
39       if self.uuid != expect_uuid
40         errors.add :uuid, 'must match checksum of manifest_text'
41         return false
42       end
43     else
44       self.uuid = expect_uuid
45     end
46     self.uuid.gsub! /$/, '+' + self.manifest_text.length.to_s
47     true
48   end
49
50   # TODO (#3036/tom) replace above assign_uuid method with below assign_uuid and self.generate_uuid
51   # def assign_uuid
52   #   # Even admins cannot assign collection uuids.
53   #   self.uuid = self.class.generate_uuid
54   # end
55   # def self.generate_uuid
56   #   # The last 10 characters of a collection uuid are the last 10
57   #   # characters of the base-36 SHA256 digest of manifest_text.
58   #   [Server::Application.config.uuid_prefix,
59   #    self.uuid_prefix,
60   #    rand(2**256).to_s(36)[-5..-1] + Digest::SHA256.hexdigest(self.manifest_text).to_i(16).to_s(36)[-10..-1],
61   #   ].join '-'
62   # end
63
64   def data_size
65     inspect_manifest_text if @data_size.nil? or manifest_text_changed?
66     @data_size
67   end
68
69   def files
70     inspect_manifest_text if @files.nil? or manifest_text_changed?
71     @files
72   end
73
74   def inspect_manifest_text
75     if !manifest_text
76       @data_size = false
77       @files = []
78       return
79     end
80
81     @data_size = 0
82     tmp = {}
83
84     manifest_text.split("\n").each do |stream|
85       toks = stream.split(" ")
86
87       stream = toks[0].gsub /\\(\\|[0-7]{3})/ do |escape_sequence|
88         case $1
89         when '\\' '\\'
90         else $1.to_i(8).chr
91         end
92       end
93
94       toks[1..-1].each do |tok|
95         if (re = tok.match /^[0-9a-f]{32}/)
96           blocksize = nil
97           tok.split('+')[1..-1].each do |hint|
98             if !blocksize and hint.match /^\d+$/
99               blocksize = hint.to_i
100             end
101             if (re = hint.match /^GS(\d+)$/)
102               blocksize = re[1].to_i
103             end
104           end
105           @data_size = false if !blocksize
106           @data_size += blocksize if @data_size
107         else
108           if (re = tok.match /^(\d+):(\d+):(\S+)$/)
109             filename = re[3].gsub /\\(\\|[0-7]{3})/ do |escape_sequence|
110               case $1
111               when '\\' '\\'
112               else $1.to_i(8).chr
113               end
114             end
115             fn = stream + '/' + filename
116             i = re[2].to_i
117             if tmp[fn]
118               tmp[fn] += i
119             else
120               tmp[fn] = i
121             end
122           end
123         end
124       end
125     end
126
127     @files = []
128     tmp.each do |k, v|
129       re = k.match(/^(.+)\/(.+)/)
130       @files << [re[1], re[2], v]
131     end
132   end
133
134   def self.uuid_like_pattern
135     "________________________________+%"
136   end
137
138   def self.normalize_uuid uuid
139     hash_part = nil
140     size_part = nil
141     uuid.split('+').each do |token|
142       if token.match /^[0-9a-f]{32,}$/
143         raise "uuid #{uuid} has multiple hash parts" if hash_part
144         hash_part = token
145       elsif token.match /^\d+$/
146         raise "uuid #{uuid} has multiple size parts" if size_part
147         size_part = token
148       end
149     end
150     raise "uuid #{uuid} has no hash part" if !hash_part
151     [hash_part, size_part].compact.join '+'
152   end
153
154   def self.uuids_for_docker_image(search_term, search_tag=nil, readers=nil)
155     readers ||= [Thread.current[:user]]
156     base_search = Link.
157       readable_by(*readers).
158       readable_by(*readers, table_name: "collections").
159       joins("JOIN collections ON links.head_uuid = collections.uuid").
160       order("links.created_at DESC")
161
162     # If the search term is a Collection locator with an associated
163     # Docker image hash link, return that Collection.
164     coll_matches = base_search.
165       where(link_class: "docker_image_hash", collections: {uuid: search_term})
166     if match = coll_matches.first
167       return [match.head_uuid]
168     end
169
170     # Find Collections with matching Docker image repository+tag pairs.
171     matches = base_search.
172       where(link_class: "docker_image_repo+tag",
173             name: "#{search_term}:#{search_tag || 'latest'}")
174
175     # If that didn't work, find Collections with matching Docker image hashes.
176     if matches.empty?
177       matches = base_search.
178         where("link_class = ? and name LIKE ?",
179               "docker_image_hash", "#{search_term}%")
180     end
181
182     # Generate an order key for each result.  We want to order the results
183     # so that anything with an image timestamp is considered more recent than
184     # anything without; then we use the link's created_at as a tiebreaker.
185     uuid_timestamps = {}
186     matches.find_each do |link|
187       uuid_timestamps[link.head_uuid] =
188         [(-link.properties["image_timestamp"].to_datetime.to_i rescue 0),
189          -link.created_at.to_i]
190     end
191     uuid_timestamps.keys.sort_by { |uuid| uuid_timestamps[uuid] }
192   end
193
194   def self.for_latest_docker_image(search_term, search_tag=nil, readers=nil)
195     image_uuid = uuids_for_docker_image(search_term, search_tag, readers).first
196     if image_uuid.nil?
197       nil
198     else
199       find_by_uuid(image_uuid)
200     end
201   end
202 end