Home | History | Annotate | Download | only in server2
      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 
      5 from copy import deepcopy
      6 
      7 from file_system import FileSystem, StatInfo, FileNotFoundError
      8 from future import Future
      9 
     10 
     11 def _GetAsyncFetchCallback(unpatched_files_future,
     12                            patched_files_future,
     13                            dirs_value,
     14                            patched_file_system):
     15   def patch_directory_listing(path, original_listing):
     16     added, deleted, modified = (
     17         patched_file_system._GetDirectoryListingFromPatch(path))
     18     if original_listing is None:
     19       if len(added) == 0:
     20         raise FileNotFoundError('Directory %s not found in the patch.' % path)
     21       return added
     22     return list((set(original_listing) | set(added)) - set(deleted))
     23 
     24   def resolve():
     25     files = unpatched_files_future.Get()
     26     files.update(patched_files_future.Get())
     27     files.update(
     28         dict((path, patch_directory_listing(path, dirs_value[path]))
     29              for path in dirs_value))
     30     return files
     31 
     32   return resolve
     33 
     34 
     35 class PatchedFileSystem(FileSystem):
     36   ''' Class to fetch resources with a patch applied.
     37   '''
     38   def __init__(self, base_file_system, patcher):
     39     self._base_file_system = base_file_system
     40     self._patcher = patcher
     41 
     42   def Read(self, paths, skip_not_found=False):
     43     patched_files = set()
     44     added, deleted, modified = self._patcher.GetPatchedFiles()
     45     if set(paths) & set(deleted):
     46       def raise_file_not_found():
     47         raise FileNotFoundError('Files are removed from the patch.')
     48       return Future(callback=raise_file_not_found)
     49     patched_files |= (set(added) | set(modified))
     50     dir_paths = set(path for path in paths if path.endswith('/'))
     51     file_paths = set(paths) - dir_paths
     52     patched_paths = file_paths & patched_files
     53     unpatched_paths = file_paths - patched_files
     54     return Future(callback=_GetAsyncFetchCallback(
     55         self._base_file_system.Read(unpatched_paths,
     56                                     skip_not_found=skip_not_found),
     57         self._patcher.Apply(patched_paths, self._base_file_system),
     58         self._TryReadDirectory(dir_paths),
     59         self))
     60 
     61   def Refresh(self):
     62     return self._base_file_system.Refresh()
     63 
     64   ''' Given the list of patched files, it's not possible to determine whether
     65   a directory to read exists in self._base_file_system. So try reading each one
     66   and handle FileNotFoundError.
     67   '''
     68   def _TryReadDirectory(self, paths):
     69     value = {}
     70     for path in paths:
     71       assert path.endswith('/')
     72       try:
     73         value[path] = self._base_file_system.ReadSingle(path).Get()
     74       except FileNotFoundError:
     75         value[path] = None
     76     return value
     77 
     78   def _GetDirectoryListingFromPatch(self, path):
     79     assert path.endswith('/')
     80     def _FindChildrenInPath(files, path):
     81       result = []
     82       for f in files:
     83         if f.startswith(path):
     84           child_path = f[len(path):]
     85           if '/' in child_path:
     86             child_name = child_path[0:child_path.find('/') + 1]
     87           else:
     88             child_name = child_path
     89           result.append(child_name)
     90       return result
     91 
     92     added, deleted, modified = (tuple(
     93         _FindChildrenInPath(files, path)
     94         for files in self._patcher.GetPatchedFiles()))
     95 
     96     # A patch applies to files only. It cannot delete directories.
     97     deleted_files = [child for child in deleted if not child.endswith('/')]
     98     # However, these directories are actually modified because their children
     99     # are patched.
    100     modified += [child for child in deleted if child.endswith('/')]
    101 
    102     return (added, deleted_files, modified)
    103 
    104   def _PatchStat(self, stat_info, version, added, deleted, modified):
    105     assert len(added) + len(deleted) + len(modified) > 0
    106     assert stat_info.child_versions is not None
    107 
    108     # Deep copy before patching to make sure it doesn't interfere with values
    109     # cached in memory.
    110     stat_info = deepcopy(stat_info)
    111 
    112     stat_info.version = version
    113     for child in added + modified:
    114       stat_info.child_versions[child] = version
    115     for child in deleted:
    116       if stat_info.child_versions.get(child):
    117         del stat_info.child_versions[child]
    118 
    119     return stat_info
    120 
    121   def Stat(self, path):
    122     version = self._patcher.GetVersion()
    123     assert version is not None
    124     version = 'patched_%s' % version
    125 
    126     directory, filename = path.rsplit('/', 1)
    127     added, deleted, modified = self._GetDirectoryListingFromPatch(
    128         directory + '/')
    129 
    130     if len(added) > 0:
    131       # There are new files added. It's possible (if |directory| is new) that
    132       # self._base_file_system.Stat will throw an exception.
    133       try:
    134         stat_info = self._PatchStat(
    135             self._base_file_system.Stat(directory + '/'),
    136             version,
    137             added,
    138             deleted,
    139             modified)
    140       except FileNotFoundError:
    141         stat_info = StatInfo(
    142             version,
    143             dict((child, version) for child in added + modified))
    144     elif len(deleted) + len(modified) > 0:
    145       # No files were added.
    146       stat_info = self._PatchStat(self._base_file_system.Stat(directory + '/'),
    147                                   version,
    148                                   added,
    149                                   deleted,
    150                                   modified)
    151     else:
    152       # No changes are made in this directory.
    153       return self._base_file_system.Stat(path)
    154 
    155     if stat_info.child_versions is not None:
    156       if filename:
    157         if filename in stat_info.child_versions:
    158           stat_info = StatInfo(stat_info.child_versions[filename])
    159         else:
    160           raise FileNotFoundError('%s was not in child versions' % filename)
    161     return stat_info
    162 
    163   def GetIdentity(self):
    164     return '%s(%s,%s)' % (self.__class__.__name__,
    165                           self._base_file_system.GetIdentity(),
    166                           self._patcher.GetIdentity())
    167