| # Copyright 2013 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from copy import deepcopy |
| |
| from file_system import FileSystem, StatInfo, FileNotFoundError |
| from future import Gettable, Future |
| |
| class _AsyncFetchFuture(object): |
| def __init__(self, |
| unpatched_files_future, |
| patched_files_future, |
| dirs_value, |
| patched_file_system): |
| self._unpatched_files_future = unpatched_files_future |
| self._patched_files_future = patched_files_future |
| self._dirs_value = dirs_value |
| self._patched_file_system = patched_file_system |
| |
| def Get(self): |
| files = self._unpatched_files_future.Get() |
| files.update(self._patched_files_future.Get()) |
| files.update( |
| dict((path, self._PatchDirectoryListing(path, self._dirs_value[path])) |
| for path in self._dirs_value)) |
| return files |
| |
| def _PatchDirectoryListing(self, path, original_listing): |
| added, deleted, modified = ( |
| self._patched_file_system._GetDirectoryListingFromPatch(path)) |
| if original_listing is None: |
| if len(added) == 0: |
| raise FileNotFoundError('Directory %s not found in the patch.' % path) |
| return added |
| return list((set(original_listing) | set(added)) - set(deleted)) |
| |
| class PatchedFileSystem(FileSystem): |
| ''' Class to fetch resources with a patch applied. |
| ''' |
| def __init__(self, base_file_system, patcher): |
| self._base_file_system = base_file_system |
| self._patcher = patcher |
| |
| def Read(self, paths, binary=False): |
| patched_files = set() |
| added, deleted, modified = self._patcher.GetPatchedFiles() |
| if set(paths) & set(deleted): |
| def raise_file_not_found(): |
| raise FileNotFoundError('Files are removed from the patch.') |
| return Future(delegate=Gettable(raise_file_not_found)) |
| patched_files |= (set(added) | set(modified)) |
| dir_paths = set(path for path in paths if path.endswith('/')) |
| file_paths = set(paths) - dir_paths |
| patched_paths = file_paths & patched_files |
| unpatched_paths = file_paths - patched_files |
| return Future(delegate=_AsyncFetchFuture( |
| self._base_file_system.Read(unpatched_paths, binary), |
| self._patcher.Apply(patched_paths, self._base_file_system, binary), |
| self._TryReadDirectory(dir_paths, binary), |
| self)) |
| |
| ''' Given the list of patched files, it's not possible to determine whether |
| a directory to read exists in self._base_file_system. So try reading each one |
| and handle FileNotFoundError. |
| ''' |
| def _TryReadDirectory(self, paths, binary): |
| value = {} |
| for path in paths: |
| assert path.endswith('/') |
| try: |
| value[path] = self._base_file_system.ReadSingle(path, binary).Get() |
| except FileNotFoundError: |
| value[path] = None |
| return value |
| |
| def _GetDirectoryListingFromPatch(self, path): |
| assert path.endswith('/') |
| def _FindChildrenInPath(files, path): |
| result = [] |
| for f in files: |
| if f.startswith(path): |
| child_path = f[len(path):] |
| if '/' in child_path: |
| child_name = child_path[0:child_path.find('/') + 1] |
| else: |
| child_name = child_path |
| result.append(child_name) |
| return result |
| |
| added, deleted, modified = (tuple( |
| _FindChildrenInPath(files, path) |
| for files in self._patcher.GetPatchedFiles())) |
| |
| # A patch applies to files only. It cannot delete directories. |
| deleted_files = [child for child in deleted if not child.endswith('/')] |
| # However, these directories are actually modified because their children |
| # are patched. |
| modified += [child for child in deleted if child.endswith('/')] |
| |
| return (added, deleted_files, modified) |
| |
| def _PatchStat(self, stat_info, version, added, deleted, modified): |
| assert len(added) + len(deleted) + len(modified) > 0 |
| assert stat_info.child_versions is not None |
| |
| # Deep copy before patching to make sure it doesn't interfere with values |
| # cached in memory. |
| stat_info = deepcopy(stat_info) |
| |
| stat_info.version = version |
| for child in added + modified: |
| stat_info.child_versions[child] = version |
| for child in deleted: |
| if stat_info.child_versions.get(child): |
| del stat_info.child_versions[child] |
| |
| return stat_info |
| |
| def Stat(self, path): |
| version = self._patcher.GetVersion() |
| assert version is not None |
| version = 'patched_%s' % version |
| |
| directory, filename = path.rsplit('/', 1) |
| added, deleted, modified = self._GetDirectoryListingFromPatch( |
| directory + '/') |
| |
| if len(added) > 0: |
| # There are new files added. It's possible (if |directory| is new) that |
| # self._base_file_system.Stat will throw an exception. |
| try: |
| stat_info = self._PatchStat( |
| self._base_file_system.Stat(directory + '/'), |
| version, |
| added, |
| deleted, |
| modified) |
| except FileNotFoundError: |
| stat_info = StatInfo( |
| version, |
| dict((child, version) for child in added + modified)) |
| elif len(deleted) + len(modified) > 0: |
| # No files were added. |
| stat_info = self._PatchStat(self._base_file_system.Stat(directory + '/'), |
| version, |
| added, |
| deleted, |
| modified) |
| else: |
| # No changes are made in this directory. |
| return self._base_file_system.Stat(path) |
| |
| if stat_info.child_versions is not None: |
| if filename: |
| if filename in stat_info.child_versions: |
| stat_info = StatInfo(stat_info.child_versions[filename]) |
| else: |
| raise FileNotFoundError('%s was not in child versions' % filename) |
| return stat_info |
| |
| def GetIdentity(self): |
| return '%s(%s,%s)' % (self.__class__.__name__, |
| self._base_file_system.GetIdentity(), |
| self._patcher.GetIdentity()) |