295 lines
28 KiB
Plaintext
295 lines
28 KiB
Plaintext
Файлы участников 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; причина пишется в лог.
|
||
|
||
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.
|
||
|
||
Проверки и диагностика
|
||
---------------------
|
||
|
||
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 остаются в тематических категориях.
|