Skip to content

Commit b078a6a

Browse files
committed
Preserve newer concurrent export destinations
1 parent 073a659 commit b078a6a

1 file changed

Lines changed: 30 additions & 1 deletion

File tree

‎src/openstatspec/spss/sav.py‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,17 @@ def _path_entry_exists(path: Path) -> bool:
310310
return path.exists() or path.is_symlink()
311311

312312

313+
_DESTINATION_IDENTITY_UNCHECKED = object()
314+
315+
316+
def _destination_identity(path: Path) -> tuple[int, int] | None:
317+
"""Identify the current directory entry without following symlinks."""
318+
if not _path_entry_exists(path):
319+
return None
320+
status = path.lstat()
321+
return status.st_dev, status.st_ino
322+
323+
313324
def _reserve_export_backup(destination: Path) -> Path:
314325
descriptor, name = mkstemp(
315326
dir=destination.parent, prefix=f".{destination.name}.",
@@ -321,7 +332,17 @@ def _reserve_export_backup(destination: Path) -> Path:
321332

322333
def _restore_export_destination(
323334
*, destination: Path, backup: Path, had_previous: bool,
335+
expected_identity: tuple[int, int] | None | object = (
336+
_DESTINATION_IDENTITY_UNCHECKED
337+
),
324338
) -> None:
339+
if (
340+
expected_identity is not _DESTINATION_IDENTITY_UNCHECKED
341+
and _destination_identity(destination) != expected_identity
342+
):
343+
raise FileExistsError(
344+
"The export destination is no longer owned by this operation."
345+
)
325346
if had_previous:
326347
os.replace(backup, destination)
327348
else:
@@ -493,6 +514,7 @@ def _export_staging_directory(
493514
destination=destination,
494515
backup=backup,
495516
had_previous=had_previous,
517+
expected_identity=publication_state["published_identity"],
496518
)
497519
except Exception as restore_error:
498520
_raise_export_cleanup_failed(
@@ -633,8 +655,14 @@ def export_sav_dataset(
633655
os.replace(destination_path, backup)
634656
backup_installed = True
635657
os.replace(staged_destination, destination_path)
658+
published_identity = _destination_identity(destination_path)
659+
if published_identity is None:
660+
raise FileNotFoundError(
661+
"The published export destination disappeared."
662+
)
636663
publication_state.update({
637664
"published": True,
665+
"published_identity": published_identity,
638666
"backup": backup,
639667
"staged": staged_destination,
640668
"had_previous": had_previous,
@@ -645,7 +673,7 @@ def export_sav_dataset(
645673
try:
646674
_restore_export_destination(
647675
destination=destination_path, backup=backup,
648-
had_previous=had_previous,
676+
had_previous=had_previous, expected_identity=None,
649677
)
650678
except Exception as cleanup_error:
651679
_raise_export_cleanup_failed(
@@ -707,6 +735,7 @@ def export_sav_dataset(
707735
_restore_export_destination(
708736
destination=destination_path, backup=backup,
709737
had_previous=had_previous,
738+
expected_identity=publication_state["published_identity"],
710739
)
711740
except Exception as cleanup_error:
712741
_raise_export_cleanup_failed(

0 commit comments

Comments
 (0)