11from __future__ import annotations
22
33import asyncio
4+ import contextlib
45import io
56import os
67import shutil
8+ import sys
9+ import uuid
710from pathlib import Path
8- from typing import TYPE_CHECKING , Self
11+ from typing import TYPE_CHECKING , BinaryIO , Literal , Self
912
1013from zarr .abc .store import (
1114 ByteRequest ,
1922from zarr .core .common import AccessModeLiteral , concurrent_map
2023
2124if TYPE_CHECKING :
22- from collections .abc import AsyncIterator , Iterable
25+ from collections .abc import AsyncIterator , Iterable , Iterator
2326
2427 from zarr .core .buffer import BufferPrototype
2528
@@ -41,27 +44,55 @@ def _get(path: Path, prototype: BufferPrototype, byte_range: ByteRequest | None)
4144 return prototype .buffer .from_bytes (f .read ())
4245
4346
47+ if sys .platform == "win32" :
48+ # Per the os.rename docs:
49+ # On Windows, if dst exists a FileExistsError is always raised.
50+ _safe_move = os .rename
51+ else :
52+ # On Unix, os.rename silently replace files, so instead we use os.link like
53+ # atomicwrites:
54+ # https://github.com/untitaker/python-atomicwrites/blob/1.4.1/atomicwrites/__init__.py#L59-L60
55+ # This also raises FileExistsError if dst exists.
56+ def _safe_move (src : Path , dst : Path ) -> None :
57+ os .link (src , dst )
58+ os .unlink (src )
59+
60+
61+ @contextlib .contextmanager
62+ def _atomic_write (
63+ path : Path ,
64+ mode : Literal ["r+b" , "wb" ],
65+ exclusive : bool = False ,
66+ ) -> Iterator [BinaryIO ]:
67+ tmp_path = path .with_suffix (f".{ uuid .uuid4 ().hex } .partial" )
68+ try :
69+ with tmp_path .open (mode ) as f :
70+ yield f
71+ if exclusive :
72+ _safe_move (tmp_path , path )
73+ else :
74+ tmp_path .replace (path )
75+ except Exception :
76+ tmp_path .unlink (missing_ok = True )
77+ raise
78+
79+
4480def _put (
4581 path : Path ,
4682 value : Buffer ,
4783 start : int | None = None ,
4884 exclusive : bool = False ,
4985) -> int | None :
5086 path .parent .mkdir (parents = True , exist_ok = True )
87+ # write takes any object supporting the buffer protocol
88+ view = value .as_buffer_like ()
5189 if start is not None :
5290 with path .open ("r+b" ) as f :
5391 f .seek (start )
54- # write takes any object supporting the buffer protocol
55- f .write (value .as_buffer_like ())
92+ f .write (view )
5693 return None
5794 else :
58- view = value .as_buffer_like ()
59- if exclusive :
60- mode = "xb"
61- else :
62- mode = "wb"
63- with path .open (mode = mode ) as f :
64- # write takes any object supporting the buffer protocol
95+ with _atomic_write (path , "wb" , exclusive = exclusive ) as f :
6596 return f .write (view )
6697
6798
0 commit comments