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

Безопасно перемещаем файлы

Прямую сортировку выполняет apply_plan(): перед каждым перемещением функция повторно проверяет конфликт. Undo остаётся отдельной обратной операцией.

SafeSort · Часть 4 из 6
Git и GitHubПланированиеПроектРеализацияТесты и CIРелиз
GitHubIssue #5 · Project «SafeSort — первый релиз»
Add explicit apply operation
Area: SafetyPriority: High
git switch -c feat/apply-operation

До сих пор ни одна функция в прямой цепочке сортировки SafeSort не меняла файловую систему. Модуль executor.py выполняет прямую операцию: функция apply_plan() получает готовый план и перемещает только перечисленные в нём файлы. Обратная операция undo тоже перемещает файлы, но восстанавливает их по записанному манифесту.

Файловая системакаталог с файламиСканерscan()Классификаторclassify()Executorapply_plan()
В прямой цепочке сортировки запись начинается только в executor; undo образует отдельную обратную цепочку.
Было
Downloads/
report.pdf
photo.jpg
archive.zip
Стало
Downloads/
Sorted/
documents/
report.pdf
images/
photo.jpg
archives/
archive.zip
apply выполняет прямую сортировку; undo позднее сможет выполнить обратные перемещения по манифесту.
src/safesort/executor.py
def apply_plan(plan: SortPlan) -> list[CompletedMove]:
    results = []
    for operation in plan.operations:
        source, destination = operation.source, operation.destination
        destination.parent.mkdir(parents=True, exist_ok=True)

        if destination.exists() or destination.is_symlink():
            results.append(CompletedMove(source, destination, completed=False,
                                          error="destination already exists"))
            continue

        shutil.move(str(source), str(destination))
        results.append(CompletedMove(source, destination, completed=True))
    return results
Партия перемещений — не транзакция базы данных
Если один файл в середине партии не удаётся переместить (например, права доступа изменились между сканированием и выполнением), это не должно остановить обработку остальных файлов и не должно откатывать уже выполненные перемещения — файловая система не умеет так же атомарно откатывать группу операций, как это делает база данных. Поэтому apply_plan() обрабатывает каждое перемещение независимо и в конце возвращает честный отчёт о том, что реально произошло с каждым файлом.

Обратите внимание на проверку destination.exists() прямо перед перемещением, хотя планировщик уже избегал этого конфликта на этапе построения плана. Между построением плана и его выполнением на диске мог появиться новый файл — например, если пользователь сам что-то туда положил в этот момент. Без повторной проверки shutil.move() в системах на основе POSIX молча перезаписал бы такой файл — а SafeSort не перезаписывает файлы молча ни при каких обстоятельствах.

~/safesort $ safesort apply ~/Downloads
Applied 9 moves.
Manifest written to:
.safesort/history/20260824T085400685280.json

Тест-чекпойнт: apply меняет только заявленный путь

tests/test_executor.py
def test_apply_plan_moves_one_file(tmp_path):
    source = tmp_path / "report.pdf"
    destination = tmp_path / "Sorted/documents/report.pdf"
    source.write_bytes(b"report")
    plan = SortPlan(tmp_path, (MoveOperation(source, destination),))

    [result] = apply_plan(plan)

    assert result.completed is True
    assert destination.read_bytes() == b"report"
    assert not source.exists()
Практика: перемещаем файлы во временном каталоге
Нужен доступ к настоящей файловой системе — выполните локально в VS Code, PyCharm или Jupyter
Практика выполняется локально
Открыть практику →
Чекпойнт · Issue #5
git commit -m "feat: add explicit apply operation"
Слился как Pull Request #18. В описании PR явно указано Closes #5 в описании, поэтому Issue #5 закрылся автоматически в момент слияния.
Статус в реальном Project: Done