5959import shutil
6060import tempfile
6161from pathlib import Path
62- from typing import TYPE_CHECKING , cast
62+ from typing import IO , TYPE_CHECKING , cast
6363from urllib import parse
6464
6565from tuf .api import exceptions
6969from tuf .ngclient .urllib3_fetcher import Urllib3Fetcher
7070
7171if TYPE_CHECKING :
72+ from collections .abc import Iterator
73+
7274 from tuf .ngclient .fetcher import FetcherInterface
7375
7476logger = logging .getLogger (__name__ )
7577
78+ try :
79+ # advisory file locking for posix
80+ import fcntl
81+ def _lock_file (f : IO ) -> None :
82+ if f .writable ():
83+ fcntl .lockf (f , fcntl .LOCK_EX )
84+
85+ except ModuleNotFoundError :
86+ # Windows file locking
87+ import msvcrt
88+
89+ def _lock_file (f : IO ) -> None :
90+ # On Windows we lock bytes, not the file
91+ f .write (b"\0 " )
92+ f .flush ()
93+ f .seek (0 )
94+ msvcrt .locking (f .fileno (), msvcrt .LK_LOCK , 1 )
95+
7696
7797class Updater :
7898 """Creates a new ``Updater`` instance and loads trusted root metadata.
@@ -139,8 +159,23 @@ def __init__(
139159 self ._trusted_set = TrustedMetadataSet (
140160 bootstrap , self .config .envelope_type
141161 )
142- self ._persist_root (self ._trusted_set .root .version , bootstrap )
143- self ._update_root_symlink ()
162+ with self ._lock_metadata ():
163+ self ._persist_root (self ._trusted_set .root .version , bootstrap )
164+ self ._update_root_symlink ()
165+
166+
167+ @contextlib .contextmanager
168+ def _lock_metadata (self ) -> Iterator [None ]:
169+ """Context manager for locking the metadata directory."""
170+ # Ensure the whole metadata directory structure exists
171+ rootdir = Path (self ._dir , "root_history" )
172+ rootdir .mkdir (exist_ok = True , parents = True )
173+
174+ with open (os .path .join (self ._dir , ".lock" ), "wb" ) as f :
175+ logger .debug ("Getting metadata lock..." )
176+ _lock_file (f )
177+ yield
178+ logger .debug ("Releasing metadata lock" )
144179
145180 def refresh (self ) -> None :
146181 """Refresh top-level metadata.
@@ -166,10 +201,11 @@ def refresh(self) -> None:
166201 DownloadError: Download of a metadata file failed in some way
167202 """
168203
169- self ._load_root ()
170- self ._load_timestamp ()
171- self ._load_snapshot ()
172- self ._load_targets (Targets .type , Root .type )
204+ with self ._lock_metadata ():
205+ self ._load_root ()
206+ self ._load_timestamp ()
207+ self ._load_snapshot ()
208+ self ._load_targets (Targets .type , Root .type )
173209
174210 def _generate_target_file_path (self , targetinfo : TargetFile ) -> str :
175211 if self .target_dir is None :
@@ -205,9 +241,14 @@ def get_targetinfo(self, target_path: str) -> TargetFile | None:
205241 ``TargetFile`` instance or ``None``.
206242 """
207243
208- if Targets .type not in self ._trusted_set :
209- self .refresh ()
210- return self ._preorder_depth_first_walk (target_path )
244+ with self ._lock_metadata ():
245+ if Targets .type not in self ._trusted_set :
246+ # refresh
247+ self ._load_root ()
248+ self ._load_timestamp ()
249+ self ._load_snapshot ()
250+ self ._load_targets (Targets .type , Root .type )
251+ return self ._preorder_depth_first_walk (target_path )
211252
212253 def find_cached_target (
213254 self ,
@@ -335,7 +376,6 @@ def _persist_root(self, version: int, data: bytes) -> None:
335376 "root_history/1.root.json").
336377 """
337378 rootdir = Path (self ._dir , "root_history" )
338- rootdir .mkdir (exist_ok = True , parents = True )
339379 self ._persist_file (str (rootdir / f"{ version } .root.json" ), data )
340380
341381 def _persist_file (self , filename : str , data : bytes ) -> None :
0 commit comments