xref: /aosp_15_r20/tools/asuite/atest/integration_tests/snapshot.py (revision c2e18aaa1096c836b086f94603d04f4eb9cf37f5)
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