Browse Source
- media_async: thread-per-task async engine with primitives (sha256, sign, copy, uuid, file_size) - media_index: media_files DB table with register_async for indexing chunks - chat_msg: async media submission via register_async, simplified chat_msg_submit fields - voice_recorder/attachment_sender: write single file (no chunk splitting on caller side) - inputbar/messagelist: updated to new simplified submit interface - test_media_async: 4-thread stress test (200K ops / 2 sec, 0 errors) - test_media_index: 9 cases covering commit edge cases + async flow - build: Makefile.am, CMakeLists.txt, utun_sources.cmake updatedtopo_upd
43 changed files with 1980 additions and 388 deletions
@ -0,0 +1,154 @@
|
||||
// media_async.c — thread-per-task async engine + crypto/file primitives
|
||||
#include "media_async.h" |
||||
#include "../../lib/u_async.h" |
||||
#include "../../lib/mem.h" |
||||
#include "../../lib/debug_config.h" |
||||
|
||||
#include <openssl/sha.h> |
||||
#include <openssl/evp.h> |
||||
#include <openssl/rand.h> |
||||
#include <pthread.h> |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <sys/stat.h> |
||||
|
||||
struct media_async { |
||||
int initialized; |
||||
}; |
||||
|
||||
struct ma_thread_ctx { |
||||
struct UASYNC* ua; |
||||
ma_work_fn work; |
||||
void* data; |
||||
ma_done_fn done; |
||||
void* arg; |
||||
}; |
||||
|
||||
static void ma_done_trampoline(void* raw) { |
||||
struct ma_thread_ctx* ctx = (struct ma_thread_ctx*)raw; |
||||
ctx->done(ctx->arg, 0); |
||||
u_free(ctx); |
||||
} |
||||
|
||||
static void* ma_thread_entry(void* raw) { |
||||
struct ma_thread_ctx* ctx = (struct ma_thread_ctx*)raw; |
||||
ctx->work(ctx->data); |
||||
uasync_post(ctx->ua, ma_done_trampoline, ctx); |
||||
return NULL; |
||||
} |
||||
|
||||
struct media_async* media_async_create(void) { |
||||
struct media_async* ma = u_malloc(sizeof(*ma)); |
||||
if (!ma) return NULL; |
||||
ma->initialized = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media_async: created"); |
||||
return ma; |
||||
} |
||||
|
||||
void media_async_destroy(struct media_async* ma) { |
||||
if (!ma) return; |
||||
ma->initialized = 0; |
||||
u_free(ma); |
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media_async: destroyed"); |
||||
} |
||||
|
||||
void media_async_submit(struct media_async* ma, struct UASYNC* ua, |
||||
ma_work_fn work, void* data, |
||||
ma_done_fn done, void* arg) { |
||||
(void)ma; |
||||
if (!ua || !work || !done) return; |
||||
|
||||
struct ma_thread_ctx* ctx = u_malloc(sizeof(*ctx)); |
||||
if (!ctx) { done(arg, -1); return; } |
||||
|
||||
ctx->ua = ua; |
||||
ctx->work = work; |
||||
ctx->data = data; |
||||
ctx->done = done; |
||||
ctx->arg = arg; |
||||
|
||||
pthread_t tid; |
||||
int rc = pthread_create(&tid, NULL, ma_thread_entry, ctx); |
||||
if (rc != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "media_async: pthread_create failed rc=%d", rc); |
||||
u_free(ctx); |
||||
done(arg, -1); |
||||
return; |
||||
} |
||||
pthread_detach(tid); |
||||
} |
||||
|
||||
/* ─── примитивы (вызываются из worker thread) ─── */ |
||||
|
||||
int ma_sha256_file(const char* path, uint8_t hash_out[32]) { |
||||
FILE* f = fopen(path, "rb"); |
||||
if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "ma_sha256_file: cannot open %s", path); return -1; } |
||||
|
||||
SHA256_CTX ctx; |
||||
SHA256_Init(&ctx); |
||||
|
||||
uint8_t buf[65536]; |
||||
size_t rd; |
||||
while ((rd = fread(buf, 1, sizeof(buf), f)) > 0) SHA256_Update(&ctx, buf, rd); |
||||
fclose(f); |
||||
|
||||
SHA256_Final(hash_out, &ctx); |
||||
return 0; |
||||
} |
||||
|
||||
int ma_sign_block(const uint8_t* ed25519_privkey, const uint8_t* data, size_t len, uint8_t sig_out[64]) { |
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, ed25519_privkey, 32); |
||||
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "ma_sign_block: EVP_PKEY_new_raw_private_key failed"); return -1; } |
||||
|
||||
EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); |
||||
if (!mdctx) { EVP_PKEY_free(pkey); return -1; } |
||||
|
||||
if (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) != 1) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "ma_sign_block: EVP_DigestSignInit failed"); |
||||
EVP_MD_CTX_free(mdctx); EVP_PKEY_free(pkey); return -1; |
||||
} |
||||
|
||||
size_t siglen = 64; |
||||
int rc = EVP_DigestSign(mdctx, sig_out, &siglen, data, len); |
||||
EVP_MD_CTX_free(mdctx); |
||||
EVP_PKEY_free(pkey); |
||||
|
||||
if (rc != 1 || siglen != 64) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "ma_sign_block: EVP_DigestSign failed rc=%d len=%zu", rc, siglen); |
||||
return -1; |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
int ma_copy_file(const char* src, const char* dst) { |
||||
if (!src || !dst) return -1; |
||||
if (strcmp(src, dst) == 0) return 0; |
||||
|
||||
FILE* s = fopen(src, "rb"); |
||||
if (!s) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "ma_copy_file: cannot open src %s", src); return -1; } |
||||
|
||||
FILE* d = fopen(dst, "wb"); |
||||
if (!d) { DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "ma_copy_file: cannot create dst %s", dst); fclose(s); return -1; } |
||||
|
||||
uint8_t buf[65536]; |
||||
size_t rd; |
||||
while ((rd = fread(buf, 1, sizeof(buf), s)) > 0) { |
||||
if (fwrite(buf, 1, rd, d) != rd) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "ma_copy_file: write error at %s", dst); |
||||
fclose(s); fclose(d); return -1; |
||||
} |
||||
} |
||||
fclose(s); fclose(d); |
||||
return 0; |
||||
} |
||||
|
||||
int ma_file_size(const char* path) { |
||||
struct stat st; |
||||
if (stat(path, &st) != 0) return -1; |
||||
return (int)(st.st_size > 0 ? st.st_size : 0); |
||||
} |
||||
|
||||
void ma_uuid(uint8_t uuid_out[16]) { |
||||
RAND_bytes(uuid_out, 16); |
||||
} |
||||
@ -0,0 +1,36 @@
|
||||
// media_async.h — асинхронный движок thread-per-task + примитивы (хеш, подпись, копия файла)
|
||||
#ifndef MEDIA_ASYNC_H |
||||
#define MEDIA_ASYNC_H |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#include <stdint.h> |
||||
#include <stddef.h> |
||||
|
||||
struct UASYNC; |
||||
|
||||
struct media_async; |
||||
|
||||
typedef void (*ma_work_fn)(void* data); |
||||
typedef void (*ma_done_fn)(void* arg, int err); |
||||
|
||||
struct media_async* media_async_create(void); |
||||
void media_async_destroy(struct media_async* ma); |
||||
|
||||
void media_async_submit(struct media_async* ma, struct UASYNC* ua, |
||||
ma_work_fn work, void* data, |
||||
ma_done_fn done, void* arg); |
||||
|
||||
int ma_sha256_file(const char* path, uint8_t hash_out[32]); |
||||
int ma_sign_block(const uint8_t* ed25519_privkey, |
||||
const uint8_t* data, size_t len, uint8_t sig_out[64]); |
||||
int ma_copy_file(const char* src, const char* dst); |
||||
int ma_file_size(const char* path); |
||||
void ma_uuid(uint8_t uuid_out[16]); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,41 @@
|
||||
#include "media_delivery.h" |
||||
#include "utun_instance.h" |
||||
#include "etcp_router.h" |
||||
#include "../lib/debug_config.h" |
||||
#include <string.h> |
||||
|
||||
static void media_delivery_etcp_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { |
||||
(void)conn; (void)entry; |
||||
} |
||||
|
||||
int media_delivery_init(struct UTUN_INSTANCE* inst) { |
||||
if (!inst) return -1; |
||||
struct media_delivery_ctx* md = &inst->md; |
||||
|
||||
memset(md, 0, sizeof(*md)); |
||||
md->self_node_id = inst->node_id; |
||||
|
||||
if (etcp_router_bind(inst, ETCP_RT_ID_MEDIA_DELIVERY, media_delivery_etcp_recv_cb) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "media_delivery: failed to bind ETCP_RT_ID_MEDIA_DELIVERY"); |
||||
return -1; |
||||
} |
||||
|
||||
md->initialized = 1; |
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "media_delivery initialized, node_id=%016llx", |
||||
(unsigned long long)md->self_node_id); |
||||
return 0; |
||||
} |
||||
|
||||
void media_delivery_destroy(struct UTUN_INSTANCE* inst) { |
||||
if (!inst || !inst->md.initialized) return; |
||||
struct media_delivery_ctx* md = &inst->md; |
||||
|
||||
etcp_router_unbind(inst, ETCP_RT_ID_MEDIA_DELIVERY); |
||||
|
||||
memset(md, 0, sizeof(*md)); |
||||
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "media_delivery destroyed"); |
||||
} |
||||
|
||||
int media_delivery_bind(struct UTUN_INSTANCE* inst) { |
||||
return media_delivery_init(inst); |
||||
} |
||||
@ -0,0 +1,27 @@
|
||||
// media_delivery.h — распространение медиа (аудио/видео стриминг)
|
||||
#ifndef MEDIA_DELIVERY_H |
||||
#define MEDIA_DELIVERY_H |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
|
||||
#include <stdint.h> |
||||
|
||||
struct UTUN_INSTANCE; |
||||
|
||||
struct media_delivery_ctx { |
||||
uint64_t self_node_id; |
||||
int initialized; |
||||
}; |
||||
|
||||
int media_delivery_init(struct UTUN_INSTANCE* inst); |
||||
void media_delivery_destroy(struct UTUN_INSTANCE* inst); |
||||
int media_delivery_bind(struct UTUN_INSTANCE* inst); |
||||
|
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,297 @@
|
||||
// media_index.c — таблица media_files: init, commit, register_async
|
||||
#include "media_index.h" |
||||
#include "../media_async/media_async.h" |
||||
#include "../../lib/u_async.h" |
||||
#include "../../lib/mem.h" |
||||
#include "../../lib/debug_config.h" |
||||
|
||||
#include <openssl/evp.h> |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <time.h> |
||||
#include <unistd.h> |
||||
|
||||
#define MEDIA_BLOCK_MIN (10 * 1024 * 1024) |
||||
#define MEDIA_BLOCK_MAX (25 * 1024 * 1024) |
||||
#define MEDIA_BLOCK_TARGET 30 |
||||
|
||||
static int mi_insert(sqlite3* db, |
||||
const uint8_t* media_id, const uint8_t* block_id, |
||||
const uint8_t* content_hash, |
||||
const char* chat_id, const char* location, |
||||
uint64_t node_id, const uint8_t* node_sign, |
||||
int64_t timestamp, int64_t file_size, |
||||
int64_t chunk_size, int chunk, int64_t offset) { |
||||
const char* sql = |
||||
"INSERT INTO media_files(media_id,block_id,content_hash,chat_id,location," |
||||
"node_id,node_sign,timestamp,file_size,chunk_size,chunk,offset)" |
||||
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?)"; |
||||
|
||||
sqlite3_stmt* stmt = NULL; |
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "media_index: insert prep fail: %s", sqlite3_errmsg(db)); |
||||
return -1; |
||||
} |
||||
sqlite3_bind_blob(stmt, 1, media_id, 16, SQLITE_STATIC); |
||||
sqlite3_bind_blob(stmt, 2, block_id, 16, SQLITE_STATIC); |
||||
sqlite3_bind_blob(stmt, 3, content_hash, 32, SQLITE_STATIC); |
||||
sqlite3_bind_text(stmt, 4, chat_id, -1, SQLITE_STATIC); |
||||
sqlite3_bind_text(stmt, 5, location, -1, SQLITE_STATIC); |
||||
sqlite3_bind_int64(stmt, 6, (sqlite3_int64)node_id); |
||||
sqlite3_bind_blob(stmt, 7, node_sign, 64, SQLITE_STATIC); |
||||
sqlite3_bind_int64(stmt, 8, timestamp); |
||||
sqlite3_bind_int64(stmt, 9, file_size); |
||||
sqlite3_bind_int64(stmt, 10, chunk_size); |
||||
sqlite3_bind_int(stmt, 11, chunk); |
||||
sqlite3_bind_int64(stmt, 12, offset); |
||||
|
||||
int rc = sqlite3_step(stmt); |
||||
sqlite3_finalize(stmt); |
||||
|
||||
if (rc != SQLITE_DONE) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "media_index: insert fail rc=%d: %s", rc, sqlite3_errmsg(db)); |
||||
return -1; |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
int media_index_init(sqlite3* db) { |
||||
if (!db) return -1; |
||||
|
||||
const char* sql = |
||||
"CREATE TABLE IF NOT EXISTS media_files (" |
||||
" id INTEGER PRIMARY KEY AUTOINCREMENT," |
||||
" media_id BLOB NOT NULL," |
||||
" block_id BLOB NOT NULL," |
||||
" content_hash BLOB NOT NULL," |
||||
" chat_id TEXT NOT NULL," |
||||
" location TEXT NOT NULL," |
||||
" node_id INTEGER NOT NULL," |
||||
" node_sign BLOB NOT NULL," |
||||
" timestamp INTEGER NOT NULL," |
||||
" file_size INTEGER NOT NULL," |
||||
" chunk_size INTEGER NOT NULL," |
||||
" chunk INTEGER NOT NULL," |
||||
" offset INTEGER NOT NULL);"; |
||||
|
||||
char* err = NULL; |
||||
int rc = sqlite3_exec(db, sql, NULL, NULL, &err); |
||||
if (rc != SQLITE_OK) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "media_index: CREATE TABLE failed: %s", err ? err : "?"); |
||||
sqlite3_free(err); |
||||
return -1; |
||||
} |
||||
|
||||
sqlite3_exec(db, |
||||
"CREATE UNIQUE INDEX IF NOT EXISTS idx_media_files_block_id ON media_files(block_id);", |
||||
NULL, NULL, NULL); |
||||
sqlite3_exec(db, |
||||
"CREATE INDEX IF NOT EXISTS idx_media_files_media_id ON media_files(media_id);", |
||||
NULL, NULL, NULL); |
||||
sqlite3_exec(db, |
||||
"CREATE INDEX IF NOT EXISTS idx_media_files_chat_id ON media_files(chat_id);", |
||||
NULL, NULL, NULL); |
||||
sqlite3_exec(db, |
||||
"CREATE INDEX IF NOT EXISTS idx_media_files_node_id ON media_files(node_id);", |
||||
NULL, NULL, NULL); |
||||
sqlite3_exec(db, |
||||
"CREATE INDEX IF NOT EXISTS idx_media_files_content_hash ON media_files(content_hash);", |
||||
NULL, NULL, NULL); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media_index: table+indices created"); |
||||
return 0; |
||||
} |
||||
|
||||
int media_calc_block_size(uint64_t file_size) { |
||||
if (file_size == 0) return 0; |
||||
uint64_t target = (file_size + MEDIA_BLOCK_TARGET - 1) / MEDIA_BLOCK_TARGET; |
||||
if (target < MEDIA_BLOCK_MIN) target = MEDIA_BLOCK_MIN; |
||||
if (target > MEDIA_BLOCK_MAX) target = MEDIA_BLOCK_MAX; |
||||
return (int)target; |
||||
} |
||||
|
||||
int media_index_generate_uuid(uint8_t uuid_out[16]) { |
||||
ma_uuid(uuid_out); |
||||
return 0; |
||||
} |
||||
|
||||
void media_index_result_free(struct media_index_result* result) { |
||||
if (!result) return; |
||||
if (result->block_ids) { u_free(result->block_ids); result->block_ids = NULL; } |
||||
if (result->block_sigs) { u_free(result->block_sigs); result->block_sigs = NULL; } |
||||
} |
||||
|
||||
int media_index_commit(sqlite3* db, const struct media_index_result* result, |
||||
uint64_t node_id, const uint8_t* ed25519_privkey, |
||||
const char* chat_id, const char* dest_path, const char* media_base) { |
||||
if (!db || !result || !chat_id || !dest_path || !media_base) return -1; |
||||
|
||||
int nb = result->num_blocks; |
||||
int64_t bs = result->block_size; |
||||
int64_t fs = result->file_size; |
||||
int64_t ts = (int64_t)time(NULL); |
||||
|
||||
size_t base_len = strlen(media_base); |
||||
if (base_len > 0 && media_base[base_len - 1] == '/') base_len--; |
||||
const char* rel = dest_path; |
||||
if (strncmp(rel, media_base, base_len) == 0 && rel[base_len] == '/') rel += base_len + 1; |
||||
|
||||
for (int n = 0; n < nb; n++) { |
||||
int64_t cs = (n == nb - 1) ? fs - n * bs : bs; |
||||
int64_t off = n * bs; |
||||
|
||||
char location[512]; |
||||
snprintf(location, sizeof(location), "%s", rel); |
||||
|
||||
uint8_t sig_msg[92]; |
||||
size_t soff = 0; |
||||
memcpy(sig_msg + soff, result->media_id, 16); soff += 16; |
||||
memcpy(sig_msg + soff, result->content_hash, 32); soff += 32; |
||||
memcpy(sig_msg + soff, &ts, 8); soff += 8; |
||||
memcpy(sig_msg + soff, &fs, 8); soff += 8; |
||||
memcpy(sig_msg + soff, &cs, 8); soff += 8; |
||||
int32_t cn = (int32_t)n; memcpy(sig_msg + soff, &cn, 4); soff += 4; |
||||
memcpy(sig_msg + soff, &off, 8); soff += 8; |
||||
memcpy(sig_msg + soff, &node_id, 8); soff += 8; |
||||
|
||||
uint8_t node_sign[64]; |
||||
if (ma_sign_block(ed25519_privkey, sig_msg, soff, node_sign) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "media_index: node_sign failed chunk=%d", n); |
||||
return -1; |
||||
} |
||||
|
||||
if (mi_insert(db, result->media_id, result->block_ids + n * 16, |
||||
result->content_hash, chat_id, location, |
||||
node_id, node_sign, ts, fs, cs, n, off) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "media_index: insert failed chunk=%d", n); |
||||
return -1; |
||||
} |
||||
} |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "media_index: committed %d chunks for chat=%s path=%s", |
||||
nb, chat_id, rel); |
||||
return 0; |
||||
} |
||||
|
||||
/* ─── register_async (worker + uasync callback) ─── */ |
||||
|
||||
struct mi_reg_ctx { |
||||
struct UASYNC* ua; |
||||
uint8_t ed25519_privkey[32]; |
||||
char src_path[1024]; |
||||
char dest_path[1024]; |
||||
int copy_file; |
||||
|
||||
uint64_t node_id; |
||||
char chat_id[64]; |
||||
char media_base[512]; |
||||
|
||||
sqlite3* db; |
||||
void (*cb)(void* arg, int err, const struct media_index_result* result); |
||||
void* cb_arg; |
||||
|
||||
struct media_index_result result; |
||||
int err; |
||||
}; |
||||
|
||||
static void mi_reg_work(void* data) { |
||||
struct mi_reg_ctx* ctx = (struct mi_reg_ctx*)data; |
||||
|
||||
if (ctx->copy_file && ma_copy_file(ctx->src_path, ctx->dest_path) != 0) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "media_index: copy failed %s -> %s", ctx->src_path, ctx->dest_path); |
||||
ctx->err = -10; return; |
||||
} |
||||
|
||||
int64_t fsize = (int64_t)ma_file_size(ctx->dest_path); |
||||
if (fsize <= 0) { ctx->err = -20; return; } |
||||
ctx->result.file_size = fsize; |
||||
|
||||
int64_t bs = (int64_t)media_calc_block_size((uint64_t)fsize); |
||||
int nb = (int)((fsize + bs - 1) / bs); |
||||
ctx->result.block_size = bs; |
||||
ctx->result.num_blocks = nb; |
||||
|
||||
if (ma_sha256_file(ctx->dest_path, ctx->result.content_hash) != 0) { |
||||
ctx->err = -30; return; |
||||
} |
||||
|
||||
ma_uuid(ctx->result.media_id); |
||||
|
||||
ctx->result.block_ids = u_malloc((size_t)nb * 16); |
||||
ctx->result.block_sigs = u_malloc((size_t)nb * 64); |
||||
if (!ctx->result.block_ids || !ctx->result.block_sigs) { ctx->err = -40; return; } |
||||
|
||||
FILE* f = fopen(ctx->dest_path, "rb"); |
||||
if (!f) { ctx->err = -50; return; } |
||||
|
||||
for (int n = 0; n < nb; n++) { |
||||
int64_t off = n * bs; |
||||
int64_t sz = (n == nb - 1) ? fsize - off : bs; |
||||
uint8_t* buf = u_malloc((size_t)sz); |
||||
if (!buf) { fclose(f); ctx->err = -60; return; } |
||||
fseeko(f, (off_t)off, SEEK_SET); |
||||
size_t rd = fread(buf, 1, (size_t)sz, f); |
||||
if (rd != (size_t)sz) { u_free(buf); fclose(f); ctx->err = -70; return; } |
||||
|
||||
ma_uuid(ctx->result.block_ids + n * 16); |
||||
if (ma_sign_block(ctx->ed25519_privkey, buf, (size_t)sz, |
||||
ctx->result.block_sigs + n * 64) != 0) { |
||||
u_free(buf); fclose(f); ctx->err = -80; return; |
||||
} |
||||
u_free(buf); |
||||
} |
||||
fclose(f); |
||||
ctx->err = 0; |
||||
} |
||||
|
||||
static void mi_reg_done(void* arg, int err) { |
||||
(void)err; |
||||
struct mi_reg_ctx* ctx = (struct mi_reg_ctx*)arg; |
||||
|
||||
if (ctx->err == 0) { |
||||
int rc = media_index_commit(ctx->db, &ctx->result, ctx->node_id, |
||||
ctx->ed25519_privkey, ctx->chat_id, |
||||
ctx->dest_path, ctx->media_base); |
||||
if (rc != 0) ctx->err = -90; |
||||
} |
||||
|
||||
if (ctx->cb) ctx->cb(ctx->cb_arg, ctx->err, ctx->err ? NULL : &ctx->result); |
||||
|
||||
media_index_result_free(&ctx->result); |
||||
u_free(ctx); |
||||
} |
||||
|
||||
void media_index_register_async( |
||||
struct media_async* ma, struct UASYNC* ua, sqlite3* db, |
||||
uint64_t node_id, const uint8_t* ed25519_privkey, |
||||
const char* chat_id, |
||||
const char* src_path, const char* dest_path, int copy_file, |
||||
const char* media_base, |
||||
void (*cb)(void* arg, int err, const struct media_index_result* result), |
||||
void* cb_arg) { |
||||
|
||||
if (!ma || !ua || !db || !ed25519_privkey || !chat_id || !src_path || !dest_path || !media_base || !cb) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "media_index: register_async invalid args"); |
||||
if (cb) cb(cb_arg, -1, NULL); |
||||
return; |
||||
} |
||||
|
||||
struct mi_reg_ctx* ctx = u_calloc(1, sizeof(*ctx)); |
||||
if (!ctx) { cb(cb_arg, -1, NULL); return; } |
||||
|
||||
ctx->ua = ua; |
||||
ctx->db = db; |
||||
ctx->node_id = node_id; |
||||
ctx->copy_file = copy_file; |
||||
ctx->cb = cb; |
||||
ctx->cb_arg = cb_arg; |
||||
|
||||
memcpy(ctx->ed25519_privkey, ed25519_privkey, 32); |
||||
snprintf(ctx->src_path, sizeof(ctx->src_path), "%s", src_path); |
||||
snprintf(ctx->dest_path, sizeof(ctx->dest_path), "%s", dest_path); |
||||
snprintf(ctx->chat_id, sizeof(ctx->chat_id), "%s", chat_id); |
||||
snprintf(ctx->media_base, sizeof(ctx->media_base), "%s", media_base); |
||||
|
||||
media_async_submit(ma, ua, mi_reg_work, ctx, mi_reg_done, ctx); |
||||
} |
||||
@ -0,0 +1,52 @@
|
||||
// media_index.h — таблица media_files + регистрация медиа с async-обработкой
|
||||
#ifndef MEDIA_INDEX_H |
||||
#define MEDIA_INDEX_H |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#include <stdint.h> |
||||
#include <stdint.h> |
||||
#include <sqlite3.h> |
||||
|
||||
struct media_async; |
||||
struct UASYNC; |
||||
|
||||
#define MEDIA_ID_SIZE 16 |
||||
#define MEDIA_HASH_SIZE 32 |
||||
|
||||
struct media_index_result { |
||||
uint8_t media_id[16]; |
||||
uint8_t content_hash[32]; |
||||
int64_t file_size; |
||||
int64_t block_size; |
||||
int num_blocks; |
||||
uint8_t* block_ids; // num_blocks * 16
|
||||
uint8_t* block_sigs; // num_blocks * 64
|
||||
}; |
||||
|
||||
int media_index_init(sqlite3* db); |
||||
|
||||
int media_index_commit( |
||||
sqlite3* db, const struct media_index_result* result, |
||||
uint64_t node_id, const uint8_t* ed25519_privkey, |
||||
const char* chat_id, const char* dest_path, const char* media_base); |
||||
|
||||
void media_index_register_async( |
||||
struct media_async* ma, struct UASYNC* ua, sqlite3* db, |
||||
uint64_t node_id, const uint8_t* ed25519_privkey, |
||||
const char* chat_id, |
||||
const char* src_path, const char* dest_path, int copy_file, |
||||
const char* media_base, |
||||
void (*cb)(void* arg, int err, const struct media_index_result* result), |
||||
void* cb_arg); |
||||
|
||||
int media_index_generate_uuid(uint8_t uuid_out[16]); |
||||
void media_index_result_free(struct media_index_result* result); |
||||
int media_calc_block_size(uint64_t file_size); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
@ -0,0 +1,299 @@
|
||||
// test_media_async.c — стресс-тест примитивов media_async (многопоточность + корректность)
|
||||
//
|
||||
// 4 потока одновременно выполняют ma_* операции на файлах 1K/5K/10K.
|
||||
// Тест длится 2 секунды, затем потоки останавливаются.
|
||||
// Проверяется корректность каждого результата и отсутствие заклиниваний.
|
||||
|
||||
#include "../src/media_async/media_async.h" |
||||
#include <pthread.h> |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <time.h> |
||||
#include <unistd.h> |
||||
#include <sys/stat.h> |
||||
#include <sys/time.h> |
||||
|
||||
#include <openssl/evp.h> |
||||
|
||||
#define NUM_THREADS 4 |
||||
#define TEST_TIMEOUT_SEC 2 |
||||
#define NUM_FILES 3 |
||||
#define SIZE_1K 1024 |
||||
#define SIZE_5K 5120 |
||||
#define SIZE_10K 10240 |
||||
#define NUM_OPS 5 |
||||
|
||||
static volatile int g_running = 1; |
||||
|
||||
static const char* g_op_names[NUM_OPS] = { |
||||
"SHA256-file", "Sign-block", "Copy-file", |
||||
"UUID", "FileSize" |
||||
}; |
||||
|
||||
struct thread_stats { |
||||
int tid; |
||||
int op_count; |
||||
int errors; |
||||
int op_counts[NUM_OPS]; |
||||
double max_time_us[NUM_OPS]; // максимальное время (микросекунды) по каждой операции
|
||||
}; |
||||
|
||||
static char g_test_dir[256]; |
||||
static char g_file_paths[NUM_FILES][512]; |
||||
static uint8_t g_file_hashes[NUM_FILES][32]; |
||||
static int g_file_sizes[NUM_FILES]; |
||||
static uint8_t g_test_data[NUM_FILES][SIZE_10K]; |
||||
static uint8_t g_ed25519_privkey[32]; |
||||
static uint8_t g_expected_sigs[NUM_FILES][64]; |
||||
|
||||
static int64_t now_us(void) { |
||||
struct timeval tv; gettimeofday(&tv, NULL); |
||||
return (int64_t)tv.tv_sec * 1000000LL + tv.tv_usec; |
||||
} |
||||
|
||||
static void fill_test_data(uint8_t* buf, int size, int seed) { |
||||
unsigned s = (unsigned)seed; |
||||
for (int i = 0; i < size; i++) { |
||||
s = s * 1103515245 + 12345; |
||||
buf[i] = (uint8_t)((s >> 16) & 0xFF); |
||||
} |
||||
} |
||||
|
||||
static void* worker_thread(void* arg) { |
||||
struct thread_stats* st = (struct thread_stats*)arg; |
||||
unsigned s = (unsigned)(time(NULL) ^ (st->tid * 0x9E3779B9 << 8)); |
||||
|
||||
while (g_running) { |
||||
s = s * 1103515245 + 12345; |
||||
int op = (int)((s >> 16) % 4); // random: 0=SHA256, 1=Sign, 2=Copy, 3=UUID+FileSize
|
||||
s = s * 1103515245 + 12345; |
||||
int fi = (int)((s >> 16) % NUM_FILES); |
||||
int size = g_file_sizes[fi]; |
||||
|
||||
int real_op = -1; |
||||
int64_t t0, elapsed; |
||||
|
||||
switch (op) { |
||||
case 0: { // SHA256 file
|
||||
real_op = 0; |
||||
uint8_t hash[32]; |
||||
t0 = now_us(); |
||||
int rc = ma_sha256_file(g_file_paths[fi], hash); |
||||
elapsed = now_us() - t0; |
||||
if (rc != 0 || memcmp(hash, g_file_hashes[fi], 32) != 0) st->errors++; |
||||
break; |
||||
} |
||||
case 1: { // Ed25519 sign
|
||||
real_op = 1; |
||||
uint8_t sig[64]; |
||||
t0 = now_us(); |
||||
int rc = ma_sign_block(g_ed25519_privkey, g_test_data[fi], (size_t)size, sig); |
||||
elapsed = now_us() - t0; |
||||
if (rc != 0 || memcmp(sig, g_expected_sigs[fi], 64) != 0) st->errors++; |
||||
break; |
||||
} |
||||
case 2: { // copy file
|
||||
real_op = 2; |
||||
char dst[512]; |
||||
snprintf(dst, sizeof(dst), "%s/cp_t%d_n%d.bin", g_test_dir, st->tid, st->op_count); |
||||
t0 = now_us(); |
||||
int rc = ma_copy_file(g_file_paths[fi], dst); |
||||
elapsed = now_us() - t0; |
||||
if (rc == 0) { |
||||
uint8_t hash[32]; |
||||
if (ma_sha256_file(dst, hash) != 0 || memcmp(hash, g_file_hashes[fi], 32) != 0) |
||||
st->errors++; |
||||
unlink(dst); |
||||
} else st->errors++; |
||||
break; |
||||
} |
||||
case 3: { // UUID
|
||||
real_op = 3; |
||||
uint8_t uuid[16]; |
||||
t0 = now_us(); |
||||
ma_uuid(uuid); |
||||
elapsed = now_us() - t0; |
||||
int zero = 1; |
||||
for (int i = 0; i < 16; i++) { if (uuid[i] != 0) { zero = 0; break; } } |
||||
if (zero) st->errors++; |
||||
|
||||
// file_size отдельно с замером времени
|
||||
t0 = now_us(); |
||||
int64_t fsz = ma_file_size(g_file_paths[fi]); |
||||
int64_t et = now_us() - t0; |
||||
if (fsz != g_file_sizes[fi]) st->errors++; |
||||
st->op_counts[4]++; |
||||
if (et > st->max_time_us[4]) st->max_time_us[4] = (double)et; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
st->op_count++; |
||||
if (real_op >= 0) { |
||||
st->op_counts[real_op]++; |
||||
if (elapsed > st->max_time_us[real_op]) |
||||
st->max_time_us[real_op] = (double)elapsed; |
||||
} |
||||
} |
||||
return NULL; |
||||
} |
||||
|
||||
int main(void) { |
||||
int ret = 0; |
||||
|
||||
snprintf(g_test_dir, sizeof(g_test_dir), "/tmp/test_media_async_%d", (int)getpid()); |
||||
if (mkdir(g_test_dir, 0755) != 0) { |
||||
perror("mkdir"); return 1; |
||||
} |
||||
|
||||
g_file_sizes[0] = SIZE_1K; |
||||
g_file_sizes[1] = SIZE_5K; |
||||
g_file_sizes[2] = SIZE_10K; |
||||
|
||||
for (int i = 0; i < NUM_FILES; i++) { |
||||
fill_test_data(g_test_data[i], g_file_sizes[i], 42 + i * 100); |
||||
snprintf(g_file_paths[i], sizeof(g_file_paths[i]), "%s/test_%dk.bin", g_test_dir, g_file_sizes[i] / 1024); |
||||
FILE* f = fopen(g_file_paths[i], "wb"); |
||||
if (!f) { perror("fopen"); ret = 1; goto cleanup; } |
||||
fwrite(g_test_data[i], 1, (size_t)g_file_sizes[i], f); |
||||
fclose(f); |
||||
ma_sha256_file(g_file_paths[i], g_file_hashes[i]); |
||||
} |
||||
|
||||
{ |
||||
EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, NULL); |
||||
if (!pctx) { fprintf(stderr, "EVP_PKEY_CTX_new_id failed\n"); ret = 1; goto cleanup; } |
||||
EVP_PKEY_keygen_init(pctx); |
||||
EVP_PKEY* pkey = NULL; |
||||
if (EVP_PKEY_keygen(pctx, &pkey) != 1 || !pkey) { |
||||
fprintf(stderr, "EVP_PKEY_keygen failed\n"); EVP_PKEY_CTX_free(pctx); ret = 1; goto cleanup; |
||||
} |
||||
EVP_PKEY_CTX_free(pctx); |
||||
size_t keylen = 32; |
||||
EVP_PKEY_get_raw_private_key(pkey, g_ed25519_privkey, &keylen); |
||||
EVP_PKEY_free(pkey); |
||||
for (int i = 0; i < NUM_FILES; i++) |
||||
ma_sign_block(g_ed25519_privkey, g_test_data[i], (size_t)g_file_sizes[i], g_expected_sigs[i]); |
||||
} |
||||
|
||||
pthread_t threads[NUM_THREADS]; |
||||
struct thread_stats stats[NUM_THREADS]; |
||||
memset(stats, 0, sizeof(stats)); |
||||
for (int i = 0; i < NUM_THREADS; i++) { |
||||
stats[i].tid = i; |
||||
if (pthread_create(&threads[i], NULL, worker_thread, &stats[i]) != 0) { |
||||
fprintf(stderr, "pthread_create(%d) failed\n", i); |
||||
g_running = 0; |
||||
for (int j = 0; j < i; j++) pthread_join(threads[j], NULL); |
||||
ret = 1; goto cleanup; |
||||
} |
||||
} |
||||
|
||||
sleep(TEST_TIMEOUT_SEC); |
||||
g_running = 0; |
||||
|
||||
for (int i = 0; i < NUM_THREADS; i++) |
||||
pthread_join(threads[i], NULL); |
||||
|
||||
printf("\n=== media_async stress test ===\n"); |
||||
printf("Timeout: %d sec, Threads: %d, Files: 1K/5K/10K\n\n", TEST_TIMEOUT_SEC, NUM_THREADS); |
||||
|
||||
int total_ops = 0, total_errors = 0; |
||||
int totals_op[NUM_OPS] = {0}; |
||||
double max_time_all[NUM_OPS] = {0}; |
||||
|
||||
for (int i = 0; i < NUM_THREADS; i++) { |
||||
printf("Thread %d: %d ops (", i, stats[i].op_count); |
||||
for (int o = 0; o < NUM_OPS; o++) { |
||||
if (o > 0) printf(" "); |
||||
printf("%s:%d", g_op_names[o], stats[i].op_counts[o]); |
||||
totals_op[o] += stats[i].op_counts[o]; |
||||
} |
||||
printf(") errors=%d\n", stats[i].errors); |
||||
|
||||
printf(" max-time(us): "); |
||||
for (int o = 0; o < NUM_OPS; o++) { |
||||
if (o > 0) printf(" "); |
||||
printf("%s:%.0f", g_op_names[o], stats[i].max_time_us[o]); |
||||
if (stats[i].max_time_us[o] > max_time_all[o]) max_time_all[o] = stats[i].max_time_us[o]; |
||||
} |
||||
printf("\n"); |
||||
|
||||
total_ops += stats[i].op_count; |
||||
total_errors += stats[i].errors; |
||||
} |
||||
|
||||
printf("\nTotal: %d ops, %d errors\n", total_ops, total_errors); |
||||
printf("Op totals: "); |
||||
for (int o = 0; o < NUM_OPS; o++) printf("%s:%d ", g_op_names[o], totals_op[o]); |
||||
printf("\nMax time(us): "); |
||||
for (int o = 0; o < NUM_OPS; o++) printf("%s:%.0f ", g_op_names[o], max_time_all[o]); |
||||
printf("\n\n"); |
||||
|
||||
/* ─── sanity checks ─── */ |
||||
|
||||
// минимальный throughput: хотя бы 1000 ops на поток за 2 сек
|
||||
int min_ops_per_thread = 1000; |
||||
// разбалансировка потоков: самый медленный поток не хуже 0.1× от самого быстрого
|
||||
double balance_ratio = 0.1; |
||||
// макс допустимое время SHA256/Sign/Copy/USB операции (микросекунды)
|
||||
double max_us_sha = 100000; // 100ms
|
||||
double max_us_sign = 100000; |
||||
double max_us_copy = 100000; |
||||
double max_us_uuid = 5000; // 5ms
|
||||
double max_us_filesize = 5000; |
||||
|
||||
int min_ops = stats[0].op_count, max_ops = stats[0].op_count; |
||||
for (int i = 1; i < NUM_THREADS; i++) { |
||||
if (stats[i].op_count < min_ops) min_ops = stats[i].op_count; |
||||
if (stats[i].op_count > max_ops) max_ops = stats[i].op_count; |
||||
} |
||||
|
||||
printf("Sanity checks:\n"); |
||||
|
||||
if (total_errors > 0) { |
||||
printf(" FAIL: %d errors\n", total_errors); |
||||
ret = 1; |
||||
} |
||||
|
||||
if (total_ops < min_ops_per_thread * NUM_THREADS) { |
||||
printf(" FAIL: total ops %d < %d (throughput too low)\n", |
||||
total_ops, min_ops_per_thread * NUM_THREADS); |
||||
ret = 1; |
||||
} else { |
||||
printf(" OK: total ops=%d >= %d\n", total_ops, min_ops_per_thread * NUM_THREADS); |
||||
} |
||||
|
||||
if (max_ops > 0 && min_ops < (int)(max_ops * balance_ratio)) { |
||||
printf(" FAIL: thread imbalance min=%d max=%d (ratio %.3f < %.1f)\n", |
||||
min_ops, max_ops, max_ops > 0 ? (double)min_ops / max_ops : 0, balance_ratio); |
||||
ret = 1; |
||||
} else { |
||||
printf(" OK: thread balance min=%d max=%d (ratio %.3f)\n", |
||||
min_ops, max_ops, max_ops > 0 ? (double)min_ops / max_ops : 0); |
||||
} |
||||
|
||||
double limits[NUM_OPS] = { max_us_sha, max_us_sign, max_us_copy, max_us_uuid, max_us_filesize }; |
||||
for (int o = 0; o < NUM_OPS; o++) { |
||||
if (max_time_all[o] > limits[o]) { |
||||
printf(" FAIL: %s max-time %.0fus > %.0fus\n", g_op_names[o], max_time_all[o], limits[o]); |
||||
ret = 1; |
||||
} else if (max_time_all[o] <= 0.0) { |
||||
printf(" WARN: %s max-time 0 (no measurements?)\n", g_op_names[o]); |
||||
} else { |
||||
printf(" OK: %s max-time %.0fus\n", g_op_names[o], max_time_all[o]); |
||||
} |
||||
} |
||||
|
||||
if (ret == 0) { |
||||
printf("\nPASS: %d ops, 0 errors, all checks OK\n", total_ops); |
||||
} else { |
||||
printf("\nFAIL: sanity checks failed\n"); |
||||
} |
||||
|
||||
cleanup: |
||||
for (int i = 0; i < NUM_FILES; i++) unlink(g_file_paths[i]); |
||||
rmdir(g_test_dir); |
||||
return ret; |
||||
} |
||||
@ -0,0 +1,510 @@
|
||||
// test_media_index.c — юнит-тесты media_index (commit + register_async)
|
||||
//
|
||||
// Покрытие:
|
||||
// 1. 1 чанк, точный размер
|
||||
// 2. N чанков, точное совпадение
|
||||
// 3. Последний чанк меньше
|
||||
// 4. Файл меньше размера чанка
|
||||
// 5. Много чанков (>3)
|
||||
// 6. media_base с trailing slash
|
||||
// 7. media_base без trailing slash
|
||||
// 8. node_sign верификация (Ed25519 verify)
|
||||
// 9. Разные chat_id
|
||||
// 10. Полный async flow (register_async + copy=1)
|
||||
|
||||
#include "../src/media_delivery/media_index.h" |
||||
#include "../src/media_async/media_async.h" |
||||
#include "../lib/u_async.h" |
||||
#include "../lib/mem.h" |
||||
#include "../lib/debug_config.h" |
||||
|
||||
#include <openssl/evp.h> |
||||
#include <sqlite3.h> |
||||
#include <stdio.h> |
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
#include <unistd.h> |
||||
#include <sys/stat.h> |
||||
|
||||
#define BS 100 |
||||
#define NODE_ID 0xDEADBEEF |
||||
|
||||
static int g_passed = 0; |
||||
static int g_failed = 0; |
||||
static int g_total = 0; |
||||
|
||||
#define TEST(name) do { \ |
||||
g_total++; \
|
||||
printf(" %-55s", name); \
|
||||
} while(0) |
||||
|
||||
#define OK() do { g_passed++; printf("OK\n"); } while(0) |
||||
#define FAIL(fmt, ...) do { \ |
||||
g_failed++; \
|
||||
printf("FAIL: " fmt "\n", ##__VA_ARGS__); \
|
||||
} while(0) |
||||
|
||||
/* ─── helpers ─── */ |
||||
|
||||
static sqlite3* make_db(void) { |
||||
sqlite3* db = NULL; |
||||
sqlite3_open(":memory:", &db); |
||||
sqlite3_exec(db, "PRAGMA journal_mode=memory", NULL, NULL, NULL); |
||||
return db; |
||||
} |
||||
|
||||
static void free_db(sqlite3* db) { if (db) sqlite3_close(db); } |
||||
|
||||
static void derive_pubkey(const uint8_t priv[32], uint8_t pub[32]) { |
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, priv, 32); |
||||
if (pkey) { |
||||
size_t l = 32; |
||||
EVP_PKEY_get_raw_public_key(pkey, pub, &l); |
||||
EVP_PKEY_free(pkey); |
||||
} |
||||
} |
||||
|
||||
static int ed25519_verify(const uint8_t pub[32], const uint8_t* msg, size_t len, const uint8_t sig[64]) { |
||||
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, pub, 32); |
||||
if (!pkey) return -1; |
||||
EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); |
||||
if (!mdctx) { EVP_PKEY_free(pkey); return -1; } |
||||
if (EVP_DigestVerifyInit(mdctx, NULL, NULL, NULL, pkey) != 1) { |
||||
EVP_MD_CTX_free(mdctx); EVP_PKEY_free(pkey); return -1; |
||||
} |
||||
int rc = EVP_DigestVerify(mdctx, sig, 64, msg, len); |
||||
EVP_MD_CTX_free(mdctx); EVP_PKEY_free(pkey); |
||||
return rc == 1 ? 0 : -1; |
||||
} |
||||
|
||||
static int row_count(sqlite3* db, const uint8_t* media_id) { |
||||
sqlite3_stmt* st = NULL; |
||||
sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM media_files WHERE media_id=?", -1, &st, NULL); |
||||
sqlite3_bind_blob(st, 1, media_id, 16, SQLITE_STATIC); |
||||
int c = 0; |
||||
if (sqlite3_step(st) == SQLITE_ROW) c = sqlite3_column_int(st, 0); |
||||
sqlite3_finalize(st); |
||||
return c; |
||||
} |
||||
|
||||
/* заполняет result рандомными данными для тестирования commit */ |
||||
static void fill_result(struct media_index_result* r, int64_t file_size, int64_t block_size, int num_blocks) { |
||||
memset(r, 0, sizeof(*r)); |
||||
r->file_size = file_size; |
||||
r->block_size = block_size; |
||||
r->num_blocks = num_blocks; |
||||
ma_uuid(r->media_id); |
||||
ma_uuid(r->content_hash); |
||||
ma_uuid(r->content_hash + 16); |
||||
r->block_ids = u_malloc((size_t)num_blocks * 16); |
||||
r->block_sigs = u_malloc((size_t)num_blocks * 64); |
||||
for (int i = 0; i < num_blocks; i++) { |
||||
ma_uuid(r->block_ids + i * 16); |
||||
ma_uuid(r->block_sigs + i * 64); |
||||
ma_uuid(r->block_sigs + i * 64 + 16); |
||||
ma_uuid(r->block_sigs + i * 64 + 32); |
||||
ma_uuid(r->block_sigs + i * 64 + 48); |
||||
} |
||||
} |
||||
|
||||
static int check_row(sqlite3* db, const uint8_t* media_id, int chunk, |
||||
const uint8_t* content_hash, int64_t file_size, |
||||
int64_t chunk_size, int64_t offset, uint64_t node_id, |
||||
const char* chat_id, const char* location, |
||||
const uint8_t* node_pubkey) { |
||||
sqlite3_stmt* st = NULL; |
||||
sqlite3_prepare_v2(db, |
||||
"SELECT block_id,content_hash,chat_id,location,node_id,node_sign,timestamp,file_size,chunk_size,chunk,offset" |
||||
" FROM media_files WHERE media_id=? AND chunk=?", |
||||
-1, &st, NULL); |
||||
if (!st) return -1; |
||||
sqlite3_bind_blob(st, 1, media_id, 16, SQLITE_STATIC); |
||||
sqlite3_bind_int(st, 2, chunk); |
||||
if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return -1; } |
||||
|
||||
const uint8_t* bid = (const uint8_t*)sqlite3_column_blob(st, 0); |
||||
int bid_len = sqlite3_column_bytes(st, 0); |
||||
const uint8_t* ch = (const uint8_t*)sqlite3_column_blob(st, 1); |
||||
int ch_len = sqlite3_column_bytes(st, 1); |
||||
const char* cid = (const char*)sqlite3_column_text(st, 2); |
||||
const char* loc = (const char*)sqlite3_column_text(st, 3); |
||||
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 4); |
||||
const uint8_t* nsig = (const uint8_t*)sqlite3_column_blob(st, 5); |
||||
int nsig_len = sqlite3_column_bytes(st, 5); |
||||
int64_t ts = sqlite3_column_int64(st, 6); |
||||
int64_t fsz = sqlite3_column_int64(st, 7); |
||||
int64_t csz = sqlite3_column_int64(st, 8); |
||||
int c = sqlite3_column_int(st, 9); |
||||
int64_t off = sqlite3_column_int64(st, 10); |
||||
|
||||
int err = 0; |
||||
if (!bid || bid_len != 16) { printf("bad block_id "); err = -1; } |
||||
if (!ch || ch_len != 32 || memcmp(ch, content_hash, 32) != 0) { printf("bad content_hash "); err = -1; } |
||||
if (!cid || strcmp(cid, chat_id) != 0) { printf("bad chat_id "); err = -1; } |
||||
if (!loc || strcmp(loc, location) != 0) { printf("bad location '%s'!='%s' ", loc ? loc : "NULL", location); err = -1; } |
||||
if (nid != node_id) { printf("bad node_id %llu!=%llu ", (unsigned long long)nid, (unsigned long long)node_id); err = -1; } |
||||
if (!nsig || nsig_len != 64) { printf("bad node_sign "); err = -1; } |
||||
if (ts <= 0) { printf("bad timestamp %lld ", (long long)ts); err = -1; } |
||||
if (fsz != file_size) { printf("bad file_size %lld!=%lld ", (long long)fsz, (long long)file_size); err = -1; } |
||||
if (csz != chunk_size) { printf("bad chunk_size %lld!=%lld ", (long long)csz, (long long)chunk_size); err = -1; } |
||||
if (c != chunk) { printf("bad chunk %d!=%d ", c, chunk); err = -1; } |
||||
if (off != offset) { printf("bad offset %lld!=%lld ", (long long)off, (long long)offset); err = -1; } |
||||
|
||||
if (err == 0 && nsig && node_pubkey) { |
||||
uint8_t sig_msg[92]; |
||||
size_t soff = 0; |
||||
memcpy(sig_msg + soff, media_id, 16); soff += 16; |
||||
memcpy(sig_msg + soff, content_hash, 32); soff += 32; |
||||
memcpy(sig_msg + soff, &ts, 8); soff += 8; |
||||
memcpy(sig_msg + soff, &fsz, 8); soff += 8; |
||||
memcpy(sig_msg + soff, &csz, 8); soff += 8; |
||||
int32_t cn = (int32_t)chunk; memcpy(sig_msg + soff, &cn, 4); soff += 4; |
||||
memcpy(sig_msg + soff, &off, 8); soff += 8; |
||||
memcpy(sig_msg + soff, &node_id, 8); soff += 8; |
||||
if (ed25519_verify(node_pubkey, sig_msg, soff, nsig) != 0) |
||||
{ printf("bad node_sign verify "); err = -1; } |
||||
} |
||||
|
||||
sqlite3_finalize(st); |
||||
return err; |
||||
} |
||||
|
||||
/* ─── тесты ─── */ |
||||
|
||||
static int test_single_chunk(void) { |
||||
TEST("single chunk, exact size (100/100)"); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); |
||||
derive_pubkey(priv, pub); |
||||
|
||||
struct media_index_result r; |
||||
fill_result(&r, 100, 100, 1); |
||||
|
||||
int rc = media_index_commit(db, &r, NODE_ID, priv, "ch1", |
||||
"/data/ch1/file.bin", "/data"); |
||||
if (rc != 0) { FAIL("commit=%d", rc); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (row_count(db, r.media_id) != 1) { FAIL("row count != 1"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
int cr = check_row(db, r.media_id, 0, r.content_hash, 100, 100, 0, NODE_ID, "ch1", |
||||
"ch1/file.bin", pub); |
||||
if (cr != 0) { FAIL("check_row"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
OK(); |
||||
media_index_result_free(&r); |
||||
free_db(db); |
||||
return 0; |
||||
} |
||||
|
||||
static int test_exact_multiple(void) { |
||||
TEST("3 chunks, exact match (300/100)"); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); derive_pubkey(priv, pub); |
||||
|
||||
struct media_index_result r; |
||||
fill_result(&r, 300, 100, 3); |
||||
|
||||
if (media_index_commit(db, &r, NODE_ID, priv, "ch2", "/data/ch2/file.bin", "/data") != 0) |
||||
{ FAIL("commit"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (row_count(db, r.media_id) != 3) { FAIL("row count != 3"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
if (check_row(db, r.media_id, 0, r.content_hash, 300, 100, 0, NODE_ID, "ch2", "ch2/file.bin", pub) != 0) |
||||
{ FAIL("chunk 0"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (check_row(db, r.media_id, 1, r.content_hash, 300, 100, 100, NODE_ID, "ch2", "ch2/file.bin", pub) != 0) |
||||
{ FAIL("chunk 1"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (check_row(db, r.media_id, 2, r.content_hash, 300, 100, 200, NODE_ID, "ch2", "ch2/file.bin", pub) != 0) |
||||
{ FAIL("chunk 2"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
OK(); |
||||
media_index_result_free(&r); free_db(db); |
||||
return 0; |
||||
} |
||||
|
||||
static int test_partial_last(void) { |
||||
TEST("last chunk smaller (250/100 → 3 chunks)"); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); derive_pubkey(priv, pub); |
||||
|
||||
struct media_index_result r; |
||||
fill_result(&r, 250, 100, 3); |
||||
|
||||
if (media_index_commit(db, &r, NODE_ID, priv, "ch3", "/data/ch3/file.bin", "/data") != 0) |
||||
{ FAIL("commit"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (row_count(db, r.media_id) != 3) { FAIL("row count != 3"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
if (check_row(db, r.media_id, 0, r.content_hash, 250, 100, 0, NODE_ID, "ch3", "ch3/file.bin", pub) != 0) |
||||
{ FAIL("chunk 0"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (check_row(db, r.media_id, 1, r.content_hash, 250, 100, 100, NODE_ID, "ch3", "ch3/file.bin", pub) != 0) |
||||
{ FAIL("chunk 1"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (check_row(db, r.media_id, 2, r.content_hash, 250, 50, 200, NODE_ID, "ch3", "ch3/file.bin", pub) != 0) |
||||
{ FAIL("chunk 2 (last, chunk_size=50)"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
OK(); |
||||
media_index_result_free(&r); free_db(db); |
||||
return 0; |
||||
} |
||||
|
||||
static int test_file_smaller_than_block(void) { |
||||
TEST("file smaller than block (50/100 → 1 chunk)"); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); derive_pubkey(priv, pub); |
||||
|
||||
struct media_index_result r; |
||||
fill_result(&r, 50, 100, 1); |
||||
|
||||
if (media_index_commit(db, &r, NODE_ID, priv, "ch4", "/data/ch4/file.bin", "/data") != 0) |
||||
{ FAIL("commit"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (row_count(db, r.media_id) != 1) { FAIL("row count != 1"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
if (check_row(db, r.media_id, 0, r.content_hash, 50, 50, 0, NODE_ID, "ch4", "ch4/file.bin", pub) != 0) |
||||
{ FAIL("chunk 0"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
OK(); |
||||
media_index_result_free(&r); free_db(db); |
||||
return 0; |
||||
} |
||||
|
||||
static int test_many_chunks(void) { |
||||
TEST("8 chunks (800/100)"); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); derive_pubkey(priv, pub); |
||||
|
||||
struct media_index_result r; |
||||
fill_result(&r, 800, 100, 8); |
||||
|
||||
if (media_index_commit(db, &r, NODE_ID, priv, "ch5", "/data/ch5/file.bin", "/data") != 0) |
||||
{ FAIL("commit"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (row_count(db, r.media_id) != 8) { FAIL("row count != 8"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
// все block_id уникальны
|
||||
for (int i = 0; i < 8; i++) { |
||||
for (int j = i + 1; j < 8; j++) { |
||||
if (memcmp(r.block_ids + i * 16, r.block_ids + j * 16, 16) == 0) |
||||
{ FAIL("block_id collision i=%d j=%d", i, j); media_index_result_free(&r); free_db(db); return -1; } |
||||
} |
||||
} |
||||
|
||||
for (int i = 0; i < 8; i++) { |
||||
int64_t cs = 100, off = i * 100; |
||||
if (check_row(db, r.media_id, i, r.content_hash, 800, cs, off, NODE_ID, "ch5", "ch5/file.bin", pub) != 0) |
||||
{ FAIL("chunk %d", i); media_index_result_free(&r); free_db(db); return -1; } |
||||
} |
||||
|
||||
OK(); |
||||
media_index_result_free(&r); free_db(db); |
||||
return 0; |
||||
} |
||||
|
||||
static int test_location_trailing_slash(void) { |
||||
TEST("media_base with trailing slash"); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); derive_pubkey(priv, pub); |
||||
|
||||
struct media_index_result r; |
||||
fill_result(&r, 100, 100, 1); |
||||
|
||||
if (media_index_commit(db, &r, NODE_ID, priv, "tc", "/root/media/tc/v.bin", "/root/") != 0) |
||||
{ FAIL("commit"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (check_row(db, r.media_id, 0, r.content_hash, 100, 100, 0, NODE_ID, "tc", "media/tc/v.bin", pub) != 0) |
||||
{ FAIL("check_row"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
OK(); |
||||
media_index_result_free(&r); free_db(db); |
||||
return 0; |
||||
} |
||||
|
||||
static int test_location_no_slash(void) { |
||||
TEST("media_base without trailing slash"); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); derive_pubkey(priv, pub); |
||||
|
||||
struct media_index_result r; |
||||
fill_result(&r, 100, 100, 1); |
||||
|
||||
if (media_index_commit(db, &r, NODE_ID, priv, "tc2", "/root/media/tc2/v.bin", "/root") != 0) |
||||
{ FAIL("commit"); media_index_result_free(&r); free_db(db); return -1; } |
||||
if (check_row(db, r.media_id, 0, r.content_hash, 100, 100, 0, NODE_ID, "tc2", "media/tc2/v.bin", pub) != 0) |
||||
{ FAIL("check_row"); media_index_result_free(&r); free_db(db); return -1; } |
||||
|
||||
OK(); |
||||
media_index_result_free(&r); free_db(db); |
||||
return 0; |
||||
} |
||||
|
||||
static int test_different_chat_ids(void) { |
||||
TEST("two commits with different chat_ids"); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); derive_pubkey(priv, pub); |
||||
|
||||
struct media_index_result r1, r2; |
||||
fill_result(&r1, 100, 100, 1); |
||||
fill_result(&r2, 200, 100, 2); |
||||
|
||||
if (media_index_commit(db, &r1, NODE_ID, priv, "chatA", "/d/cA/f.bin", "/d") != 0) |
||||
{ FAIL("commit1"); goto err; } |
||||
if (media_index_commit(db, &r2, NODE_ID + 1, priv, "chatB", "/d/cB/f.bin", "/d") != 0) |
||||
{ FAIL("commit2"); goto err; } |
||||
if (row_count(db, r1.media_id) != 1) { FAIL("r1 count"); goto err; } |
||||
if (row_count(db, r2.media_id) != 2) { FAIL("r2 count"); goto err; } |
||||
if (check_row(db, r1.media_id, 0, r1.content_hash, 100, 100, 0, NODE_ID, "chatA", "cA/f.bin", pub) != 0) |
||||
{ FAIL("r1 row"); goto err; } |
||||
if (check_row(db, r2.media_id, 0, r2.content_hash, 200, 100, 0, NODE_ID+1, "chatB", "cB/f.bin", pub) != 0) |
||||
{ FAIL("r2 row 0"); goto err; } |
||||
if (check_row(db, r2.media_id, 1, r2.content_hash, 200, 100, 100, NODE_ID+1, "chatB", "cB/f.bin", pub) != 0) |
||||
{ FAIL("r2 row 1"); goto err; } |
||||
|
||||
OK(); |
||||
media_index_result_free(&r1); media_index_result_free(&r2); free_db(db); |
||||
return 0; |
||||
err: |
||||
media_index_result_free(&r1); media_index_result_free(&r2); free_db(db); |
||||
return -1; |
||||
} |
||||
|
||||
/* ─── интеграционный тест: register_async ─── */ |
||||
|
||||
struct async_ctx { |
||||
volatile int done; |
||||
int err; |
||||
struct media_index_result result; |
||||
}; |
||||
|
||||
static void on_registered(void* arg, int err, const struct media_index_result* r) { |
||||
struct async_ctx* ctx = (struct async_ctx*)arg; |
||||
ctx->err = err; |
||||
if (!err && r) { |
||||
memcpy(&ctx->result, r, sizeof(*r)); |
||||
if (r->block_ids) { |
||||
ctx->result.block_ids = u_malloc((size_t)r->num_blocks * 16); |
||||
memcpy(ctx->result.block_ids, r->block_ids, (size_t)r->num_blocks * 16); |
||||
} |
||||
if (r->block_sigs) { |
||||
ctx->result.block_sigs = u_malloc((size_t)r->num_blocks * 64); |
||||
memcpy(ctx->result.block_sigs, r->block_sigs, (size_t)r->num_blocks * 64); |
||||
} |
||||
} else memset(&ctx->result, 0, sizeof(ctx->result)); |
||||
ctx->done = 1; |
||||
} |
||||
|
||||
static void on_timeout(void* arg) { |
||||
volatile int* done = (volatile int*)arg; |
||||
if (done) *done = 1; |
||||
} |
||||
|
||||
static int create_file(const char* path, int size) { |
||||
FILE* f = fopen(path, "wb"); |
||||
if (!f) return -1; |
||||
for (int i = 0; i < size; i++) fputc((i * 7 + 13) & 0xFF, f); |
||||
fclose(f); |
||||
return 0; |
||||
} |
||||
|
||||
static int test_register_async_flow(void) { |
||||
TEST("register_async: copy file + verify DB"); |
||||
char tempd[256]; |
||||
snprintf(tempd, sizeof(tempd), "/tmp/test_media_idx_%d", (int)getpid()); |
||||
mkdir(tempd, 0755); |
||||
|
||||
char src[512]; snprintf(src, sizeof(src), "%s/src.bin", tempd); |
||||
char dst[512]; snprintf(dst, sizeof(dst), "%s/media/ch_test/dst.bin", tempd); |
||||
{ |
||||
char t1[512], t2[512]; |
||||
snprintf(t1, sizeof(t1), "%s/media", tempd); mkdir(t1, 0755); |
||||
snprintf(t2, sizeof(t2), "%s/media/ch_test", tempd); mkdir(t2, 0755); |
||||
} |
||||
|
||||
create_file(src, 400); // 400 bytes → 4 chunks of 100
|
||||
|
||||
struct UASYNC* ua = uasync_create(); |
||||
if (!ua) { FAIL("uasync_create"); rmdir(tempd); return -1; } |
||||
|
||||
struct async_ctx ctx = {0}; |
||||
void* th = uasync_set_timeout(ua, 50000, &ctx.done, on_timeout, "tout_async"); |
||||
|
||||
struct media_async* ma = media_async_create(); |
||||
sqlite3* db = make_db(); |
||||
media_index_init(db); |
||||
|
||||
uint8_t priv[32], pub[32]; |
||||
ma_uuid(priv); ma_uuid(priv + 16); derive_pubkey(priv, pub); |
||||
|
||||
media_index_register_async(ma, ua, db, 0xCAFE, priv, "ch_test", |
||||
src, dst, 1, tempd, on_registered, &ctx); |
||||
|
||||
while (!ctx.done) uasync_poll(ua, 100); |
||||
|
||||
uasync_cancel_timeout(ua, th); |
||||
|
||||
if (ctx.err != 0) { FAIL("register_async err=%d", ctx.err); goto cleanup; } |
||||
{ struct stat st; if (stat(dst, &st) != 0) { FAIL("dst file not created"); goto cleanup; } } |
||||
|
||||
int nb = ctx.result.num_blocks; |
||||
int64_t bs = ctx.result.block_size; |
||||
int64_t fs = ctx.result.file_size; |
||||
if (fs != 400) { FAIL("file_size=%lld != 400", (long long)fs); goto cleanup; } |
||||
if (nb < 1 || nb > 400) { FAIL("num_blocks=%d unexpected", nb); goto cleanup; } |
||||
if (row_count(db, ctx.result.media_id) != nb) { FAIL("row count %d != %d", row_count(db, ctx.result.media_id), nb); goto cleanup; } |
||||
|
||||
for (int i = 0; i < nb; i++) { |
||||
int64_t cs = (i == nb - 1) ? fs - i * bs : bs; |
||||
int64_t off = i * bs; |
||||
if (check_row(db, ctx.result.media_id, i, ctx.result.content_hash, fs, cs, off, 0xCAFE, "ch_test", |
||||
"media/ch_test/dst.bin", pub) != 0) |
||||
{ FAIL("chunk %d", i); goto cleanup; } |
||||
} |
||||
|
||||
// block_ids уникальны
|
||||
for (int i = 0; i < nb; i++) |
||||
for (int j = i+1; j < nb; j++) |
||||
if (memcmp(ctx.result.block_ids + i*16, ctx.result.block_ids + j*16, 16) == 0) |
||||
{ FAIL("block_id collision i=%d j=%d", i, j); goto cleanup; } |
||||
|
||||
OK(); |
||||
|
||||
cleanup: |
||||
media_index_result_free(&ctx.result); |
||||
free_db(db); |
||||
media_async_destroy(ma); |
||||
uasync_destroy(ua, 0); |
||||
unlink(src); unlink(dst); |
||||
{ char t1[512]; snprintf(t1,sizeof(t1),"%s/media/ch_test",tempd); rmdir(t1); } |
||||
{ char t2[512]; snprintf(t2,sizeof(t2),"%s/media",tempd); rmdir(t2); } |
||||
rmdir(tempd); |
||||
return 0; |
||||
} |
||||
|
||||
/* ─── main ─── */ |
||||
|
||||
int main(void) { |
||||
debug_set_level(DEBUG_LEVEL_ERROR); |
||||
printf("=== test_media_index ===\n\n"); |
||||
|
||||
test_single_chunk(); |
||||
test_exact_multiple(); |
||||
test_partial_last(); |
||||
test_file_smaller_than_block(); |
||||
test_many_chunks(); |
||||
test_location_trailing_slash(); |
||||
test_location_no_slash(); |
||||
test_different_chat_ids(); |
||||
test_register_async_flow(); |
||||
|
||||
printf("\nResults: %d/%d passed, %d failed\n", g_passed, g_total, g_failed); |
||||
fflush(stdout); |
||||
return g_failed ? 1 : 0; |
||||
} |
||||
@ -0,0 +1,90 @@
|
||||
#include "storagesettingspage.h" |
||||
#include "../db/db_manager.h" |
||||
#include "../transport/gui_bridge.h" |
||||
#include "../../lib/mem.h" |
||||
extern "C" { |
||||
#include "chat/chat_core.h" |
||||
} |
||||
#include <QVBoxLayout> |
||||
#include <QFormLayout> |
||||
#include <QCheckBox> |
||||
#include <QSpinBox> |
||||
#include <QLabel> |
||||
#include <QFrame> |
||||
|
||||
static void saveUiStateInt(const char* key, int val) { |
||||
QByteArray v = QByteArray::number(val); |
||||
size_t klen = strlen(key), vlen = (size_t)v.size(), total = klen + 1 + vlen + 1; |
||||
if (total > 256) return; |
||||
void* arg = u_malloc(total); |
||||
memcpy(arg, key, klen + 1); |
||||
memcpy((char*)arg + klen + 1, v.constData(), vlen + 1); |
||||
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); |
||||
} |
||||
|
||||
StorageSettingsPage::StorageSettingsPage(DbManager* db, QWidget* parent) |
||||
: QWidget(parent) |
||||
, m_db(db) |
||||
{ |
||||
auto* layout = new QVBoxLayout(this); |
||||
layout->setContentsMargins(0, 12, 0, 0); |
||||
layout->setSpacing(14); |
||||
|
||||
auto* title = new QLabel("Media Storage", this); |
||||
title->setStyleSheet("font-weight: bold; font-size: 13px;"); |
||||
layout->addWidget(title); |
||||
|
||||
auto* form = new QFormLayout(); |
||||
form->setSpacing(10); |
||||
form->setContentsMargins(0, 8, 0, 0); |
||||
|
||||
m_autoloadCheck = new QCheckBox("Autoload media", this); |
||||
m_autoloadCheck->setStyleSheet("font-size: 13px;"); |
||||
form->addRow(m_autoloadCheck); |
||||
|
||||
m_autoloadMaxSize = new QSpinBox(this); |
||||
m_autoloadMaxSize->setRange(1, 500); |
||||
m_autoloadMaxSize->setSuffix(" MB"); |
||||
m_autoloadMaxSize->setFixedWidth(110); |
||||
m_autoloadMaxSize->setStyleSheet( |
||||
"QSpinBox { border: 1px solid palette(mid); border-radius: 4px;" |
||||
" padding: 5px 8px; font-size: 13px; background: palette(base); }"); |
||||
form->addRow("Autoload max size:", m_autoloadMaxSize); |
||||
|
||||
m_maxStorageSize = new QSpinBox(this); |
||||
m_maxStorageSize->setRange(1, 100); |
||||
m_maxStorageSize->setSuffix(" GB"); |
||||
m_maxStorageSize->setFixedWidth(110); |
||||
m_maxStorageSize->setStyleSheet( |
||||
"QSpinBox { border: 1px solid palette(mid); border-radius: 4px;" |
||||
" padding: 5px 8px; font-size: 13px; background: palette(base); }"); |
||||
form->addRow("Max storage size:", m_maxStorageSize); |
||||
|
||||
layout->addLayout(form); |
||||
|
||||
auto* sep = new QFrame(this); |
||||
sep->setFrameShape(QFrame::HLine); |
||||
sep->setStyleSheet("QFrame { color: palette(mid); }"); |
||||
layout->addWidget(sep); |
||||
|
||||
layout->addStretch(); |
||||
} |
||||
|
||||
void StorageSettingsPage::loadFromDb() { |
||||
if (!m_db) return; |
||||
|
||||
bool autoload = m_db->getUiStateInt("storage_autoload", 1); |
||||
m_autoloadCheck->setChecked(autoload); |
||||
|
||||
int maxSizeMb = m_db->getUiStateInt("storage_autoload_maxsize_mb", 10); |
||||
m_autoloadMaxSize->setValue(maxSizeMb); |
||||
|
||||
int maxGb = m_db->getUiStateInt("storage_maxsize_gb", 1); |
||||
m_maxStorageSize->setValue(maxGb); |
||||
} |
||||
|
||||
void StorageSettingsPage::applyAndSave() { |
||||
saveUiStateInt("storage_autoload", m_autoloadCheck->isChecked() ? 1 : 0); |
||||
saveUiStateInt("storage_autoload_maxsize_mb", m_autoloadMaxSize->value()); |
||||
saveUiStateInt("storage_maxsize_gb", m_maxStorageSize->value()); |
||||
} |
||||
@ -0,0 +1,22 @@
|
||||
#pragma once |
||||
|
||||
#include <QWidget> |
||||
|
||||
class DbManager; |
||||
class QCheckBox; |
||||
class QSpinBox; |
||||
|
||||
class StorageSettingsPage : public QWidget { |
||||
Q_OBJECT |
||||
public: |
||||
explicit StorageSettingsPage(DbManager* db, QWidget* parent = nullptr); |
||||
|
||||
void loadFromDb(); |
||||
void applyAndSave(); |
||||
|
||||
private: |
||||
DbManager* m_db = nullptr; |
||||
QCheckBox* m_autoloadCheck; |
||||
QSpinBox* m_autoloadMaxSize; |
||||
QSpinBox* m_maxStorageSize; |
||||
}; |
||||
Loading…
Reference in new issue