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

Сканируем каталог

scan() строго читает файловую систему — обходит каталог рекурсивно и не меняет ни одного файла.

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

Первый шаг SafeSort — обойти каталог и составить список файлов. Функция scan() — одна из трёх строго читающих команд SafeSort: она только вызывает iterdir() и stat(), ни разу не создавая, не перемещая и не удаляя ничего на диске.

Downloads/
report.pdf
cat.jpg
archive.zip
Файловая системакаталог с файламиСканерscan()FileInfoпуть, размер,расширение
Эта цепочка будет расти: следующие страницы добавят в неё новые звенья.
src/safesort/scanner.py
def scan(root: Path, config: Config) -> list[FileInfo]:
    root = Path(root)
    excluded = config.excluded_names()
    results: list[FileInfo] = []
    _scan_dir(root, excluded, results)
    return results

Сам обход рекурсивный: функция _scan_dir() заходит в каждый подкаталог и вызывает себя снова:

src/safesort/scanner.py
for entry in entries:
    if entry.is_symlink():
        continue  # символические ссылки пропускаются — см. следующую страницу
    if entry.is_dir():
        if entry.name in excluded:
            continue  # исключённые каталоги пропускаются — см. следующую страницу
        _scan_dir(entry, excluded, results)
    elif entry.is_file():
        size = entry.stat().st_size
        results.append(FileInfo(path=entry, size=size, extension=entry.suffix.lower()))
Каталог, который нельзя прочитать, не должен остановить всё сканирование
Если для какого-то подкаталога недостаточно прав доступа, iterdir() вызывает PermissionError. Реализация _scan_dir() перехватывает эту ошибку для каждого подкаталога отдельно, записывает предупреждение в журнал и продолжает сканировать остальные каталоги — так один недоступный подкаталог не обрывает весь просмотр целиком. Позже мы рассмотрим это подробнее вместе с остальными ошибками файловой системы.

Проверить сканер можно прямо сейчас — реальный вывод:

~/safesort $ safesort scan ~/Downloads
Files scanned: 9
Documents: 3
Images: 1
Archives: 1
Other: 4

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

tests/test_scanner.py
def test_scan_finds_file_without_changing_it(tmp_path):
    source = tmp_path / "report.pdf"
    source.write_bytes(b"report")

    files = scan(tmp_path, Config())

    assert [item.path for item in files] == [source]
    assert source.read_bytes() == b"report"

Сначала требование наблюдается на одном примере, затем тест сохраняет его как контракт. Поздняя секция тестирования расширит эту основу fixtures и edge cases.

Практика: сканируем настоящий каталог
Нужен доступ к настоящей файловой системе — выполните локально в VS Code, PyCharm или Jupyter
Практика выполняется локально
Открыть практику →