Файлы участников CHAT-группы =========================== Модуль: src/member_files/. Desktop-браузер: tools/chatgui/src/member_files/. Отдельный сервис router ETCP_RT_ID_MEMBER_FILES (0x39), отдельно от вложений сообщений media_delivery и E2E-файлов DM. Серверы выбираются явно; между серверными деревьями автоматической репликации нет. Хранение и конфигурация ---------------------- Корень по умолчанию: /member_files/ (без db_path — ./member_files/). Публичное дерево: <корень>///<относительный путь>. Временные файлы: <корень>/.uploads/. Корень создаётся лениво по первому разрешённому файловому запросу. Очистка кэша вложений media/ это дерево не обходит. Настройки [chatserver] / [chat]: member_files_enabled=1 member_files_quota_mb=1024 member_files_upload_ttl_sec=3600 member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8) Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart. enabled проверяется на каждом запросе. Квота общая для дерева всех групп, включает опубликованные файлы и полный конечный размер временных загрузок. Замена требует места одновременно для прежнего файла и нового. Нехватка квоты/места возвращает NO_SPACE; опубликованные файлы не вытесняются. Сервер — действующий полный мембер группы с подписанным adm_tags.storage="yes". Колонка peers.storage — производный сетевой приоритет, для полномочий не используется. Каждый запрос и ответ подписаны Ed25519 и зашифрованы end-to-end средствами route_crypto. group/src берутся только из проверенного заголовка router; ETCP peer промежуточного хопа не считается автором запроса. Читать могут действующие участники той же группы. Изменять дерево может только src == owner. Удалённые записи и source=1 (плейсхолдеры) доступа не дают. Открытый fd/UUID загрузки не отменяют повторную проверку членства. Файлы удалённого участника сохраняются на диске, но через сервис не доступны. Wire, версия 1 -------------- Все числа LE, без сырых структур и padding. Фиксированный заголовок 125 байт: 0 protocol_version:u8 = 1 1 kind:u8 (1=request, 2=response) 2 command:u8 3 status:u16 (request=0) 5 request_id:u64, ненулевой случайный ID 13 owner:u64 (UID каталога; 0 только для LIST виртуального корня группы) 21 offset:u64 29 size:u64 37 cursor:u64 45 version:16 61 upload:16 77 hash:32 109 mtime:u64, Unix seconds 117 type:u8 (1=file, 2=directory) 118 more:u8 (0/1) 119 path_len:u16 121 target_len:u16 123 data_len:u16 После заголовка: path[path_len], target[target_len], data[data_len], без NUL. Длина пакета обязана точно совпадать с указанными размерами. Путь максимум 1024 байта, компонент максимум 255 байт, максимум 60 компонентов. Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX. Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты, компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы, Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь. POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются. Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points. Команды (1..10) --------------- 1 LIST: owner/path/cursor/version. Первая страница cursor=0, version=0. Ответ data — записи: name_len:u16,type:u8,size:u64,mtime:u64,version:16,name. cursor — сколько записей уже выдано. more=1 требует следующую страницу с возвращёнными cursor/version. При изменении снимка — STALE, начать сначала. Размер каталога не ограничивает память страницы. Виртуальный корень группы выдаёт UID действующих полных участников, даже если каталог ещё не создан. Изменение namespace сервера консервативно инвалидирует дисковые LIST-курсоры. 2 STAT: owner/path. Ответ type/size/mtime/version. Не хеширует весь файл. version — непрозрачный снимок identity/size/mtime/ctime и эпохи сервера. Закрытие fd по таймауту токен не меняет; restart меняет дисковые токены. 3 READ: owner/path/version/offset/size (1..8192). Ответ size — полный размер файла, data_len — фактически прочитано, more — есть ли байты после ответа. offset==EOF возвращает 0 байт/more=0; offset>EOF — INVALID_RANGE. Изменение файла между блоками — STALE. Клиент не смешивает версии и публикует локальный файл только по завершении. 4 BEGIN_WRITE: owner/path/upload/size/hash/version. upload — 16 случайных байт, size — конечный размер, hash — SHA256 содержимого. version=0 означает создать новый файл, существующий возвращает EXISTS. Для замены требуется версия из STAT/LIST. В один путь одна активная загрузка. UUID привязан к group/src/owner/path; повтор BEGIN с теми же параметрами безопасен. 5 WRITE: owner/path/upload/offset/size/data. size==data_len, диапазон внутри конечного размера. Можно писать в произвольном порядке и повторять блок. ACK подтверждает запись в temp, а не durable публикацию всего файла. 6 COMMIT: owner/path/upload. Проверяет SHA256, fsync/_commit и ожидаемую версию назначения, затем атомарно заменяет файл и синхронизирует namespace. Прежний файл виден до публикации. Ответ содержит размер/версию/SHA256. Незаписанные sparse-области читаются как нули; неправильное содержимое не проходит SHA256. Пустые файлы поддерживаются. При I/O-ошибке во время публикации результат может быть неопределённым; нужно повторно проверить STAT. 7 ABORT: owner/path/upload. Удаляет temp, освобождает резервацию. Неизвестный UUID — OK; опубликованные файлы никогда не удаляет. 8 MKDIR: owner/path. Создаёт один каталог; родители должны существовать. 9 RENAME: owner/path/target/version. Только внутри UID/группы, без замены существующего назначения. Активная загрузка в затрагиваемом пути — BUSY. 10 REMOVE: owner/path/version. Файл или пустой каталог, без рекурсивного удаления. Статусы (0..13): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE, EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT. Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд (64 квитанции); тот же request_id с другим содержимым — BAD_PACKET. COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока не дают гарантии того же результата; перед новой мутацией требуется STAT. Потоки, таймауты и остановка --------------------------- На экземпляр один дисковый pthread с очередью до 64 обычных задач. Сеть, ACL, таймеры и state machines выполняются в uasync; open/stat/readdir/read/write, resize/hash/sync/rename/remove/close — в дисковом потоке. GUI только передаёт команды через bridge и получает события. Нет вызова дискового I/O из callback таймера. Completion резервируется до постановки задачи; без polling workers. Локальные файлы transfer и серверное дерево используют тот же дисковый поток. Для обязательной очистки резервируется отдельная задача до открытия файла. Обращение к своему серверу доставляется через отдельную ограниченную очередь call_soon в тот же обработчик, без сетевого канала. ACL и дисковый поток общие; сетевой recv не принимает unsigned даже при src == self. Сервер держит до 64 открытых файлов. Каждый READ/WRITE/BEGIN продлевает idle. После 10 секунд простоя fd закрывается на очередном дисковом sweep (таймер 1 с); занятый дисковый поток откладывает sweep до освобождения. Закрытие не публикует и не удаляет файл. Temp можно открыть заново до истечения upload TTL. После restart незавершённые temp удаляются; опубликованные файлы остаются. Лимиты: 16 клиентских запросов, 32 серверных задания/ответа, 16 временных загрузок, 4 общих transfer. Router-очередь обслуживается через backpressure. Transfer владеет CM handle до окончания/отмены; DOWN/TIMEOUT handle не освобождает. quiesce запрещает новые команды, отменяет таймеры, waiter и запросы; destroy вне callbacks дожидается только своих работ/completions и закрывает дисковый поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O. Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion. Desktop ------- Контекстное меню канала: Group files. Меню участника: Member files. Окно: выбор сервера/UID, путь, Up/Refresh, таблица каталогов/файлов, Upload/Download/New folder/Rename/Delete, прогресс и Cancel transfer. Чужой UID доступен только для чтения. Автор может быть offline. Open скачивает в выбранный пользователем локальный файл и открывает его приложением ОС после завершения. Смена контекста отбрасывает поздние ответы. Проверки и диагностика --------------------- tests/test_member_files: wire/UTF-8, произвольные блоки, повторы, EOF, версии, короткая замена, SHA256, пустой файл, квота, ссылки, expiry/restart, 10-секундный fd idle и принадлежность work/done потокам. tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль, upload/download, побайтная проверка, чужая запись, виртуальный корень, подписанное удаление читателя. test_services — stop/start общего чат-сервиса. Логи: media=debug для операций/диапазонов/ACL/fd; info — публикации и transfer, warn/error — отказы с группой, UID, путём, смещением и точной причиной.