Skip to content

Commit e4f0ac7

Browse files
committed
Publish viewer downloads on WASM without hard links
1 parent 69c9f0d commit e4f0ac7

3 files changed

Lines changed: 50 additions & 5 deletions

File tree

‎src/blosc2/b2view/ordinary_file.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from urllib.parse import urlsplit
99
from urllib.request import url2pathname
1010

11+
import blosc2
1112
from blosc2.core import find_url_separator, fsspec_filesystem, is_fsspec_url, parse_container_url
1213

1314
MAX_READ_BYTES = 16 << 20
@@ -71,6 +72,19 @@ def ordinary_filesystem(source, storage_options=None):
7172
return fsspec_filesystem(source, options)
7273

7374

75+
def _publish_download(temporary, destination, overwrite):
76+
if overwrite:
77+
os.replace(temporary, destination)
78+
elif blosc2.IS_WASM:
79+
# Emscripten has no hard links. Its single-threaded virtual FS cannot
80+
# interleave a writer between this check and rename (no callbacks/awaits).
81+
if os.path.lexists(destination):
82+
raise FileExistsError(destination)
83+
os.rename(temporary, destination)
84+
else:
85+
os.link(temporary, destination)
86+
87+
7488
class OrdinaryFile:
7589
"""A direct regular file with bounded reads and independent transfer aliases.
7690
@@ -186,10 +200,7 @@ def download(self, destination, *, overwrite=False, progress=None, cancel=None):
186200
self._check_open()
187201
if cancel is not None and cancel():
188202
raise InterruptedError("File operation cancelled")
189-
if overwrite:
190-
os.replace(temporary, destination)
191-
else:
192-
os.link(temporary, destination)
203+
_publish_download(temporary, destination, overwrite)
193204
return str(destination)
194205
finally:
195206
Path(temporary).unlink(missing_ok=True)

‎tests/b2view/test_directories.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def test_hdf5_mount_in_directory(tmp_path):
7474

7575

7676
def test_chained_fsspec_directory(tmp_path):
77+
pytest.importorskip("fsspec")
7778
import zipfile
7879

7980
archive = tmp_path / "files.zip"

‎tests/b2view/test_ordinary_files.py‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def test_empty_unknown_pdf_and_missing_files(tmp_path):
150150

151151

152152
def test_chained_fsspec_file_and_storage_options(tmp_path, monkeypatch):
153+
pytest.importorskip("fsspec")
153154
import zipfile
154155

155156
from blosc2.b2view import ordinary_file
@@ -177,7 +178,7 @@ def filesystem(source, options):
177178
assert all(options == {"custom": "value"} for options in seen)
178179

179180

180-
def test_publication_race_and_late_cancellation(tmp_path, monkeypatch):
181+
def test_late_cancellation(tmp_path):
181182
source = tmp_path / "source.txt"
182183
source.write_bytes(b"hello")
183184
file = OrdinaryFile(source)
@@ -192,6 +193,14 @@ def progress(done, total):
192193
file.download(destination, progress=progress, cancel=lambda: cancelled)
193194
assert not destination.exists()
194195
assert not list(tmp_path.glob(".b2view-download-*"))
196+
197+
198+
@pytest.mark.skipif(blosc2.IS_WASM, reason="emscripten has no hard links")
199+
def test_publication_race(tmp_path, monkeypatch):
200+
source = tmp_path / "source.txt"
201+
source.write_bytes(b"hello")
202+
file = OrdinaryFile(source)
203+
destination = tmp_path / "destination.txt"
195204
original = __import__("os").link
196205

197206
def race(temporary, target):
@@ -205,6 +214,30 @@ def race(temporary, target):
205214
assert not list(tmp_path.glob(".b2view-download-*"))
206215

207216

217+
@pytest.mark.parametrize("competing_writer", [False, True])
218+
def test_wasm_download_publication(tmp_path, monkeypatch, competing_writer):
219+
monkeypatch.setattr(blosc2, "IS_WASM", True)
220+
source = tmp_path / "source.txt"
221+
source.write_bytes(b"hello")
222+
destination = tmp_path / "destination.txt"
223+
file = OrdinaryFile(source)
224+
225+
def progress(done, total):
226+
if competing_writer:
227+
destination.write_bytes(b"existing")
228+
229+
if competing_writer:
230+
with pytest.raises(FileExistsError):
231+
file.download(destination, progress=progress)
232+
assert destination.read_bytes() == b"existing"
233+
else:
234+
file.download(destination, progress=progress)
235+
assert destination.read_bytes() == b"hello"
236+
with pytest.raises(FileExistsError):
237+
file.download(destination)
238+
assert not list(tmp_path.glob(".b2view-download-*"))
239+
240+
208241
@pytest.mark.parametrize(
209242
"source",
210243
[

0 commit comments

Comments
 (0)