From e4820b7be71a429bfadc1d7d9045cf8c3b016ade Mon Sep 17 00:00:00 2001 From: Alex <25013571+alexhb1@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:05:02 +0100 Subject: [PATCH] Fix temp filename max size --- shelfmark/download/fs.py | 12 ++++++++++-- tests/core/test_download_processing.py | 14 ++++++++++++++ tests/core/test_hardlink.py | 21 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/shelfmark/download/fs.py b/shelfmark/download/fs.py index a52c0022..cfe1eb76 100644 --- a/shelfmark/download/fs.py +++ b/shelfmark/download/fs.py @@ -101,6 +101,8 @@ def run_blocking_io[T](func: Callable[..., T], *args: Any, **kwargs: Any) -> T: _VERIFY_IO_WAIT_SECONDS = 3.0 _PUBLISH_VERIFY_RETRY_SECONDS = 0.25 +_TEMPFILE_PREFIX = ".shelfmark." +_TEMPFILE_SUFFIX = ".tmp" def _verify_transfer_size( @@ -339,10 +341,16 @@ def _hardlink_not_supported(error: OSError) -> bool: def _create_temp_path(dest_path: Path) -> Path: + """Create a destination-adjacent temp file without inheriting the full basename. + + Reusing the entire destination filename in the temp prefix can push otherwise + valid long names over the filesystem component limit once `tempfile` adds its + random suffix. + """ fd, temp_path = run_blocking_io( tempfile.mkstemp, - prefix=f".{dest_path.name}.", - suffix=".tmp", + prefix=_TEMPFILE_PREFIX, + suffix=_TEMPFILE_SUFFIX, dir=str(dest_path.parent), ) run_blocking_io(os.close, fd) diff --git a/tests/core/test_download_processing.py b/tests/core/test_download_processing.py index 35f701ff..6836fa25 100644 --- a/tests/core/test_download_processing.py +++ b/tests/core/test_download_processing.py @@ -147,6 +147,20 @@ def test_multiple_collisions(self, tmp_path): assert result == tmp_path / "dest_3.txt" + def test_long_destination_name(self, tmp_path): + """Copies long-but-valid destination names without overflowing temp filenames.""" + from shelfmark.download.fs import atomic_copy as _atomic_copy + + source = tmp_path / "source.epub" + source.write_bytes(b"epub content") + dest = tmp_path / f"{'A' * 240}.epub" + + result = _atomic_copy(source, dest) + + assert result == dest + assert result.exists() + assert result.read_bytes() == b"epub content" + def test_preserves_extension(self, tmp_path): """Keeps extension when adding counter suffix.""" from shelfmark.download.fs import atomic_copy as _atomic_copy diff --git a/tests/core/test_hardlink.py b/tests/core/test_hardlink.py index 36eab0cc..cc58571c 100644 --- a/tests/core/test_hardlink.py +++ b/tests/core/test_hardlink.py @@ -350,6 +350,27 @@ def test_cross_filesystem_fallback(self): assert not source.exists() assert result.read_text() == "content" + def test_cross_filesystem_fallback_handles_long_destination_name(self, tmp_path, monkeypatch): + """Cross-filesystem fallback handles long destination names safely.""" + import errno + + from shelfmark.download.fs import atomic_move as _atomic_move + + source = tmp_path / "source.epub" + source.write_text("content") + dest = tmp_path / f"{'A' * 240}.epub" + + def _raise_exdev(*_args, **_kwargs): + raise OSError(errno.EXDEV, "Cross-device link") + + monkeypatch.setattr(os, "rename", _raise_exdev) + + result = _atomic_move(source, dest) + + assert result == dest + assert not source.exists() + assert result.read_text() == "content" + def test_cross_filesystem_permission_fallback(self, tmp_path, monkeypatch): """Falls back to copy when cross-filesystem move hits permission error.""" import errno