1*c2e18aaaSAndroid Build Coastguard Worker#!/usr/bin/env python3 2*c2e18aaaSAndroid Build Coastguard Worker# 3*c2e18aaaSAndroid Build Coastguard Worker# Copyright 2023, The Android Open Source Project 4*c2e18aaaSAndroid Build Coastguard Worker# 5*c2e18aaaSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 6*c2e18aaaSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 7*c2e18aaaSAndroid Build Coastguard Worker# You may obtain a copy of the License at 8*c2e18aaaSAndroid Build Coastguard Worker# 9*c2e18aaaSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 10*c2e18aaaSAndroid Build Coastguard Worker# 11*c2e18aaaSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 12*c2e18aaaSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 13*c2e18aaaSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14*c2e18aaaSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 15*c2e18aaaSAndroid Build Coastguard Worker# limitations under the License. 16*c2e18aaaSAndroid Build Coastguard Worker 17*c2e18aaaSAndroid Build Coastguard Worker"""Preserves and restores the state of a repository. 18*c2e18aaaSAndroid Build Coastguard Worker 19*c2e18aaaSAndroid Build Coastguard WorkerThis module includes a `Snapshot` class that provides methods to: 20*c2e18aaaSAndroid Build Coastguard Worker- Take snapshots of a directory, including or excluding specified paths. 21*c2e18aaaSAndroid Build Coastguard Worker- Preserve environment variables. 22*c2e18aaaSAndroid Build Coastguard Worker- Restore the directory state from previously taken snapshots, managing file and 23*c2e18aaaSAndroid Build Coastguard Workerdirectory deletions and replacements. 24*c2e18aaaSAndroid Build Coastguard Worker""" 25*c2e18aaaSAndroid Build Coastguard Worker 26*c2e18aaaSAndroid Build Coastguard Workerimport functools 27*c2e18aaaSAndroid Build Coastguard Workerimport glob 28*c2e18aaaSAndroid Build Coastguard Workerimport hashlib 29*c2e18aaaSAndroid Build Coastguard Workerimport json 30*c2e18aaaSAndroid Build Coastguard Workerimport logging 31*c2e18aaaSAndroid Build Coastguard Workerimport os 32*c2e18aaaSAndroid Build Coastguard Workerimport pathlib 33*c2e18aaaSAndroid Build Coastguard Workerimport threading 34*c2e18aaaSAndroid Build Coastguard Workerfrom typing import Any, Optional 35*c2e18aaaSAndroid Build Coastguard Worker 36*c2e18aaaSAndroid Build Coastguard Worker 37*c2e18aaaSAndroid Build Coastguard Workerdef _synchronized(func): 38*c2e18aaaSAndroid Build Coastguard Worker """Ensures thread-safe execution of the wrapped function.""" 39*c2e18aaaSAndroid Build Coastguard Worker lock = threading.Lock() 40*c2e18aaaSAndroid Build Coastguard Worker 41*c2e18aaaSAndroid Build Coastguard Worker @functools.wraps(func) 42*c2e18aaaSAndroid Build Coastguard Worker def _synchronized_func(*args, **kwargs): 43*c2e18aaaSAndroid Build Coastguard Worker with lock: 44*c2e18aaaSAndroid Build Coastguard Worker return func(*args, **kwargs) 45*c2e18aaaSAndroid Build Coastguard Worker 46*c2e18aaaSAndroid Build Coastguard Worker return _synchronized_func 47*c2e18aaaSAndroid Build Coastguard Worker 48*c2e18aaaSAndroid Build Coastguard Worker 49*c2e18aaaSAndroid Build Coastguard Workerclass Snapshot: 50*c2e18aaaSAndroid Build Coastguard Worker """Provides functionality to take and restore snapshots of a directory.""" 51*c2e18aaaSAndroid Build Coastguard Worker 52*c2e18aaaSAndroid Build Coastguard Worker def __init__(self, storage_dir: pathlib.Path): 53*c2e18aaaSAndroid Build Coastguard Worker """Initializes a Snapshot object. 54*c2e18aaaSAndroid Build Coastguard Worker 55*c2e18aaaSAndroid Build Coastguard Worker Args: 56*c2e18aaaSAndroid Build Coastguard Worker storage_dir: The directory where snapshots will be stored. 57*c2e18aaaSAndroid Build Coastguard Worker """ 58*c2e18aaaSAndroid Build Coastguard Worker self._dir_snapshot = _DirSnapshot(storage_dir) 59*c2e18aaaSAndroid Build Coastguard Worker self._env_snapshot = _EnvSnapshot(storage_dir) 60*c2e18aaaSAndroid Build Coastguard Worker self._obj_snapshot = _ObjectSnapshot(storage_dir) 61*c2e18aaaSAndroid Build Coastguard Worker self._lock = self._get_threading_lock(storage_dir) 62*c2e18aaaSAndroid Build Coastguard Worker 63*c2e18aaaSAndroid Build Coastguard Worker @_synchronized 64*c2e18aaaSAndroid Build Coastguard Worker def _get_threading_lock( 65*c2e18aaaSAndroid Build Coastguard Worker self, 66*c2e18aaaSAndroid Build Coastguard Worker name: str, 67*c2e18aaaSAndroid Build Coastguard Worker ): 68*c2e18aaaSAndroid Build Coastguard Worker """Gets a threading lock for the snapshot directory.""" 69*c2e18aaaSAndroid Build Coastguard Worker locks_dict_attr_name = 'threading_locks' 70*c2e18aaaSAndroid Build Coastguard Worker current_function = self._get_threading_lock.__func__ 71*c2e18aaaSAndroid Build Coastguard Worker if not hasattr(current_function, locks_dict_attr_name): 72*c2e18aaaSAndroid Build Coastguard Worker setattr(current_function, locks_dict_attr_name, {}) 73*c2e18aaaSAndroid Build Coastguard Worker if name not in getattr(current_function, locks_dict_attr_name): 74*c2e18aaaSAndroid Build Coastguard Worker getattr(current_function, locks_dict_attr_name)[name] = threading.Lock() 75*c2e18aaaSAndroid Build Coastguard Worker return getattr(current_function, locks_dict_attr_name)[name] 76*c2e18aaaSAndroid Build Coastguard Worker 77*c2e18aaaSAndroid Build Coastguard Worker # pylint: disable=too-many-arguments 78*c2e18aaaSAndroid Build Coastguard Worker def take_snapshot( 79*c2e18aaaSAndroid Build Coastguard Worker self, 80*c2e18aaaSAndroid Build Coastguard Worker name: str, 81*c2e18aaaSAndroid Build Coastguard Worker root_path: str, 82*c2e18aaaSAndroid Build Coastguard Worker include_paths: list[str], 83*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: Optional[list[str]] = None, 84*c2e18aaaSAndroid Build Coastguard Worker env_keys: Optional[list[str]] = None, 85*c2e18aaaSAndroid Build Coastguard Worker env: Optional[dict[str, str]] = None, 86*c2e18aaaSAndroid Build Coastguard Worker objs: Optional[dict[str, Any]] = None, 87*c2e18aaaSAndroid Build Coastguard Worker ) -> None: 88*c2e18aaaSAndroid Build Coastguard Worker """Takes a snapshot of the directory at the given path. 89*c2e18aaaSAndroid Build Coastguard Worker 90*c2e18aaaSAndroid Build Coastguard Worker Args: 91*c2e18aaaSAndroid Build Coastguard Worker name: The name of the snapshot. 92*c2e18aaaSAndroid Build Coastguard Worker root_path: The path to the directory to snapshot. 93*c2e18aaaSAndroid Build Coastguard Worker include_paths: A list of relative paths to include in the snapshot. 94*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: A list of relative paths to exclude from the snapshot. 95*c2e18aaaSAndroid Build Coastguard Worker env_keys: A list of environment variable keys to save. 96*c2e18aaaSAndroid Build Coastguard Worker env: Environment variables to use while restoring. 97*c2e18aaaSAndroid Build Coastguard Worker objs: A dictionary of objects to save. The current implementation limits 98*c2e18aaaSAndroid Build Coastguard Worker the type of objects to the types that can be serialized by the json 99*c2e18aaaSAndroid Build Coastguard Worker module. 100*c2e18aaaSAndroid Build Coastguard Worker """ 101*c2e18aaaSAndroid Build Coastguard Worker with self._lock: 102*c2e18aaaSAndroid Build Coastguard Worker self._dir_snapshot.take_snapshot( 103*c2e18aaaSAndroid Build Coastguard Worker name, root_path, include_paths, exclude_paths, env 104*c2e18aaaSAndroid Build Coastguard Worker ) 105*c2e18aaaSAndroid Build Coastguard Worker self._env_snapshot.take_snapshot(name, env_keys) 106*c2e18aaaSAndroid Build Coastguard Worker self._obj_snapshot.take_snapshot(name, objs) 107*c2e18aaaSAndroid Build Coastguard Worker 108*c2e18aaaSAndroid Build Coastguard Worker def restore_snapshot( 109*c2e18aaaSAndroid Build Coastguard Worker self, 110*c2e18aaaSAndroid Build Coastguard Worker name: str, 111*c2e18aaaSAndroid Build Coastguard Worker root_path: str, 112*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: Optional[list[str]] = None, 113*c2e18aaaSAndroid Build Coastguard Worker ) -> tuple[dict[str, str], dict[str, Any]]: 114*c2e18aaaSAndroid Build Coastguard Worker """Restores directory at given path to a snapshot with the given name. 115*c2e18aaaSAndroid Build Coastguard Worker 116*c2e18aaaSAndroid Build Coastguard Worker Args: 117*c2e18aaaSAndroid Build Coastguard Worker name: The name of the snapshot. 118*c2e18aaaSAndroid Build Coastguard Worker root_path: The path to the target directory. 119*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: A list of paths to ignore during restore. 120*c2e18aaaSAndroid Build Coastguard Worker 121*c2e18aaaSAndroid Build Coastguard Worker Returns: 122*c2e18aaaSAndroid Build Coastguard Worker A tuple of restored environment variables and object dictionary. 123*c2e18aaaSAndroid Build Coastguard Worker """ 124*c2e18aaaSAndroid Build Coastguard Worker env = self._env_snapshot.restore_snapshot(name, root_path) 125*c2e18aaaSAndroid Build Coastguard Worker objs = self._obj_snapshot.restore_snapshot(name) 126*c2e18aaaSAndroid Build Coastguard Worker with self._lock: 127*c2e18aaaSAndroid Build Coastguard Worker self._dir_snapshot.restore_snapshot(name, root_path, exclude_paths, env) 128*c2e18aaaSAndroid Build Coastguard Worker return env, objs 129*c2e18aaaSAndroid Build Coastguard Worker 130*c2e18aaaSAndroid Build Coastguard Worker 131*c2e18aaaSAndroid Build Coastguard Workerclass _ObjectSnapshot: 132*c2e18aaaSAndroid Build Coastguard Worker """Save and restore a dictionary of objects through json.""" 133*c2e18aaaSAndroid Build Coastguard Worker 134*c2e18aaaSAndroid Build Coastguard Worker def __init__(self, storage_path: pathlib.Path): 135*c2e18aaaSAndroid Build Coastguard Worker self._storage_path = storage_path 136*c2e18aaaSAndroid Build Coastguard Worker 137*c2e18aaaSAndroid Build Coastguard Worker def take_snapshot( 138*c2e18aaaSAndroid Build Coastguard Worker self, 139*c2e18aaaSAndroid Build Coastguard Worker name: str, 140*c2e18aaaSAndroid Build Coastguard Worker objs: Optional[dict[str, Any]] = None, 141*c2e18aaaSAndroid Build Coastguard Worker ) -> None: 142*c2e18aaaSAndroid Build Coastguard Worker """Save a dictionary of objects in snapshot. 143*c2e18aaaSAndroid Build Coastguard Worker 144*c2e18aaaSAndroid Build Coastguard Worker Args: 145*c2e18aaaSAndroid Build Coastguard Worker name: The name of the snapshot 146*c2e18aaaSAndroid Build Coastguard Worker objs: A dictionary of objects to snapshot. Note: The current 147*c2e18aaaSAndroid Build Coastguard Worker implementation limits the type of objects to the types that can be 148*c2e18aaaSAndroid Build Coastguard Worker serialized by the json module. 149*c2e18aaaSAndroid Build Coastguard Worker """ 150*c2e18aaaSAndroid Build Coastguard Worker if objs is None: 151*c2e18aaaSAndroid Build Coastguard Worker objs = {} 152*c2e18aaaSAndroid Build Coastguard Worker with open( 153*c2e18aaaSAndroid Build Coastguard Worker self._storage_path.joinpath('%s.objs.json' % name), 154*c2e18aaaSAndroid Build Coastguard Worker 'w', 155*c2e18aaaSAndroid Build Coastguard Worker encoding='utf-8', 156*c2e18aaaSAndroid Build Coastguard Worker ) as f: 157*c2e18aaaSAndroid Build Coastguard Worker json.dump(objs, f) 158*c2e18aaaSAndroid Build Coastguard Worker 159*c2e18aaaSAndroid Build Coastguard Worker def restore_snapshot(self, name: str) -> dict[str, Any]: 160*c2e18aaaSAndroid Build Coastguard Worker """Restore saved objects from snapshot.""" 161*c2e18aaaSAndroid Build Coastguard Worker with open( 162*c2e18aaaSAndroid Build Coastguard Worker self._storage_path.joinpath('%s.objs.json' % name), 163*c2e18aaaSAndroid Build Coastguard Worker 'r', 164*c2e18aaaSAndroid Build Coastguard Worker encoding='utf-8', 165*c2e18aaaSAndroid Build Coastguard Worker ) as f: 166*c2e18aaaSAndroid Build Coastguard Worker return json.load(f) 167*c2e18aaaSAndroid Build Coastguard Worker 168*c2e18aaaSAndroid Build Coastguard Worker 169*c2e18aaaSAndroid Build Coastguard Workerclass _EnvSnapshot: 170*c2e18aaaSAndroid Build Coastguard Worker """Save and restore environment variables.""" 171*c2e18aaaSAndroid Build Coastguard Worker 172*c2e18aaaSAndroid Build Coastguard Worker _repo_root_placeholder = '<repo_root_placeholder>' 173*c2e18aaaSAndroid Build Coastguard Worker 174*c2e18aaaSAndroid Build Coastguard Worker def __init__(self, storage_path: pathlib.Path): 175*c2e18aaaSAndroid Build Coastguard Worker self._storage_path = storage_path 176*c2e18aaaSAndroid Build Coastguard Worker 177*c2e18aaaSAndroid Build Coastguard Worker def take_snapshot( 178*c2e18aaaSAndroid Build Coastguard Worker self, 179*c2e18aaaSAndroid Build Coastguard Worker name: str, 180*c2e18aaaSAndroid Build Coastguard Worker env_keys: Optional[list[str]] = None, 181*c2e18aaaSAndroid Build Coastguard Worker ) -> None: 182*c2e18aaaSAndroid Build Coastguard Worker """Save a subset of environment variables.""" 183*c2e18aaaSAndroid Build Coastguard Worker if env_keys is None: 184*c2e18aaaSAndroid Build Coastguard Worker env_keys = [] 185*c2e18aaaSAndroid Build Coastguard Worker original_env = os.environ.copy() 186*c2e18aaaSAndroid Build Coastguard Worker subset_env = { 187*c2e18aaaSAndroid Build Coastguard Worker key: os.environ[key] for key in env_keys if key in original_env 188*c2e18aaaSAndroid Build Coastguard Worker } 189*c2e18aaaSAndroid Build Coastguard Worker modified_env = { 190*c2e18aaaSAndroid Build Coastguard Worker key: value.replace( 191*c2e18aaaSAndroid Build Coastguard Worker os.environ['ANDROID_BUILD_TOP'], self._repo_root_placeholder 192*c2e18aaaSAndroid Build Coastguard Worker ) 193*c2e18aaaSAndroid Build Coastguard Worker for key, value in subset_env.items() 194*c2e18aaaSAndroid Build Coastguard Worker } 195*c2e18aaaSAndroid Build Coastguard Worker with open(self._get_env_file_path(name), 'w', encoding='utf-8') as f: 196*c2e18aaaSAndroid Build Coastguard Worker json.dump(modified_env, f) 197*c2e18aaaSAndroid Build Coastguard Worker 198*c2e18aaaSAndroid Build Coastguard Worker def restore_snapshot(self, name: str, root_path: str) -> dict[str, str]: 199*c2e18aaaSAndroid Build Coastguard Worker """Load saved environment variables.""" 200*c2e18aaaSAndroid Build Coastguard Worker with self._get_env_file_path(name).open('r') as f: 201*c2e18aaaSAndroid Build Coastguard Worker loaded_env = json.load(f) 202*c2e18aaaSAndroid Build Coastguard Worker restored_env = { 203*c2e18aaaSAndroid Build Coastguard Worker key: value.replace( 204*c2e18aaaSAndroid Build Coastguard Worker self._repo_root_placeholder, 205*c2e18aaaSAndroid Build Coastguard Worker root_path, 206*c2e18aaaSAndroid Build Coastguard Worker ) 207*c2e18aaaSAndroid Build Coastguard Worker for key, value in loaded_env.items() 208*c2e18aaaSAndroid Build Coastguard Worker } 209*c2e18aaaSAndroid Build Coastguard Worker if 'PATH' in os.environ: 210*c2e18aaaSAndroid Build Coastguard Worker if 'PATH' in restored_env: 211*c2e18aaaSAndroid Build Coastguard Worker restored_env['PATH'] = restored_env['PATH'] + ':' + os.environ['PATH'] 212*c2e18aaaSAndroid Build Coastguard Worker else: 213*c2e18aaaSAndroid Build Coastguard Worker restored_env['PATH'] = os.environ['PATH'] 214*c2e18aaaSAndroid Build Coastguard Worker return restored_env 215*c2e18aaaSAndroid Build Coastguard Worker 216*c2e18aaaSAndroid Build Coastguard Worker def _get_env_file_path(self, name: str) -> pathlib.Path: 217*c2e18aaaSAndroid Build Coastguard Worker """Get environment file path.""" 218*c2e18aaaSAndroid Build Coastguard Worker return self._storage_path / (name + '_env.json') 219*c2e18aaaSAndroid Build Coastguard Worker 220*c2e18aaaSAndroid Build Coastguard Worker 221*c2e18aaaSAndroid Build Coastguard Workerclass _FileInfo: 222*c2e18aaaSAndroid Build Coastguard Worker """An object to save file information.""" 223*c2e18aaaSAndroid Build Coastguard Worker 224*c2e18aaaSAndroid Build Coastguard Worker # pylint: disable=too-many-arguments 225*c2e18aaaSAndroid Build Coastguard Worker def __init__( 226*c2e18aaaSAndroid Build Coastguard Worker self, 227*c2e18aaaSAndroid Build Coastguard Worker path: str, 228*c2e18aaaSAndroid Build Coastguard Worker timestamp: float, 229*c2e18aaaSAndroid Build Coastguard Worker content_hash: str, 230*c2e18aaaSAndroid Build Coastguard Worker permissions: int, 231*c2e18aaaSAndroid Build Coastguard Worker symlink_target: str, 232*c2e18aaaSAndroid Build Coastguard Worker is_directory: bool, 233*c2e18aaaSAndroid Build Coastguard Worker is_target_in_workspace: bool = False, 234*c2e18aaaSAndroid Build Coastguard Worker ): 235*c2e18aaaSAndroid Build Coastguard Worker self.path = path 236*c2e18aaaSAndroid Build Coastguard Worker self.timestamp = timestamp 237*c2e18aaaSAndroid Build Coastguard Worker self.content_hash = content_hash 238*c2e18aaaSAndroid Build Coastguard Worker self.permissions = permissions 239*c2e18aaaSAndroid Build Coastguard Worker self.symlink_target = symlink_target 240*c2e18aaaSAndroid Build Coastguard Worker self.is_directory = is_directory 241*c2e18aaaSAndroid Build Coastguard Worker self.is_target_in_workspace = is_target_in_workspace 242*c2e18aaaSAndroid Build Coastguard Worker 243*c2e18aaaSAndroid Build Coastguard Worker 244*c2e18aaaSAndroid Build Coastguard Workerclass _BlobStore: 245*c2e18aaaSAndroid Build Coastguard Worker """Class to save and load file content.""" 246*c2e18aaaSAndroid Build Coastguard Worker 247*c2e18aaaSAndroid Build Coastguard Worker def __init__(self, path: str): 248*c2e18aaaSAndroid Build Coastguard Worker self.path = pathlib.Path(path) 249*c2e18aaaSAndroid Build Coastguard Worker self.cache = self._load_cache() 250*c2e18aaaSAndroid Build Coastguard Worker 251*c2e18aaaSAndroid Build Coastguard Worker def add(self, path: pathlib.Path, timestamp: float) -> str: 252*c2e18aaaSAndroid Build Coastguard Worker """Add a file path to the store.""" 253*c2e18aaaSAndroid Build Coastguard Worker cache_key = path.as_posix() + str(timestamp) 254*c2e18aaaSAndroid Build Coastguard Worker if cache_key in self.cache: 255*c2e18aaaSAndroid Build Coastguard Worker return self.cache[cache_key] 256*c2e18aaaSAndroid Build Coastguard Worker content = path.read_bytes() 257*c2e18aaaSAndroid Build Coastguard Worker content_hash = hashlib.sha256(content).hexdigest() 258*c2e18aaaSAndroid Build Coastguard Worker content_path = self.path.joinpath(content_hash[:2], content_hash[2:]) 259*c2e18aaaSAndroid Build Coastguard Worker if not content_path.exists(): 260*c2e18aaaSAndroid Build Coastguard Worker content_path.parent.mkdir(parents=True, exist_ok=True) 261*c2e18aaaSAndroid Build Coastguard Worker content_path.write_bytes(content) 262*c2e18aaaSAndroid Build Coastguard Worker self.cache[cache_key] = content_hash 263*c2e18aaaSAndroid Build Coastguard Worker return content_hash 264*c2e18aaaSAndroid Build Coastguard Worker 265*c2e18aaaSAndroid Build Coastguard Worker def get(self, content_hash: str) -> bytes: 266*c2e18aaaSAndroid Build Coastguard Worker """Read file content from a content hash.""" 267*c2e18aaaSAndroid Build Coastguard Worker file_path = self.path.joinpath(content_hash[:2], content_hash[2:]) 268*c2e18aaaSAndroid Build Coastguard Worker if file_path.exists(): 269*c2e18aaaSAndroid Build Coastguard Worker return file_path.read_bytes() 270*c2e18aaaSAndroid Build Coastguard Worker return None 271*c2e18aaaSAndroid Build Coastguard Worker 272*c2e18aaaSAndroid Build Coastguard Worker def dump_cache(self) -> None: 273*c2e18aaaSAndroid Build Coastguard Worker """Dump the saved file path cache to speed up next run.""" 274*c2e18aaaSAndroid Build Coastguard Worker self._get_cache_path().parent.mkdir(parents=True, exist_ok=True) 275*c2e18aaaSAndroid Build Coastguard Worker with self._get_cache_path().open('w', encoding='utf-8') as f: 276*c2e18aaaSAndroid Build Coastguard Worker json.dump(self.cache, f) 277*c2e18aaaSAndroid Build Coastguard Worker 278*c2e18aaaSAndroid Build Coastguard Worker def _load_cache(self) -> dict[str, str]: 279*c2e18aaaSAndroid Build Coastguard Worker if not self._get_cache_path().exists(): 280*c2e18aaaSAndroid Build Coastguard Worker return {} 281*c2e18aaaSAndroid Build Coastguard Worker with self._get_cache_path().open('r', encoding='utf-8') as f: 282*c2e18aaaSAndroid Build Coastguard Worker return json.load(f) 283*c2e18aaaSAndroid Build Coastguard Worker 284*c2e18aaaSAndroid Build Coastguard Worker def _get_cache_path(self) -> pathlib.Path: 285*c2e18aaaSAndroid Build Coastguard Worker return self.path.joinpath('cache.json') 286*c2e18aaaSAndroid Build Coastguard Worker 287*c2e18aaaSAndroid Build Coastguard Worker 288*c2e18aaaSAndroid Build Coastguard Workerclass _DirSnapshot: 289*c2e18aaaSAndroid Build Coastguard Worker """Class to take and restore snapshot for a directory path.""" 290*c2e18aaaSAndroid Build Coastguard Worker 291*c2e18aaaSAndroid Build Coastguard Worker def __init__(self, storage_path: pathlib.Path): 292*c2e18aaaSAndroid Build Coastguard Worker self._storage_path = storage_path 293*c2e18aaaSAndroid Build Coastguard Worker self._blob_store = _BlobStore(self._storage_path.joinpath('blobs')) 294*c2e18aaaSAndroid Build Coastguard Worker 295*c2e18aaaSAndroid Build Coastguard Worker def _expand_vars_paths( 296*c2e18aaaSAndroid Build Coastguard Worker self, paths: list[str], variables: dict[str, str] 297*c2e18aaaSAndroid Build Coastguard Worker ) -> list[str]: 298*c2e18aaaSAndroid Build Coastguard Worker """Expand variables in paths with the given environment variables. 299*c2e18aaaSAndroid Build Coastguard Worker 300*c2e18aaaSAndroid Build Coastguard Worker This function is similar to os.path.expandvars(path) which relies on 301*c2e18aaaSAndroid Build Coastguard Worker os.environ. 302*c2e18aaaSAndroid Build Coastguard Worker 303*c2e18aaaSAndroid Build Coastguard Worker Args: 304*c2e18aaaSAndroid Build Coastguard Worker paths: A list of paths that might contains variables to expand. 305*c2e18aaaSAndroid Build Coastguard Worker variables: A dictionary of variable names and values. 306*c2e18aaaSAndroid Build Coastguard Worker 307*c2e18aaaSAndroid Build Coastguard Worker Returns: 308*c2e18aaaSAndroid Build Coastguard Worker A list containing paths whose variables have been expanded if known. 309*c2e18aaaSAndroid Build Coastguard Worker """ 310*c2e18aaaSAndroid Build Coastguard Worker if not variables: 311*c2e18aaaSAndroid Build Coastguard Worker return paths 312*c2e18aaaSAndroid Build Coastguard Worker path_result = paths.copy() 313*c2e18aaaSAndroid Build Coastguard Worker for idx, _ in enumerate(path_result): 314*c2e18aaaSAndroid Build Coastguard Worker for key, val in sorted( 315*c2e18aaaSAndroid Build Coastguard Worker variables.items(), key=lambda item: len(item[0]), reverse=True 316*c2e18aaaSAndroid Build Coastguard Worker ): 317*c2e18aaaSAndroid Build Coastguard Worker path_result[idx] = path_result[idx].replace(f'${key}', val) 318*c2e18aaaSAndroid Build Coastguard Worker return path_result 319*c2e18aaaSAndroid Build Coastguard Worker 320*c2e18aaaSAndroid Build Coastguard Worker def _expand_wildcard_paths( 321*c2e18aaaSAndroid Build Coastguard Worker self, 322*c2e18aaaSAndroid Build Coastguard Worker root_path: str, 323*c2e18aaaSAndroid Build Coastguard Worker paths: list[str], 324*c2e18aaaSAndroid Build Coastguard Worker env: Optional[dict[str, str]] = None, 325*c2e18aaaSAndroid Build Coastguard Worker ) -> list[str]: 326*c2e18aaaSAndroid Build Coastguard Worker """Expand wildcard paths.""" 327*c2e18aaaSAndroid Build Coastguard Worker compose = lambda inner, outer: lambda path: outer(inner(path)) 328*c2e18aaaSAndroid Build Coastguard Worker get_abs_path = ( 329*c2e18aaaSAndroid Build Coastguard Worker lambda path: path 330*c2e18aaaSAndroid Build Coastguard Worker if os.path.isabs(path) 331*c2e18aaaSAndroid Build Coastguard Worker else os.path.join(root_path, path) 332*c2e18aaaSAndroid Build Coastguard Worker ) 333*c2e18aaaSAndroid Build Coastguard Worker glob_path = functools.partial(glob.glob, recursive=True) 334*c2e18aaaSAndroid Build Coastguard Worker return sum( 335*c2e18aaaSAndroid Build Coastguard Worker map( 336*c2e18aaaSAndroid Build Coastguard Worker compose(get_abs_path, glob_path), 337*c2e18aaaSAndroid Build Coastguard Worker self._expand_vars_paths(paths, env), 338*c2e18aaaSAndroid Build Coastguard Worker ), 339*c2e18aaaSAndroid Build Coastguard Worker [], 340*c2e18aaaSAndroid Build Coastguard Worker ) 341*c2e18aaaSAndroid Build Coastguard Worker 342*c2e18aaaSAndroid Build Coastguard Worker def _is_excluded(self, path: str, exclude_paths: list[pathlib.Path]) -> bool: 343*c2e18aaaSAndroid Build Coastguard Worker """Check whether a path should be excluded.""" 344*c2e18aaaSAndroid Build Coastguard Worker return exclude_paths and any( 345*c2e18aaaSAndroid Build Coastguard Worker path.startswith(exclude_path) for exclude_path in exclude_paths 346*c2e18aaaSAndroid Build Coastguard Worker ) 347*c2e18aaaSAndroid Build Coastguard Worker 348*c2e18aaaSAndroid Build Coastguard Worker def _filter_excluded_paths( 349*c2e18aaaSAndroid Build Coastguard Worker self, 350*c2e18aaaSAndroid Build Coastguard Worker root: pathlib.Path, 351*c2e18aaaSAndroid Build Coastguard Worker paths: list[pathlib.Path], 352*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: list[pathlib.Path], 353*c2e18aaaSAndroid Build Coastguard Worker ) -> None: 354*c2e18aaaSAndroid Build Coastguard Worker """Filter a list of paths with a list of exclude paths.""" 355*c2e18aaaSAndroid Build Coastguard Worker new_paths = [ 356*c2e18aaaSAndroid Build Coastguard Worker path 357*c2e18aaaSAndroid Build Coastguard Worker for path in paths 358*c2e18aaaSAndroid Build Coastguard Worker if not self._is_excluded(os.path.join(root, path), exclude_paths) 359*c2e18aaaSAndroid Build Coastguard Worker ] 360*c2e18aaaSAndroid Build Coastguard Worker if len(new_paths) == len(paths): 361*c2e18aaaSAndroid Build Coastguard Worker return 362*c2e18aaaSAndroid Build Coastguard Worker paths.clear() 363*c2e18aaaSAndroid Build Coastguard Worker paths.extend(new_paths) 364*c2e18aaaSAndroid Build Coastguard Worker 365*c2e18aaaSAndroid Build Coastguard Worker def take_snapshot( 366*c2e18aaaSAndroid Build Coastguard Worker self, 367*c2e18aaaSAndroid Build Coastguard Worker name: str, 368*c2e18aaaSAndroid Build Coastguard Worker root_path: str, 369*c2e18aaaSAndroid Build Coastguard Worker include_paths: list[str], 370*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: Optional[list[str]] = None, 371*c2e18aaaSAndroid Build Coastguard Worker env: Optional[dict[str, str]] = None, 372*c2e18aaaSAndroid Build Coastguard Worker ) -> tuple[dict[str, _FileInfo], list[str]]: 373*c2e18aaaSAndroid Build Coastguard Worker """Creates a snapshot of the directory at the given path. 374*c2e18aaaSAndroid Build Coastguard Worker 375*c2e18aaaSAndroid Build Coastguard Worker Args: 376*c2e18aaaSAndroid Build Coastguard Worker name: The name of the snapshot. 377*c2e18aaaSAndroid Build Coastguard Worker root_path: The path to the root directory. 378*c2e18aaaSAndroid Build Coastguard Worker include_paths: A list of relative paths to include in the snapshot. 379*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: A list of relative paths to exclude from the snapshot. 380*c2e18aaaSAndroid Build Coastguard Worker env: Environment variables to use while restoring. 381*c2e18aaaSAndroid Build Coastguard Worker 382*c2e18aaaSAndroid Build Coastguard Worker Returns: 383*c2e18aaaSAndroid Build Coastguard Worker A tuple containing: 384*c2e18aaaSAndroid Build Coastguard Worker - A dictionary of _FileInfo objects keyed by their relative path 385*c2e18aaaSAndroid Build Coastguard Worker within the directory. 386*c2e18aaaSAndroid Build Coastguard Worker """ 387*c2e18aaaSAndroid Build Coastguard Worker include_paths = ( 388*c2e18aaaSAndroid Build Coastguard Worker self._expand_wildcard_paths(root_path, include_paths, env) 389*c2e18aaaSAndroid Build Coastguard Worker if include_paths 390*c2e18aaaSAndroid Build Coastguard Worker else [] 391*c2e18aaaSAndroid Build Coastguard Worker ) 392*c2e18aaaSAndroid Build Coastguard Worker exclude_paths = ( 393*c2e18aaaSAndroid Build Coastguard Worker self._expand_wildcard_paths(root_path, exclude_paths, env) 394*c2e18aaaSAndroid Build Coastguard Worker if exclude_paths 395*c2e18aaaSAndroid Build Coastguard Worker else [] 396*c2e18aaaSAndroid Build Coastguard Worker ) 397*c2e18aaaSAndroid Build Coastguard Worker 398*c2e18aaaSAndroid Build Coastguard Worker file_infos = {} 399*c2e18aaaSAndroid Build Coastguard Worker 400*c2e18aaaSAndroid Build Coastguard Worker def process_directory(path: pathlib.Path) -> None: 401*c2e18aaaSAndroid Build Coastguard Worker if path.is_symlink(): 402*c2e18aaaSAndroid Build Coastguard Worker process_link(path) 403*c2e18aaaSAndroid Build Coastguard Worker return 404*c2e18aaaSAndroid Build Coastguard Worker relative_path = path.relative_to(root_path).as_posix() 405*c2e18aaaSAndroid Build Coastguard Worker if relative_path == '.': 406*c2e18aaaSAndroid Build Coastguard Worker return 407*c2e18aaaSAndroid Build Coastguard Worker file_infos[relative_path] = _FileInfo( 408*c2e18aaaSAndroid Build Coastguard Worker relative_path, 409*c2e18aaaSAndroid Build Coastguard Worker timestamp=None, 410*c2e18aaaSAndroid Build Coastguard Worker content_hash=None, 411*c2e18aaaSAndroid Build Coastguard Worker permissions=path.stat().st_mode, 412*c2e18aaaSAndroid Build Coastguard Worker symlink_target=None, 413*c2e18aaaSAndroid Build Coastguard Worker is_directory=True, 414*c2e18aaaSAndroid Build Coastguard Worker ) 415*c2e18aaaSAndroid Build Coastguard Worker 416*c2e18aaaSAndroid Build Coastguard Worker def process_file(path: pathlib.Path) -> None: 417*c2e18aaaSAndroid Build Coastguard Worker if path.is_symlink(): 418*c2e18aaaSAndroid Build Coastguard Worker process_link(path) 419*c2e18aaaSAndroid Build Coastguard Worker return 420*c2e18aaaSAndroid Build Coastguard Worker relative_path = path.relative_to(root_path).as_posix() 421*c2e18aaaSAndroid Build Coastguard Worker timestamp = path.stat().st_mtime 422*c2e18aaaSAndroid Build Coastguard Worker file_infos[relative_path] = _FileInfo( 423*c2e18aaaSAndroid Build Coastguard Worker relative_path, 424*c2e18aaaSAndroid Build Coastguard Worker timestamp=timestamp, 425*c2e18aaaSAndroid Build Coastguard Worker content_hash=self._blob_store.add(path, timestamp) 426*c2e18aaaSAndroid Build Coastguard Worker if path.stat().st_size 427*c2e18aaaSAndroid Build Coastguard Worker else None, 428*c2e18aaaSAndroid Build Coastguard Worker permissions=path.stat().st_mode, 429*c2e18aaaSAndroid Build Coastguard Worker symlink_target=None, 430*c2e18aaaSAndroid Build Coastguard Worker is_directory=False, 431*c2e18aaaSAndroid Build Coastguard Worker ) 432*c2e18aaaSAndroid Build Coastguard Worker 433*c2e18aaaSAndroid Build Coastguard Worker def process_link(path: pathlib.Path) -> None: 434*c2e18aaaSAndroid Build Coastguard Worker relative_path = path.relative_to(root_path).as_posix() 435*c2e18aaaSAndroid Build Coastguard Worker symlink_target = path.readlink() 436*c2e18aaaSAndroid Build Coastguard Worker is_target_in_workspace = False 437*c2e18aaaSAndroid Build Coastguard Worker if symlink_target.is_relative_to(root_path): 438*c2e18aaaSAndroid Build Coastguard Worker symlink_target = symlink_target.relative_to(root_path) 439*c2e18aaaSAndroid Build Coastguard Worker is_target_in_workspace = True 440*c2e18aaaSAndroid Build Coastguard Worker file_infos[relative_path] = _FileInfo( 441*c2e18aaaSAndroid Build Coastguard Worker relative_path, 442*c2e18aaaSAndroid Build Coastguard Worker timestamp=None, 443*c2e18aaaSAndroid Build Coastguard Worker content_hash=None, 444*c2e18aaaSAndroid Build Coastguard Worker permissions=None, 445*c2e18aaaSAndroid Build Coastguard Worker symlink_target=symlink_target.as_posix(), 446*c2e18aaaSAndroid Build Coastguard Worker is_target_in_workspace=is_target_in_workspace, 447*c2e18aaaSAndroid Build Coastguard Worker is_directory=False, 448*c2e18aaaSAndroid Build Coastguard Worker ) 449*c2e18aaaSAndroid Build Coastguard Worker 450*c2e18aaaSAndroid Build Coastguard Worker def process_path(path: pathlib.Path) -> None: 451*c2e18aaaSAndroid Build Coastguard Worker if self._is_excluded(path.as_posix(), exclude_paths): 452*c2e18aaaSAndroid Build Coastguard Worker return 453*c2e18aaaSAndroid Build Coastguard Worker if path.is_symlink(): 454*c2e18aaaSAndroid Build Coastguard Worker process_link(path) 455*c2e18aaaSAndroid Build Coastguard Worker elif path.is_file(): 456*c2e18aaaSAndroid Build Coastguard Worker process_file(path) 457*c2e18aaaSAndroid Build Coastguard Worker elif path.is_dir(): 458*c2e18aaaSAndroid Build Coastguard Worker process_directory(path) 459*c2e18aaaSAndroid Build Coastguard Worker for root, directories, files in os.walk(path): 460*c2e18aaaSAndroid Build Coastguard Worker self._filter_excluded_paths(root, directories, exclude_paths) 461*c2e18aaaSAndroid Build Coastguard Worker self._filter_excluded_paths(root, files, exclude_paths) 462*c2e18aaaSAndroid Build Coastguard Worker for directory in directories: 463*c2e18aaaSAndroid Build Coastguard Worker process_directory(pathlib.Path(root).joinpath(directory)) 464*c2e18aaaSAndroid Build Coastguard Worker for file in files: 465*c2e18aaaSAndroid Build Coastguard Worker process_file(pathlib.Path(root).joinpath(file)) 466*c2e18aaaSAndroid Build Coastguard Worker else: 467*c2e18aaaSAndroid Build Coastguard Worker # We are not throwing error here because it might be just a 468*c2e18aaaSAndroid Build Coastguard Worker # corner case which likely doesn't affect the test process. 469*c2e18aaaSAndroid Build Coastguard Worker logging.error('Unexpected path type: %s', path.as_posix()) 470*c2e18aaaSAndroid Build Coastguard Worker 471*c2e18aaaSAndroid Build Coastguard Worker for path in include_paths: 472*c2e18aaaSAndroid Build Coastguard Worker process_path(pathlib.Path(path)) 473*c2e18aaaSAndroid Build Coastguard Worker 474*c2e18aaaSAndroid Build Coastguard Worker snapshot_path = self._storage_path.joinpath(name + '_metadata.json') 475*c2e18aaaSAndroid Build Coastguard Worker snapshot_path.parent.mkdir(parents=True, exist_ok=True) 476*c2e18aaaSAndroid Build Coastguard Worker with snapshot_path.open('w') as f: 477*c2e18aaaSAndroid Build Coastguard Worker json.dump(file_infos, f, default=lambda o: o.__dict__) 478*c2e18aaaSAndroid Build Coastguard Worker 479*c2e18aaaSAndroid Build Coastguard Worker self._blob_store.dump_cache() 480*c2e18aaaSAndroid Build Coastguard Worker 481*c2e18aaaSAndroid Build Coastguard Worker return file_infos 482*c2e18aaaSAndroid Build Coastguard Worker 483*c2e18aaaSAndroid Build Coastguard Worker def restore_snapshot( 484*c2e18aaaSAndroid Build Coastguard Worker self, 485*c2e18aaaSAndroid Build Coastguard Worker name: str, 486*c2e18aaaSAndroid Build Coastguard Worker root_path: str, 487*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: Optional[list[str]] = None, 488*c2e18aaaSAndroid Build Coastguard Worker env: Optional[dict[str, str]] = None, 489*c2e18aaaSAndroid Build Coastguard Worker ) -> tuple[list[str], list[str], list[str]]: 490*c2e18aaaSAndroid Build Coastguard Worker """Restores directory at given path to snapshot with given name. 491*c2e18aaaSAndroid Build Coastguard Worker 492*c2e18aaaSAndroid Build Coastguard Worker Args: 493*c2e18aaaSAndroid Build Coastguard Worker name: The name of the snapshot. 494*c2e18aaaSAndroid Build Coastguard Worker root_path: The path to the root directory. 495*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: A list of relative paths to ignore during restoring. 496*c2e18aaaSAndroid Build Coastguard Worker env: Environment variables to use while restoring. 497*c2e18aaaSAndroid Build Coastguard Worker 498*c2e18aaaSAndroid Build Coastguard Worker Returns: 499*c2e18aaaSAndroid Build Coastguard Worker A tuple containing 3 lists: 500*c2e18aaaSAndroid Build Coastguard Worker - Files and directories that were deleted. 501*c2e18aaaSAndroid Build Coastguard Worker - Files that were replaced. 502*c2e18aaaSAndroid Build Coastguard Worker """ 503*c2e18aaaSAndroid Build Coastguard Worker with self._storage_path.joinpath(name + '_metadata.json').open('r') as f: 504*c2e18aaaSAndroid Build Coastguard Worker file_infos_dict = { 505*c2e18aaaSAndroid Build Coastguard Worker key: _FileInfo(**val) for key, val in json.load(f).items() 506*c2e18aaaSAndroid Build Coastguard Worker } 507*c2e18aaaSAndroid Build Coastguard Worker 508*c2e18aaaSAndroid Build Coastguard Worker exclude_paths = ( 509*c2e18aaaSAndroid Build Coastguard Worker self._expand_wildcard_paths(root_path, exclude_paths, env) 510*c2e18aaaSAndroid Build Coastguard Worker if exclude_paths 511*c2e18aaaSAndroid Build Coastguard Worker else [] 512*c2e18aaaSAndroid Build Coastguard Worker ) 513*c2e18aaaSAndroid Build Coastguard Worker 514*c2e18aaaSAndroid Build Coastguard Worker deleted = self._remove_extra_files( 515*c2e18aaaSAndroid Build Coastguard Worker file_infos_dict, root_path, exclude_paths 516*c2e18aaaSAndroid Build Coastguard Worker ) 517*c2e18aaaSAndroid Build Coastguard Worker self._restore_directories(file_infos_dict, root_path, exclude_paths) 518*c2e18aaaSAndroid Build Coastguard Worker replaced = self._restore_files(file_infos_dict, root_path, exclude_paths) 519*c2e18aaaSAndroid Build Coastguard Worker 520*c2e18aaaSAndroid Build Coastguard Worker return deleted, replaced 521*c2e18aaaSAndroid Build Coastguard Worker 522*c2e18aaaSAndroid Build Coastguard Worker def _remove_extra_files( 523*c2e18aaaSAndroid Build Coastguard Worker self, 524*c2e18aaaSAndroid Build Coastguard Worker file_infos_dict: dict[str, _FileInfo], 525*c2e18aaaSAndroid Build Coastguard Worker root_path: str, 526*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: list[str], 527*c2e18aaaSAndroid Build Coastguard Worker ): 528*c2e18aaaSAndroid Build Coastguard Worker """Internal method to remove extra files during snapshot restore.""" 529*c2e18aaaSAndroid Build Coastguard Worker deleted = [] 530*c2e18aaaSAndroid Build Coastguard Worker for root, directories, files in os.walk(root_path): 531*c2e18aaaSAndroid Build Coastguard Worker self._filter_excluded_paths(root, directories, exclude_paths) 532*c2e18aaaSAndroid Build Coastguard Worker self._filter_excluded_paths(root, files, exclude_paths) 533*c2e18aaaSAndroid Build Coastguard Worker for directory in directories: 534*c2e18aaaSAndroid Build Coastguard Worker dir_path = pathlib.Path(root).joinpath(directory) 535*c2e18aaaSAndroid Build Coastguard Worker # Ignore non link directories because complicated to deal 536*c2e18aaaSAndroid Build Coastguard Worker # with file paths in include filters and unnecessary 537*c2e18aaaSAndroid Build Coastguard Worker if dir_path.is_symlink(): 538*c2e18aaaSAndroid Build Coastguard Worker dir_path.unlink() 539*c2e18aaaSAndroid Build Coastguard Worker for file in files: 540*c2e18aaaSAndroid Build Coastguard Worker file_path = pathlib.Path(root).joinpath(file) 541*c2e18aaaSAndroid Build Coastguard Worker if file_path.is_symlink(): 542*c2e18aaaSAndroid Build Coastguard Worker file_path.unlink() 543*c2e18aaaSAndroid Build Coastguard Worker elif file_path.relative_to(root_path).as_posix() not in file_infos_dict: 544*c2e18aaaSAndroid Build Coastguard Worker file_path.unlink() 545*c2e18aaaSAndroid Build Coastguard Worker deleted.append(file_path.as_posix()) 546*c2e18aaaSAndroid Build Coastguard Worker return deleted 547*c2e18aaaSAndroid Build Coastguard Worker 548*c2e18aaaSAndroid Build Coastguard Worker def _restore_directories( 549*c2e18aaaSAndroid Build Coastguard Worker self, 550*c2e18aaaSAndroid Build Coastguard Worker file_infos_dict: dict[str, _FileInfo], 551*c2e18aaaSAndroid Build Coastguard Worker root_path: str, 552*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: list[str], 553*c2e18aaaSAndroid Build Coastguard Worker ): 554*c2e18aaaSAndroid Build Coastguard Worker """Internal method to restore directories during snapshot restore.""" 555*c2e18aaaSAndroid Build Coastguard Worker for relative_path, file_info in file_infos_dict.items(): 556*c2e18aaaSAndroid Build Coastguard Worker if not file_info.is_directory: 557*c2e18aaaSAndroid Build Coastguard Worker continue 558*c2e18aaaSAndroid Build Coastguard Worker dir_path = pathlib.Path(root_path).joinpath(relative_path) 559*c2e18aaaSAndroid Build Coastguard Worker if self._is_excluded(dir_path.as_posix(), exclude_paths): 560*c2e18aaaSAndroid Build Coastguard Worker continue 561*c2e18aaaSAndroid Build Coastguard Worker dir_path.mkdir(parents=True, exist_ok=True) 562*c2e18aaaSAndroid Build Coastguard Worker os.chmod(dir_path, file_info.permissions) 563*c2e18aaaSAndroid Build Coastguard Worker 564*c2e18aaaSAndroid Build Coastguard Worker def _restore_files( 565*c2e18aaaSAndroid Build Coastguard Worker self, 566*c2e18aaaSAndroid Build Coastguard Worker file_infos_dict: dict[str, _FileInfo], 567*c2e18aaaSAndroid Build Coastguard Worker root_path: str, 568*c2e18aaaSAndroid Build Coastguard Worker exclude_paths: list[str], 569*c2e18aaaSAndroid Build Coastguard Worker ): 570*c2e18aaaSAndroid Build Coastguard Worker """Internal method to restore files during snapshot restore.""" 571*c2e18aaaSAndroid Build Coastguard Worker replaced = [] 572*c2e18aaaSAndroid Build Coastguard Worker for relative_path, file_info in file_infos_dict.items(): 573*c2e18aaaSAndroid Build Coastguard Worker file_path = pathlib.Path(root_path).joinpath(relative_path) 574*c2e18aaaSAndroid Build Coastguard Worker if self._is_excluded(file_path.as_posix(), exclude_paths): 575*c2e18aaaSAndroid Build Coastguard Worker continue 576*c2e18aaaSAndroid Build Coastguard Worker if file_info.symlink_target: 577*c2e18aaaSAndroid Build Coastguard Worker file_path.parent.mkdir(parents=True, exist_ok=True) 578*c2e18aaaSAndroid Build Coastguard Worker target = file_info.symlink_target 579*c2e18aaaSAndroid Build Coastguard Worker if bool(file_info.is_target_in_workspace): 580*c2e18aaaSAndroid Build Coastguard Worker target = pathlib.Path(root_path).joinpath(target) 581*c2e18aaaSAndroid Build Coastguard Worker file_path.parent.mkdir(parents=True, exist_ok=True) 582*c2e18aaaSAndroid Build Coastguard Worker file_path.symlink_to(target) 583*c2e18aaaSAndroid Build Coastguard Worker continue 584*c2e18aaaSAndroid Build Coastguard Worker 585*c2e18aaaSAndroid Build Coastguard Worker if file_info.is_directory: 586*c2e18aaaSAndroid Build Coastguard Worker continue 587*c2e18aaaSAndroid Build Coastguard Worker 588*c2e18aaaSAndroid Build Coastguard Worker if ( 589*c2e18aaaSAndroid Build Coastguard Worker file_path.exists() 590*c2e18aaaSAndroid Build Coastguard Worker and file_path.stat().st_mtime == file_info.timestamp 591*c2e18aaaSAndroid Build Coastguard Worker ): 592*c2e18aaaSAndroid Build Coastguard Worker continue 593*c2e18aaaSAndroid Build Coastguard Worker 594*c2e18aaaSAndroid Build Coastguard Worker file_path.parent.mkdir(parents=True, exist_ok=True) 595*c2e18aaaSAndroid Build Coastguard Worker file_path.unlink(missing_ok=True) 596*c2e18aaaSAndroid Build Coastguard Worker if not file_info.content_hash: 597*c2e18aaaSAndroid Build Coastguard Worker file_path.touch() 598*c2e18aaaSAndroid Build Coastguard Worker else: 599*c2e18aaaSAndroid Build Coastguard Worker file_path.write_bytes(self._blob_store.get(file_info.content_hash)) 600*c2e18aaaSAndroid Build Coastguard Worker os.utime(file_path, (file_info.timestamp, file_info.timestamp)) 601*c2e18aaaSAndroid Build Coastguard Worker os.chmod(file_path, file_info.permissions) 602*c2e18aaaSAndroid Build Coastguard Worker replaced.append(file_path.as_posix()) 603*c2e18aaaSAndroid Build Coastguard Worker return replaced 604