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