33# SPDX-License-Identifier: MIT
44
55import hashlib
6- import shutil
6+ import sys
77import time
88import urllib
99from contextlib import contextmanager
1010from pathlib import Path
1111
12- from . import _progress
12+ from . import _progress , _utils
1313
1414__all__ = [
1515 "atomic_file" ,
@@ -86,21 +86,26 @@ def atomic_file(
8686 if not _file_exists_and_is_fresh (target , ttl ):
8787 with _create_key_tmpdir (cache_dir , key ) as tmpdir :
8888 if tmpdir :
89- fetchfunc (tmpdir / filename )
90- _swap_in_fetched_file (
91- target ,
92- tmpdir / filename ,
93- timeout = timeout_for_read_elsewhere ,
94- )
95- _add_url_file (keydir , key_url )
89+ filepath = tmpdir / filename
90+ try :
91+ fetchfunc (filepath )
92+ _utils .swap_in_file (
93+ target ,
94+ filepath ,
95+ timeout = timeout_for_read_elsewhere ,
96+ )
97+ _add_url_file (keydir , key_url )
98+ finally :
99+ _utils .unlink_tempfile (filepath )
96100 else : # Somebody else is currently fetching
97101 _wait_for_dir_to_vanish (
98102 _key_tmpdir (cache_dir , key ),
99103 timeout = timeout_for_fetch_elsewhere ,
100104 )
101105 if not _file_exists_and_is_fresh (target , ttl = 2 ** 63 ):
102106 raise Exception (
103- f"Fetching of file { target } appears to have been completed elsewhere, but file does not exist"
107+ f"Another process was fetching { target } but the file is not present; "
108+ f"the other process may have failed or been interrupted."
104109 )
105110 return target
106111
@@ -135,17 +140,21 @@ def permanent_directory(
135140 if not keydir .is_dir ():
136141 with _create_key_tmpdir (cache_dir , key ) as tmpdir :
137142 if tmpdir :
138- fetchfunc (tmpdir )
139- _move_in_fetched_directory (keydir , tmpdir )
140- _add_url_file (keydir , key_url )
143+ try :
144+ fetchfunc (tmpdir )
145+ _move_in_fetched_directory (keydir , tmpdir )
146+ _add_url_file (keydir , key_url )
147+ finally :
148+ _utils .rmtree_tempdir (tmpdir )
141149 else : # Somebody else is currently fetching
142150 _wait_for_dir_to_vanish (
143151 _key_tmpdir (cache_dir , key ),
144152 timeout = timeout_for_fetch_elsewhere ,
145153 )
146154 if not keydir .is_dir ():
147155 raise Exception (
148- f"Fetching of directory { keydir } appears to have been completed elsewhere, but directory does not exist"
156+ f"Another process was fetching { keydir } but the directory is not present; "
157+ f"the other process may have failed or been interrupted"
149158 )
150159 return keydir
151160
@@ -181,8 +190,7 @@ def _create_key_tmpdir(cache_dir, key):
181190 try :
182191 yield tmpdir
183192 finally :
184- if tmpdir .is_dir ():
185- shutil .rmtree (tmpdir )
193+ _utils .rmtree_tempdir (tmpdir )
186194
187195
188196def _key_directory (cache_dir : Path , key ) -> Path :
@@ -193,43 +201,6 @@ def _key_tmpdir(cache_dir: Path, key) -> Path:
193201 return cache_dir / "v0" / Path ("fetching" , * key )
194202
195203
196- def _swap_in_fetched_file (target , tmpfile , timeout , progress = False ):
197- # On POSIX, we only need to try once to move tmpfile to target; this will
198- # work even if target is opened by others, and any failure (e.g.
199- # insufficient permissions) is permanent.
200- # On Windows, there is the case where the file is open by others (busy); we
201- # should wait a little and retry in this case. It is not possible to do
202- # this cleanly, because the error we get when the target is busy is "Access
203- # is denied" (PermissionError, a subclass of OSError, with .winerror = 5),
204- # which is indistinguishable from the case where target permanently has bad
205- # permissions.
206- # But because this implementation is only intended for small files that
207- # will not be kept open for long, and because permanent bad permissions is
208- # not expected in the typical use case, we can do something that almost
209- # always results in the intended behavior.
210- WINDOWS_ERROR_ACCESS_DENIED = 5
211-
212- target .parent .mkdir (parents = True , exist_ok = True )
213- with _progress .indefinite (
214- enabled = progress , text = "File busy; waiting"
215- ) as update_pbar :
216- for wait_seconds in _backoff_seconds (0.001 , 0.5 , timeout ):
217- try :
218- tmpfile .replace (target )
219- except OSError as e :
220- if (
221- hasattr (e , "winerror" )
222- and e .winerror == WINDOWS_ERROR_ACCESS_DENIED
223- and wait_seconds > 0
224- ):
225- time .sleep (wait_seconds )
226- update_pbar ()
227- continue
228- raise
229- else :
230- return
231-
232-
233204def _move_in_fetched_directory (target , tmpdir ):
234205 target .parent .mkdir (parents = True , exist_ok = True )
235206 tmpdir .replace (target )
@@ -241,10 +212,18 @@ def _add_url_file(keydir, key_url):
241212
242213
243214def _wait_for_dir_to_vanish (directory , timeout , progress = True ):
215+ print (
216+ "cjdk: Another process is currently downloading the same file" ,
217+ file = sys .stderr ,
218+ )
219+ print (
220+ f"cjdk: If you are sure this is not the case (e.g., previous download crashed), try again after deleting the directory { directory } " ,
221+ file = sys .stderr ,
222+ )
244223 with _progress .indefinite (
245224 enabled = progress , text = "Already downloading; waiting"
246225 ) as update_pbar :
247- for wait_seconds in _backoff_seconds (0.001 , 0.5 , timeout ):
226+ for wait_seconds in _utils . backoff_seconds (0.001 , 0.5 , timeout ):
248227 if not directory .is_dir ():
249228 return
250229 if wait_seconds < 0 :
@@ -253,30 +232,3 @@ def _wait_for_dir_to_vanish(directory, timeout, progress=True):
253232 )
254233 time .sleep (wait_seconds )
255234 update_pbar ()
256-
257-
258- def _backoff_seconds (initial_interval , max_interval , max_total , factor = 1.5 ):
259- """
260- Yield intervals to sleep after repeated attempts with exponential backoff.
261-
262- The last-yielded value is -1. When -1 is received, the caller should make
263- the final attempt before giving up.
264- """
265- assert initial_interval > 0
266- assert max_total >= 0
267- assert factor > 1
268- total = 0
269- next_interval = initial_interval
270- while max_total > 0 :
271- next_total = total + next_interval
272- if next_total > max_total :
273- remaining = max_total - total
274- if remaining > 0.01 :
275- yield remaining
276- break
277- yield next_interval
278- total = next_total
279- next_interval *= factor
280- if next_interval > max_interval :
281- next_interval = max_interval
282- yield - 1
0 commit comments