""" 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 concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from pathlib import Path from typing import Callable, Awaitable import blake3 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer from meshbay_common.paths import fold, find_fold_collisions, long_path from meshbay_common.protocol import IndexEntry from meshbay_node.indexer.cache import IndexCache from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import Root, RootSet 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