Глава 23 · Часть IV · Реализуем SafeSort

Отмена последней операции

undo восстанавливает файлы из журнала — и отказывается затирать то, что успело появиться на исходном месте.

SafeSort · Часть 4 из 6Реализация
GitHubIssue #8 · Project «SafeSort — первый релиз»
Add undo
Area: SafetyPriority: High
git switch -c feat/manifest-and-undo

Команда undo находит последний манифест, читает список выполненных перемещений и возвращает файлы туда, откуда они были взяты. Здесь действует то же правило, что и во всей программе: ничего не перезаписывать молча.

manifest.jsonпоследняя записьсписокперемещенийdestination → sourceundoвосстанавливает файлы
Тот же путь, что и у apply, но в обратную сторону.
src/safesort/manifest.py
def undo(manifest: OperationManifest) -> UndoResult:
    restored, conflicts = [], []
    for move in manifest.moves:
        if not move.completed:
            continue
        source, destination = move.source, move.destination

        if source.exists() or source.is_symlink():
            conflicts.append(UndoConflict(source, destination,
                "a file already exists at the original location"))
            continue

        shutil.move(str(destination), str(source))
        restored.append(CompletedMove(destination, source, completed=True))
    return UndoResult(restored=tuple(restored), conflicts=tuple(conflicts))
Отмена не восстанавливает то, чего не было выполнено
Цикл проверяет if not move.completed: continue — перемещения, которые не удались во время apply, никогда не касались диска, и отменять для них нечего.

Сначала — обычный успешный откат:

~/safesort $ safesort undo ~/Downloads
Restored 9 moves.

Конфликт при отмене — реальный сценарий

Что произойдёт, если запустить undo ещё раз, когда файлы уже на исходных местах? Простое перемещение назад стёрло бы то, что там уже есть. Вместо этого SafeSort проверяет исходное место перед восстановлением каждого файла и, если там уже что-то есть, отказывается восстанавливать именно этот файл:

~/safesort $ safesort undo ~/Downloads
ERROR:safesort.manifest:Refusing to undo Sorted/other/bigfile_b.dat -> bigfile_b.dat: a file already exists at the original location: bigfile_b.dat
ERROR:safesort.manifest:Refusing to undo Sorted/documents/notes.txt -> notes.txt: a file already exists at the original location: notes.txt
… (ещё 7 таких строк)
Restored 0 moves.
9 moves could not be restored:
Sorted/other/bigfile_b.dat -> bigfile_b.dat: a file already exists at the original location: bigfile_b.dat
… (ещё 8 строк)

Отказ — а не тихая перезапись. Каждый конфликт обрабатывается независимо: файлы без конфликта восстановились бы, даже если часть списка отказала.

Тест-чекпойнт: обратная операция

tests/test_manifest.py
def test_undo_restores_original_location(tmp_path):
    source = tmp_path / "report.pdf"
    source.write_bytes(b"report")
    plan = build_plan(scan(tmp_path, Config()), tmp_path, Config())
    moves = apply_plan(plan)
    manifest, _ = write_manifest(tmp_path, moves)

    result = undo(manifest)

    assert len(result.restored) == 1
    assert source.read_bytes() == b"report"
Практика: отмена и конфликт при восстановлении
Нужен доступ к настоящей файловой системе — выполните локально в VS Code, PyCharm или Jupyter
Практика выполняется локально
Открыть практику →
Чекпойнт · Issue #8
git commit -m "feat: record operation manifest and add undo"
Та же ветка и тот же Pull Request #19, что закрыл Issue #7 на предыдущей странице.
Статус в реальном Project: Done