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