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