Skip to content

Commit a0e2931

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 e0028f8 commit a0e2931

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
@@ -1725,10 +1725,32 @@ async def get(
17251725
)
17261726
wmode = "w" if as_text else "wb"
17271727

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

0 commit comments

Comments
 (0)