1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6
7from extensions_paths import APP_YAML
8
9
10_APP_YAML_CONTAINER = '''
11application: chrome-apps-doc
12version: %s
13runtime: python27
14api_version: 1
15threadsafe: false
16'''
17
18
19class AppYamlHelper(object):
20  '''Parses the app.yaml file, and is able to step back in the host file
21  system's revision history to find when it changed to some given version.
22  '''
23  def __init__(self,
24               object_store_creator,
25               host_file_system_provider):
26    self._store = object_store_creator.Create(
27        AppYamlHelper,
28        category=host_file_system_provider.GetMaster().GetIdentity(),
29        start_empty=False)
30    self._host_file_system_provider = host_file_system_provider
31
32  @staticmethod
33  def ExtractVersion(app_yaml, key='version'):
34    '''Extracts the 'version' key from the contents of an app.yaml file.
35    Allow overriding the key to parse e.g. the cron file ('target').
36    '''
37    # We could properly parse this using a yaml library but Python doesn't have
38    # one built in so whatevs.
39    key_colon = '%s:' % key
40    versions = [line.strip()[len(key_colon):].strip()
41                for line in app_yaml.split('\n')
42                if line.strip().startswith(key_colon)]
43    if not versions:
44      raise ValueError('No versions found for %s in %s' % (
45          key, app_yaml))
46    if len(set(versions)) > 1:
47      raise ValueError('Inconsistent versions found for %s in %s: %s' % (
48          key, app_yaml, versions))
49    return versions[0]
50
51  @staticmethod
52  def IsGreater(lhs, rhs):
53    '''Return whether the app.yaml version |lhs| > |rhs|. This is tricky
54    because versions are typically not numbers but rather 2-0-9, 2-0-12,
55    2-1-0, etc - and 2-1-0 > 2-0-10 > 2-0-9.
56    '''
57    lhs_parts = lhs.replace('-', '.').split('.')
58    rhs_parts = rhs.replace('-', '.').split('.')
59    while lhs_parts and rhs_parts:
60      lhs_msb = int(lhs_parts.pop(0))
61      rhs_msb = int(rhs_parts.pop(0))
62      if lhs_msb != rhs_msb:
63        return lhs_msb > rhs_msb
64    return len(lhs) > len(rhs)
65
66  @staticmethod
67  def GenerateAppYaml(version):
68    '''Probably only useful for tests.
69    '''
70    return _APP_YAML_CONTAINER % version
71
72  def IsUpToDate(self, app_version):
73    '''Returns True if the |app_version| is up to date with respect to the one
74    checked into the host file system.
75    '''
76    checked_in_app_version = AppYamlHelper.ExtractVersion(
77        self._host_file_system_provider.GetMaster().ReadSingle(APP_YAML).Get())
78    if app_version == checked_in_app_version:
79      return True
80    if AppYamlHelper.IsGreater(app_version, checked_in_app_version):
81      logging.warning(
82          'Server is too new! Checked in %s < currently running %s' % (
83              checked_in_app_version, app_version))
84      return True
85    return False
86
87  def GetFirstRevisionGreaterThan(self, app_version):
88    '''Finds the first revision that the version in app.yaml was greater than
89    |app_version|.
90
91    WARNING: if there is no such revision (e.g. the app is up to date, or
92    *oops* the app is even newer) then this will throw a ValueError. Use
93    IsUpToDate to validate the input before calling this method.
94    '''
95    stored = self._store.Get(app_version).Get()
96    if stored is None:
97      stored = self._GetFirstRevisionGreaterThanImpl(app_version)
98      assert stored is not None
99      self._store.Set(app_version, stored)
100    return stored
101
102  def _GetFirstRevisionGreaterThanImpl(self, app_version):
103    # XXX(ahernandez): Tricky. The 'version' of app.yaml coming from
104    # GitilesFileSystem is a blob ID.
105    def get_app_yaml_revision(file_system):
106      return int(file_system.Stat(APP_YAML).version)
107
108    def has_greater_app_version(file_system):
109      app_version_in_file_system = AppYamlHelper.ExtractVersion(
110          file_system.ReadSingle(APP_YAML).Get())
111      return AppYamlHelper.IsGreater(app_version_in_file_system, app_version)
112
113    found = None
114    next_file_system = self._host_file_system_provider.GetMaster()
115
116    while has_greater_app_version(next_file_system):
117      found = get_app_yaml_revision(next_file_system)
118      # Back up a revision then find when app.yaml was last updated before then.
119      if found == 0:
120        logging.warning('All revisions are greater than %s' % app_version)
121        return 0
122      next_file_system = self._host_file_system_provider.GetMaster(
123          commit=next_file_system.GetPreviousCommitID().Get())
124
125    if found is None:
126      raise ValueError('All revisions are less than %s' % app_version)
127    return found
128