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 5from copy import deepcopy 6 7from file_system import FileSystem, StatInfo, FileNotFoundError 8from future import Future 9 10 11def _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 35class 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