/* dm_media — файлы личных бесед и ограниченное ciphertext-хранилище M. * Шифрование/проверка/публикация файлов в media_async; все API ниже, БД и сеть — в uasync. * dm_core вызывает track внутри своей транзакции. Квитанция файла независима * от квитанции сообщения; публикуется после проверки, публикации файла и commit. * Суперузлом и заданиями удаления владеет dm_mailbox_media. * Квота M проверяется после успешного чтения занятого места и finalize SQL-запроса. * Ошибки SQL логируются без REJECT, сохраняя штатный повтор STORE со стороны S. * Instance/SQLite/группы должны пережить workers и их callbacks; остановка — quiesce, drain, destroy. * Входные строки/буферы заимствуются до возврата, кроме явно переданного временного файла. * Из C++ этот C API включать внутри extern "C", как в клиентах Qt/JNI. */ #ifndef DM_MEDIA_H #define DM_MEDIA_H #include #include #include "../media_async/attachment.h" struct UTUN_INSTANCE; #define DM_MEDIA_DESC_SIZE 56 /* UUID:16, plaintext_size:8 native-endian, SHA256(ciphertext):32. */ #define DM_MEDIA_RECEIPT_SIZE 152 #define DM_MEDIA_MAX_SIZE (1024ULL * 1024 * 1024) /* ETCP_RT_ID_DM_MEDIA=0x38: [cmd:1][body], router-подпись обязательна; числа native-endian. * OFFER: [recipient:8][holder:8][expires:8][каноническое file-сообщение]. A предлагает себя с expires=0. * STORE: [recipient:8][file-сообщение] от S к M; STORED: [UUID:16][expires:8]. * RECEIPT/DELETE: ["DMFILE01":8][conv:8][seq:8][author:8][recipient:8][UUID:16][SHA256(message):32][sig_B:64]. * B подписывает первые 88 байт; DELETE переносит ту же квитанцию, не создаёт новую. * DELETE_ACK/EXPIRED: [UUID:16]; REJECT: [UUID:16][reason:1], reason=1 — недостаток квоты. * EXPIRED завершает промежуточное хранение, а не подтверждает доставку B. */ #define DM_MEDIA_OFFER 1 #define DM_MEDIA_STORE 2 #define DM_MEDIA_STORED 3 #define DM_MEDIA_RECEIPT 4 #define DM_MEDIA_DELETE 5 #define DM_MEDIA_DELETE_ACK 6 #define DM_MEDIA_EXPIRED 7 #define DM_MEDIA_REJECT 8 /* После dm_core_init с SQLite/config/router/ua и media_async ядра: таблицы, каталоги, file_transfer, bind и таймер. * 0=успех, -1=ошибка, в том числе повторный init живого owner. При отказе созданные таблицы/каталоги остаются. * Корень: db_path/dm_media и db_path/dm_pending; без db_path используется текущий каталог. * После restart сбрасываются downloading/retry_at; история, custody и квитанции сохраняются. */ int dm_media_init(struct UTUN_INSTANCE* inst); /* Идемпотентно запретить новые задачи, отменить таймер/bind и file_transfer с его CM handles. * Вызвать до media_async_destroy; callbacks отмены ещё используют живые owner/SQLite/группы. NULL допустим. */ void dm_media_quiesce(struct UTUN_INSTANCE* inst); /* После drain/destroy media_async, до групп/core/mailbox: освободить owner, сохранить durable данные. * При живых jobs/cleanup_active логирует ошибку и оставляет owner для повторного destroy. NULL допустим. */ void dm_media_destroy(struct UTUN_INSTANCE* inst); /* Передать подготовку копии и enqueue file-сообщения в media_async. * Строки conv/path и info копируются при вызове; сам файл должен быть доступен worker до завершения. * 0=submit вызван, -1=отказ до submit. Submit может синхронно завершиться ошибкой через callback; * готовность/ошибка подготовки сообщается CHAT_EVT_ATTACHMENT_PREPARATION, включая до возврата 0. * Беседа должна быть видимой; до 4 одновременных подготовок, plaintext до DM_MEDIA_MAX_SIZE. * remove_source=1 передаёт worker собственный временный файл приложения для удаления; при -1 его убирает caller. * Успех подготовки означает commit file-сообщения, не доставку. Пользовательский исходник: remove_source=0. */ int dm_send_file(struct UTUN_INSTANCE* inst, const char* conv, const char* path, const struct attachment_info* info, int remove_source); struct dm_file_req { struct UTUN_INSTANCE* inst; /* Заимствованное ядро до callback. */ char conv[64]; /* Десятичный ID беседы с NUL. */ uint64_t generation; /* Ожидаемое поколение; 0=захватить при входе в uasync. */ char path[1024]; /* Путь исходника с NUL; копируется при принятии. */ struct attachment_info info; /* Проверенные метаданные вложения. */ int remove_source; /* Только принадлежащий приложению временный файл. */ }; /* arg — один блок u_malloc/u_calloc(dm_file_req); трамплин освобождает его, также при отказе. * Проверяет generation до submit; непринятый remove_source удаляет через worker. */ void dm_send_file_trampoline(void* arg); /* Внутри транзакции dm_core после проверки канонического msg/len; dir: 0=входящее, 1=исходящее. * 0=записано/точный повтор UUID и тела, -1=ошибка/конфликт; rollback делает вызывающий. * Для не-file — пустая операция. Исходящий file требует уже подготовленного .file нужного размера. */ int dm_media_track(struct UTUN_INSTANCE* inst, int dir, const uint8_t* msg, size_t len); /* Заимствованный descriptor внутри точного канонического file-тела либо NULL. * Проверяет только формат/длину, не подпись и не размер/хеш файла; указатель живёт вместе с msg. */ const uint8_t* dm_message_media(const uint8_t* msg, size_t len); /* Ciphertext bytes для plain_size<=DM_MEDIA_MAX_SIZE: plaintext плюс 16 байт на порцию 32768 (для пустого — 16). */ uint64_t dm_media_cipher_size(uint64_t plain_size); /* receipt — обязательные 152 доступных байта; подпись B/идентичность проверяются по nodes SQLite. * При msg!=NULL проверяется соответствие каноническому file-телу msg/len, UUID и полному SHA256. * 0=корректно, -1=отказ. Ничего не записывает, факт скачивания этим вызовом не устанавливается. */ int dm_media_verify_receipt(struct UTUN_INSTANCE* inst, const uint8_t* receipt, const uint8_t* msg, size_t len); /* Для локального A проверить 152-байтовую квитанцию и сохранить её в точном исходящем dm_files. * 0=сохранена/повтор либо точное подтверждение уже удалённого файла; -1=ошибка/неизвестное тело. * Историю и собственный файл A не удаляет; text outbox от этой квитанции не меняется. */ int dm_media_accept_receipt(struct UTUN_INSTANCE* inst, const uint8_t* receipt); /* Внутри транзакции dm_history_delete: сохранить свои пути в манифесте поколения и удалить dm_files беседы. * 0=успех, -1=ошибка; требует живого dm_media, commit/rollback делает вызывающий. Не удаляет файлы/transfer. */ int dm_media_forget_prepare(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation); /* Только после commit: отменить transfer удалённых UUID и начать асинхронную уборку старых поколений. * Callback отмены transfer синхронен; ожидания workers здесь нет, busy защищает их пути. */ void dm_media_forget_committed(struct UTUN_INSTANCE* inst, const char* conv); /* Зарегистрировать принадлежащий приложению path и увеличить busy до запуска worker/transfer. * conv/generation должны быть текущими и видимыми; 0=захват, -1=ошибка/чужое поколение пути. */ int dm_media_file_hold(struct UTUN_INSTANCE* inst, const char* conv, uint64_t generation, const char* path); /* Освободить один успешный hold после callback; SQL/нарушение счётчика логируются. * Уборка старого поколения ждёт освобождения всех holds; busy=-1 означает захват уборщиком. */ void dm_media_file_release(struct UTUN_INSTANCE* inst, const char* path); /* Отправить cmd/body/len через подписанный router в group к peer; нужен живой dm_media. * 0=пакет принят транспортом, ненулевое значение=ошибка/backpressure. Durable очередь не создаёт. * Локальный peer обрабатывается синхронно: 0 означает вызов обработчика, не его durable успех. * body заимствован до возврата; вызывающий соблюдает формат команды, size<=DM_MEDIA_BODY_MAX+24. */ int dm_media_send(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, uint8_t cmd, const uint8_t* body, size_t len); /* 1, если node/author/recipient — живые мемберы group и node имеет role=yes в adm_tags. * role=NULL проверяет только членство; 0 при отсутствии полномочий/ошибке SQL. Не проверяет сетевой путь. */ int dm_media_scope(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t node, const char* role, uint64_t author, uint64_t recipient); /* Опубликовать CHAT_EVT_DM_MSG_RECEIVED [conv_len:1][conv:decimal][local_node:8] для обновления снимка UI. */ void dm_media_notify(struct UTUN_INSTANCE* inst, uint64_t conv); #endif