| OLD | NEW |
| (Empty) | |
| 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({path: self._PatchDirectoryListing(path, |
| 25 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 = {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(version, {child: version |
| 139 for child in added + modified}) |
| 140 elif len(deleted) + len(modified) > 0: |
| 141 # No files were added. |
| 142 stat_info = self._PatchStat(self._host_file_system.Stat(directory + '/'), |
| 143 version, |
| 144 added, |
| 145 deleted, |
| 146 modified) |
| 147 else: |
| 148 # No changes are made in this directory. |
| 149 return self._host_file_system.Stat(path) |
| 150 |
| 151 if stat_info.child_versions is not None: |
| 152 if filename: |
| 153 if filename in stat_info.child_versions: |
| 154 stat_info = StatInfo(stat_info.child_versions[filename]) |
| 155 else: |
| 156 raise FileNotFoundError('%s was not in child versions' % filename) |
| 157 return stat_info |
| OLD | NEW |