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