""" Directory indexer — watches a group's roots and maintains a GroupIndex. Uses watchdog for filesystem events. On any change the affected file is re-scanned and the GroupIndex is updated. File metadata (blake3 hash, size, type) is computed on first scan; hashing runs in a thread pool. Two properties are worth stating because they are what the code is shaped around, not incidental: **A root that goes away freezes; it never empties.** Unmounting a volume either makes watchdog emit a deletion for every file under it or presents an empty directory to the next scan. Both would propagate deletions for a whole library as though the owner had erased it, to every member. So a deletion is acted on only once the root it belongs to has been confirmed still readable, and a root that is not is marked unavailable with its entries left exactly where they are. **Events are not trusted to be complete.** `ReadDirectoryChangesW` drops events when its buffer overflows under a burst, and inotify on a FUSE mount misses changes made outside it. Most users are on Windows sharing from exFAT, so both apply. A periodic reconciliation scan is therefore not a belt-and-braces extra; it is the only thing that recovers a missed event. """ import asyncio import logging import time from collections.abc import Awaitable, Callable from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from dataclasses import field as dataclass_field from pathlib import Path import blake3 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.background import spawn from meshbay_common.paths import find_fold_collisions, fold, long_path from meshbay_common.protocol import IndexEntry from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer from meshbay_node.indexer.cache import IndexCache from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import Root, RootSet, off_disk log = logging.getLogger(__name__) # File types we include in the index (skip hidden files, temp files, etc.) EXCLUDED_PREFIXES = (".", "~", "#") EXCLUDED_SUFFIXES = (".tmp", ".part", ".crdownload", ".download") MEDIA_EXTENSIONS = { "video": {".mp4", ".mkv", ".avi", ".mov", ".wmv", ".flv", ".webm", ".m4v"}, # .wma and .mpc are real audio, tagged the same as anything else here # (enrich_audio.py reads both), but neither one has native decode # support in any mainstream browser's