You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

375 lines
37 KiB

Файлы участников CHAT-группы
===========================
Модуль: src/member_files/. Desktop-браузер: tools/chatgui/src/member_files/.
Отдельный сервис router ETCP_RT_ID_MEMBER_FILES (0x39), отдельно от вложений
сообщений media_delivery и E2E-файлов DM. Серверы выбираются явно; между
серверными деревьями автоматической репликации нет.
Хранение и конфигурация
----------------------
Корень по умолчанию: <db_path>/member_files/ (без db_path — ./member_files/).
Публичное дерево: <корень>/<group_id>/<node_id>/<относительный путь>.
Временные файлы: <корень>/.uploads/<upload UUID в hex>.
При старте сервис создаёт/открывает корень в дисковом worker, удаляет старые
незавершённые temp и сканирует опубликованные файлы. До готовности отвечает BUSY.
Ошибка сканирования оставляет локальное хранилище недоступным до restart;
частичные счётчики клиенту не выдаются. Клиентские передачи к другим серверам продолжают работать.
Очистка кэша вложений media/ это дерево не обходит.
Настройки [chatserver] / [chat]:
member_files_enabled=1
member_files_user_quota_mb=1024
member_files_upload_ttl_sec=3600
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
по всем группам; корни групп, корни UID и .uploads исключены. Пустой опубликованный файл считается файлом.
Сохранённые файлы удалённых участников также занимают квоту.
При начале загрузки резервируется размер нового файла или положительный прирост при замене.
WRITE не изменяет used; COMMIT обновляет used на разницу размеров и освобождает резерв.
ABORT/Cancel/TTL освобождают резерв после удаления temp; REMOVE вычитает размер файла.
Переименование внутри UID не меняет размер и числа объектов. Новый COMMIT увеличивает files на 1;
замена существующего файла, включая нулевой размер, сохраняет files. MKDIR увеличивает directories на 1,
REMOVE уменьшает соответствующий счётчик. Для mkdir учитывается факт создания даже при последующей ошибке sync.
BEGIN/WRITE/ABORT/Cancel/TTL не увеличивают files: temp не опубликован.
Все изменения корректируют счётчики инкрементально, без обхода каталога пользователя;
QUOTA только читает их из памяти. LIST выполняет свой обычный обход для отображения записей, не пересчитывая статистику.
Повторы BEGIN/COMMIT/MKDIR/REMOVE не меняют учёт повторно.
Уже превышенная квота не удаляет данные: чтение, удаление и уменьшение файла доступны,
новый положительный прирост запрещён. Физический temp может занимать больше места,
чем резерв прироста; нехватка места файловой системы остаётся отдельной I/O-ошибкой.
Нехватка квоты/места возвращает NO_SPACE; опубликованные файлы не вытесняются.
Общая member_files_quota_mb удалена. Корень обслуживается только файловым сервисом;
ручные изменения требуют повторного сканирования через restart.
Сервер — действующий полный мембер группы с подписанным adm_tags.storage="yes".
Колонка peers.storage — производный сетевой приоритет, для полномочий не используется.
Каждый запрос и ответ подписаны Ed25519 и зашифрованы end-to-end средствами
route_crypto. group/src берутся только из проверенного заголовка router;
ETCP peer промежуточного хопа не считается автором запроса.
Читать могут действующие участники той же группы. Изменять дерево может только
src == owner. Удалённые записи и source=1 (плейсхолдеры) доступа не дают.
Открытый fd/UUID загрузки не отменяют повторную проверку членства.
Файлы удалённого участника сохраняются на диске, но через сервис не доступны.
Wire, версия 4
--------------
Все числа LE, без сырых структур и padding. Фиксированный заголовок 125 байт:
0 protocol_version:u8 = 4
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 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. Файл или пустой каталог, без рекурсивного удаления.
11 STREAM_WRITE: открытие потоковой загрузки; поля как BEGIN_WRITE,
cursor — ненулевой ID потока. Ответ подтверждает готовность temp и конечный размер.
12 STREAM_READ: открытие потокового скачивания; owner/path/version/size/cursor,
upload — случайный токен клиента. Сервер сверяет снимок и размер перед началом.
13 STREAM_DATA: request_id — ID потока, owner/path/upload, offset/size/data.
kind=request для upload, response для download. Ответа на блок нет.
offset обязан равняться следующему принятому смещению; size==data_len, не более 8192.
14 STREAM_END: ID потока, owner/path/upload, offset==size==размер файла, SHA256.
Upload: клиент отправляет END после всех DATA, сервер отвечает END после COMMIT.
Download: сервер отправляет END после DATA и повторной проверки версии; клиент
проверяет SHA256 записанных данных, синхронизирует и публикует локальный файл.
Ошибка сервера приходит END.status!=OK; ошибка открытия — ответом на STREAM_READ/WRITE.
15 STREAM_CANCEL: клиент отменяет конкретный поток. Поздние DATA не публикуются.
16 QUOTA: owner=UID, path/target пустые, cursor=0, data_len=0.
Права чтения те же, что у STAT/LIST выбранного участника. Ответ data_len=40:
used:u64, limit:u64, reserved:u64, files:u64, directories:u64, LE.
Первые три значения в байтах, последние два — числа объектов. Снимок общий для UID во всех группах.
Не резервирует место и не занимает квитанцию мутации. used может превышать limit
после уменьшения лимита; сервер не выдаёт фиктивный нулевой размер при ошибке учёта.
Открытие потока — обычный повторяемый запрос с отдельным request_id. cursor связывает
его с DATA/END/CANCEL. Поток определяется group/peer/kind/ID; один ID не может
одновременно обозначать разные открытия. Каждый пакет по-прежнему подписан и зашифрован.
При закрытии/перезапуске router-канала его handles завершаются с IO_ERROR;
байты разных эпох не смешиваются. Возобновление через новую эпоху пока не реализовано.
Статусы (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 временных
загрузок, 16 клиентских и 16 серверных потоков на экземпляр. Каждый поток имеет
кольцо 32 x 8192 байт (256 КиБ); worker обрабатывает до 16 блоков за задачу.
Чтение следующей порции перекрывается с отправкой уже подготовленной.
Все серверные и локальные I/O используют один FIFO worker, без блокировки uasync.
Отправитель добавляет пакеты, пока send_q router находится под нижним порогом
(сейчас count<=0), затем ждёт etcp_router_on_send_ready. Файловый слой не устанавливает
своё окно inflight и не повторяет DATA; транспортные ACK, retransmit и порядок — router.
Callback отправляет не более 8 блоков за запуск, уступая очередь другим потокам.
Параллельные файлы одного пира разделяют канал и FIFO/round-robin waiters.
Готовность приёмника проверяется router до доставки следующего seq. Заполненное
кольцо или очередь заданий оставляют пакет в recv_q: rx_seq/ACK не продвигаются.
Освобождение места вызывает отложенный resume; pump отменяется при reset/close.
ACK RECEIVE_BUSY (flags bit0) раз в 100 мс отличает ожидание диска от потери транспорта.
Отправитель приостанавливает новые DATA, а живые busy ACK продлевают таймаут router.
Буфер router ограничен его receive window. Локальная доставка использует такую же
проверку готовности и ограниченный FIFO, а не сеть или отдельные подтверждения.
При остановке сначала отменяются producers/requests и дожидаются все stream cleanup
continuations; только затем worker освобождает storage. Отмена после уже выполненной
публикации не удаляет опубликованный файл.
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 скачивает в выбранный пользователем локальный файл и открывает его
приложением ОС после завершения. Смена контекста отбрасывает поздние ответы.
Для выбранного UID строка хранилища показывает занято/всего, число файлов/каталогов и резерв текущих загрузок.
Точные байты доступны в tooltip. QUOTA запрашивается отдельно от LIST при выборе
сервера/UID, обновлении каталога, завершении передачи и каждые две секунды видимого окна.
Поздние ответы старого UID/сервера игнорируются. BUSY при стартовом сканировании
сохраняет Loading directory и повторяет LIST через секунду; новый переход отменяет повтор.
API параллельных передач
-----------------------
member_files_transfer_start(request, callback, arg) копирует request и возвращает
отдельный mf_transfer* на каждый файл. upload задаёт направление, group/server/owner
выбираются для каждой операции. id=0 выбирает случайный ID; id в callback — фактический.
NULL означает отказ запуска без callback; причина пишется в лог.
max_bytes=0 оставляет прежний лимит; ненулевое значение ограничивает размер файла.
Upload проверяет размер источника до хеширования, download — ответ STAT до открытия
локального temp. STREAM привязан к проверенным размеру/version; превышение — INVALID_RANGE.
callback(arg, mf_transfer_progress*) получает id/done/total/status/finished.
Прогресс заимствован до возврата callback. done для отправителя означает байты,
принятые router в очередь; успешный finished подтверждает завершение операции,
для upload — публикацию на сервере, для download — публикацию локального файла.
finished приходит ровно один раз, handle к этому моменту уже освобождён;
вызывающий обнуляет свой указатель в callback. member_files_transfer_cancel(handle)
отменяет только этот файл, ждёт его I/O и ставит CANCEL в очередь с backpressure.
Stop сервиса/группы завершает все принадлежащие ему handles.
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
Один локальный файл назначения не может одновременно принадлежать двум download.
Аватарки пользователей
---------------------
Относительный путь avatars/<sha256>.<расширение> находится внутри обычного дерева UID:
<корень>/<group>/<UID>/avatars/<sha256>.<расширение>. Дополнительного UID в API/пути нет.
Аватарка по умолчанию копируется в каждую группу, где выбранный сервер имеет
подписанную storage-роль. Ссылка каждого канала публикуется только после COMMIT
соответствующей копии; отсутствующий сервер в другом канале не создаёт битую ссылку.
Новое членство обнаруживается через снимки участников и повторяет публикацию.
Между серверами автоматического копирования нет.
userinfo содержит строковые avatar_server (16 hex цифр) и avatar_file (64 hex SHA256 + .json).
Оба поля отсутствуют при удалённой аватарке. Профиль подписан ключом участника;
смена имени/адресов сохраняет остальные плоские JSON-атрибуты. userinfo ограничен
1024 байтами UTF-8. Merkle, JOIN_INFO_RESP, CHANNEL_INVITE и JOIN_REQUEST используют
длину userinfo:u16 BE вместо прежнего u8. Bootstrap передаёт существующий подписанный
профиль инвайтера, не создавая более свежую запись только с именем. Обе стороны
нужно обновить: обратной совместимости нет.
Снимок участника CHAT_MEMBER_DISPLAY_SIZE теперь 158 байт: прежние 79 байт,
avatar_server:8LE, avatar_file:70 (с NUL), storage_role:1. storage_role обозначает
подписанную роль сервера, независимо от существующей колонки storage — сетевого
приоритета, который может быть 0 у сервера за NAT. Потребители используют общий
chat_member.h; локальное изменение профиля также вызывает MEMBER_UPDATED.
В Settings → Profile доступны Upload avatar, выбор storage-сервера, предпросмотр
и Remove avatar. Изменения применяются сразу, независимо от Save других настроек.
Перед загрузкой desktop/Android показывают редактор: неподвижная квадратная рамка,
перемещение фото и масштабирование (мышь/колесо или жесты). Отмена не публикует новую ссылку.
Worker принимает JPEG/PNG/WebP до 32 МиБ и 16 мегапикселей. Исходные байты, включая EXIF,
сохраняются без изменений. Выбранная область после EXIF-ориентации становится WebP lossy
256×256, quality=85, максимум 1 МиБ. Описание JSON (максимум 4096 байт): version=1,
original/preview — имена объектов; original_bytes/preview_bytes, width/height — размеры
ориентированного оригинала; x/y/w/h — нормализованный квадрат в его координатах.
Все объекты лежат в avatars/<SHA256 собственных байтов>.<jpg|png|webp|json>.
Порядок COMMIT: оригинал, превью, описание; затем подписанная ссылка на описание.
Повтор использует уже сохранённые неизменяемые объекты. Сервер проверяет имя/SHA256,
лимит объекта и запрещает замену под прежним именем и rename в/из avatars.
Файлы и каталог входят в обычные квоты. Прежние версии сохраняются на сервере;
владелец может удалить их через файловый браузер. Старые PNG-ссылки не поддерживаются.
Клик по аватарке сообщения/участника открывает галерею пользователя: LIST каталога
выбирает только описания, проходит все страницы cursor/version, сортирует mtime DESC,
при равенстве — по имени. Изначально выбрана текущая подписанная ссылка. Большое фото —
полный оригинал, до его загрузки показывается превью. Внизу пять квадратных позиций:
две предыдущие, выбранная по центру, две следующие; за границами списка позиции пустые.
Desktop: кнопки/клавиши ←/→; Android: кнопки/свайпы при исходном масштабе. Превью кликабельны.
Загружаются выбранный оригинал и видимые превью; закрытие отменяет запросы без других владельцев.
Для DM источник — общий канал, из которого берётся отображаемая аватарка.
AvatarManager общий для списка участников и сообщений, максимум две операции
одновременно, одинаковые хеши не скачиваются параллельно. Изменение своего профиля
имеет приоритет перед фоновыми загрузками. Для DM используются ссылки из общих
каналов; при отсутствии доступной ссылки остаются инициалы. Онлайн-статус списка
отображается отдельной точкой поверх фотографии. Поздний результат старой ссылки
пополняет кеш, но не заменяет картинку новой ссылки.
Кеш аккаунта: <db_path>/avatar_cache/<local_UID_hex>/<sha256>.<расширение>; без db_path
используется Qt AppLocalDataLocation. Проверяются SHA256 объектов, описание и WebP 256×256;
превышение/битая картинка не публикуются. Публикация кеша атомарна через QSaveFile,
незавершённые download/part очищаются при старте. Готовые файлы не требуют TTL/STAT:
смена содержимого меняет имя. Дисковый LRU — 100 МиБ, кеш QImage — 32 МиБ;
собственная аватарка и источники незавершённых upload сохраняются при вытеснении.
Дисковое I/O и декодирование выполняются вне GUI/uasync. Повторы неудачных загрузок:
5/10/20/40/80/160 секунд; новая ссылка сбрасывает задержку. RTT/online не меняют ссылку
и не вызывают повторной загрузки. Желаемая собственная ссылка/удаление сохраняются
атомарно одной строкой ui_state.avatar_default, незавершённая публикация повторяется
после запуска при наличии локального источника.
test_avatars проверяет нормализацию, хеш/размеры, очистку temp, настоящее storage
в двух каналах, публикацию, сохранение при смене имени, DM-кеш, restart без сети,
автоматический download после потери кеша и удаление ссылок. Также проверяются
max_bytes до открытия local temp и смена аватарки при задержанном download старой
версии: поздний результат не заменяет новый рисунок. test_member_adapter проверяет
подписанные профили 191/255/256/512/1023/1024 байт вместе с adm_tags до 8192.
Категория media=debug показывает discovery, cache hit/eviction, направление,
group/owner/server/hash, ошибки проверки и retry; media=info — передачи/публикации.
Проверки и диагностика
---------------------
tests/test_member_files: wire/UTF-8, произвольные блоки, повторы, EOF, версии,
короткая замена, SHA256, пустой файл, независимые квоты UID, общий учёт по группам,
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
fd idle и принадлежность work/done потокам.
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
проверка, отмена одного handle, restart router и destroy с активным server I/O.
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
test_services — stop/start общего чат-сервиса.
Текущая диагностика выделена в категорию debug. Настройка для полного трасса:
[debug]
debug=trace
debug=debug показывает запрос/ответ, ACL, очереди, маршрут, ACK, crypto-решения,
I/O, fd idle и состояние браузера. trace дополнительно показывает каждый
read/write и передачу задач/завершений между дисковым потоком и uasync.
stream progress раз в секунду показывает ID/направление, done/total, число блоков
в кольце, активность I/O, причину ожидания и скорость B/s. Router логирует переходы
receive paused/resumed и peer receive busy без повторного сообщения на каждый блок.
quota scan started/ready показывают путь, число пользователей и время обхода;
quota updated показывает UID/операцию/used/reserved/files/directories; quota exceeded — used/reserved/growth/limit.
По request_id видны GUI post → core request → router seq → server rx → ACL →
disk queued/start/done → response tx → GUI result/bridge → directory ready.
PID создания worker, PID отправки задачи и startup PID позволяют однозначно
обнаружить fork после создания потока; несовпадение PID логируется как ошибка.
Ключи шифрования, plaintext файлов и сырые медиа в debug не выводятся.
Старые VAD/рация/GUI/вложения сообщения перенесены в vad/radio/chat/media/config;
ошибки и пользовательские info остаются в тематических категориях.