| 201 | self.write_journal("active") |
| 202 | |
| 203 | def rollback(self) -> None: |
| 204 | # Restore children before parents so directory deletes cannot remove |
| 205 | # paths that still need to be restored from a more specific backup. |
| 206 | for target, backup in sorted( |
| 207 | self.entries.items(), |
| 208 | key=lambda item: len(item[0].parts), |
| 209 | reverse=True, |
| 210 | ): |
| 211 | # A hardlinked dir backup supports an O(touched) inode-diff restore |
| 212 | # (leave untouched shared-inode files, only touch changed ones) — |
| 213 | # do NOT rmtree it first, which would discard those shared inodes. |
| 214 | if target.is_dir() and target in self.hardlinked_dirs: |
| 215 | if backup is not None and backup.is_dir(): |
| 216 | _restore_hardlinked_dir(backup, target) |
| 217 | else: |
| 218 | shutil.rmtree(target, ignore_errors=True) # new dir, no backup |
| 219 | continue |
| 220 | # Non-hardlinked (file, or copied dir): unconditional remove + restore. |
| 221 | if target.is_dir(): |
| 222 | shutil.rmtree(target, ignore_errors=True) |
| 223 | else: |
| 224 | target.unlink(missing_ok=True) |
| 225 | if backup is None: |
| 226 | continue |
| 227 | target.parent.mkdir(parents=True, exist_ok=True) |
| 228 | if backup.is_dir(): |
| 229 | shutil.copytree(backup, target) |
| 230 | else: |
| 231 | _copy_file_atomic(backup, target) |
| 232 | self.write_journal("rolled_back") |
| 233 | |
| 234 | def rollback_best_effort(self) -> Exception | None: |
| 235 | try: |