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