Skip to content

Commit 90d3289

Browse files
Use atomic file operations to ensure valid downloads
When downloading and caching files such as Python wheels, eggs, or zip files, it's crucial to ensure that the files are fully and correctly written before they are made available for use. This change implements atomic file operations by writing to a temporary file first and then renaming it to the target filename once the write is complete and the file has been validated. This prevents issues where a partially written or corrupt file could be read by other processes. Signed-off-by: Marcel Bochtler <[email protected]>
1 parent 378557a commit 90d3289

File tree

1 file changed

+26
-4
lines changed

1 file changed

+26
-4
lines changed

src/python_inspector/utils_pypi.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1730,10 +1730,32 @@ async def get(
17301730
)
17311731
wmode = "w" if as_text else "wb"
17321732

1733-
# acquire lock and wait until timeout to get a lock or die
1734-
with lockfile.FileLock(lock_file).locked(timeout=PYINSP_CACHE_LOCK_TIMEOUT):
1735-
async with aiofiles.open(cached, mode=wmode) as fo:
1736-
await fo.write(content)
1733+
# Use atomic file operations.
1734+
temp_file = f"{cached}.tmp.{os.getpid()}"
1735+
1736+
try:
1737+
# acquire lock and wait until timeout to get a lock or die
1738+
with lockfile.FileLock(lock_file).locked(timeout=PYINSP_CACHE_LOCK_TIMEOUT):
1739+
async with aiofiles.open(temp_file, mode=wmode) as fo:
1740+
await fo.write(content)
1741+
1742+
# Validate zip files before making them "live"
1743+
if not as_text and path_or_url.endswith((".whl", ".egg", ".zip")):
1744+
if not zipfile.is_zipfile(temp_file):
1745+
raise Exception(
1746+
f"Downloaded file is not a valid zip: {path_or_url}\n"
1747+
f"Size: {os.path.getsize(temp_file)} bytes"
1748+
)
1749+
1750+
# Atomic rename - readers will never see partial/corrupt file
1751+
os.rename(temp_file, cached)
1752+
1753+
except Exception:
1754+
# Clean up temp file on any error
1755+
if os.path.exists(temp_file):
1756+
os.remove(temp_file)
1757+
raise
1758+
17371759
return content, cached
17381760
else:
17391761
if TRACE_DEEP:

0 commit comments

Comments
 (0)