@@ -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+
313324def _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
322333def _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