database_flatfile: single-file storage with sparse index
Replace per-contact directory structure with a single flat file per contact. Each file uses pipe-delimited records with a sparse byte-offset index that is rebuilt on demand and cached in memory. - Rewrite _ff_write_record / _ff_read_records for single-file format - Add sparse index (every Nth record) for O(log N) seek on history read - Cursor-based pagination: _ff_get_previous_chat uses saved byte offset - LMC corrections applied in-place during read (_ff_apply_lmc) - Newline escaping (\n) in message bodies for line-based storage - Simplify verify: parse + timestamp order + LMC reference check - Update parser helpers for new field layout
This commit is contained in:
@@ -31,6 +31,8 @@
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <glib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -52,6 +54,261 @@
|
||||
// =========================================================================
|
||||
|
||||
char* g_flatfile_account_jid = NULL;
|
||||
static GHashTable* g_contact_states = NULL;
|
||||
|
||||
// =========================================================================
|
||||
// Per-contact state / sparse index
|
||||
// =========================================================================
|
||||
|
||||
ff_contact_state_t*
|
||||
ff_state_new(const char* filepath)
|
||||
{
|
||||
ff_contact_state_t* state = g_malloc0(sizeof(ff_contact_state_t));
|
||||
state->filepath = g_strdup(filepath);
|
||||
state->cursor_offset = -1;
|
||||
return state;
|
||||
}
|
||||
|
||||
void
|
||||
ff_state_free(ff_contact_state_t* state)
|
||||
{
|
||||
if (!state)
|
||||
return;
|
||||
g_free(state->filepath);
|
||||
g_free(state->entries);
|
||||
g_free(state);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_ff_state_build(ff_contact_state_t* state)
|
||||
{
|
||||
FILE* fp = fopen(state->filepath, "r");
|
||||
if (!fp)
|
||||
return FALSE;
|
||||
|
||||
int c1 = fgetc(fp);
|
||||
int c2 = fgetc(fp);
|
||||
int c3 = fgetc(fp);
|
||||
if (c1 == 0xEF && c2 == 0xBB && c3 == 0xBF) {
|
||||
state->bom_len = 3;
|
||||
} else {
|
||||
state->bom_len = 0;
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
state->n_entries = 0;
|
||||
state->total_lines = 0;
|
||||
size_t msg_count = 0;
|
||||
|
||||
while (1) {
|
||||
off_t pos = ftell(fp);
|
||||
gboolean truncated = FALSE;
|
||||
char* buf = ff_readline(fp, &truncated);
|
||||
if (!buf)
|
||||
break;
|
||||
if (truncated) {
|
||||
free(buf);
|
||||
break;
|
||||
}
|
||||
|
||||
if (buf[0] == '\0' || buf[0] == '#') {
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
state->total_lines++;
|
||||
|
||||
if (msg_count % FF_INDEX_STEP == 0) {
|
||||
char* space = strchr(buf, ' ');
|
||||
if (space) {
|
||||
char* ts_str = g_strndup(buf, space - buf);
|
||||
GDateTime* dt = g_date_time_new_from_iso8601(ts_str, NULL);
|
||||
if (dt) {
|
||||
if (state->n_entries >= state->cap_entries) {
|
||||
state->cap_entries = state->cap_entries ? state->cap_entries * 2 : 64;
|
||||
state->entries = g_realloc(state->entries,
|
||||
state->cap_entries * sizeof(ff_index_entry_t));
|
||||
}
|
||||
state->entries[state->n_entries].byte_offset = pos;
|
||||
state->entries[state->n_entries].timestamp_epoch = g_date_time_to_unix(dt);
|
||||
state->n_entries++;
|
||||
g_date_time_unref(dt);
|
||||
}
|
||||
g_free(ts_str);
|
||||
}
|
||||
}
|
||||
msg_count++;
|
||||
free(buf);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
struct stat st;
|
||||
if (stat(state->filepath, &st) == 0) {
|
||||
state->stamp.mtime = st.st_mtime;
|
||||
state->stamp.size = st.st_size;
|
||||
state->stamp.inode = st.st_ino;
|
||||
}
|
||||
|
||||
log_debug("flatfile: index built for %s (%zu entries, %zu lines)",
|
||||
state->filepath, state->n_entries, state->total_lines);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_ff_state_extend(ff_contact_state_t* state)
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(state->filepath, &st) != 0)
|
||||
return FALSE;
|
||||
|
||||
if (st.st_size <= state->stamp.size)
|
||||
return _ff_state_build(state);
|
||||
|
||||
FILE* fp = fopen(state->filepath, "r");
|
||||
if (!fp)
|
||||
return FALSE;
|
||||
|
||||
fseek(fp, state->stamp.size, SEEK_SET);
|
||||
if (state->stamp.size > 0) {
|
||||
fseek(fp, state->stamp.size - 1, SEEK_SET);
|
||||
int prev = fgetc(fp);
|
||||
if (prev != '\n' && prev != EOF) {
|
||||
int ch;
|
||||
while ((ch = fgetc(fp)) != EOF && ch != '\n') {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
size_t msg_count = state->total_lines;
|
||||
|
||||
while (1) {
|
||||
off_t pos = ftell(fp);
|
||||
gboolean truncated = FALSE;
|
||||
char* buf = ff_readline(fp, &truncated);
|
||||
if (!buf)
|
||||
break;
|
||||
if (truncated) {
|
||||
free(buf);
|
||||
break;
|
||||
}
|
||||
|
||||
if (buf[0] == '\0' || buf[0] == '#') {
|
||||
free(buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
state->total_lines++;
|
||||
|
||||
if (msg_count % FF_INDEX_STEP == 0) {
|
||||
char* space = strchr(buf, ' ');
|
||||
if (space) {
|
||||
char* ts_str = g_strndup(buf, space - buf);
|
||||
GDateTime* dt = g_date_time_new_from_iso8601(ts_str, NULL);
|
||||
if (dt) {
|
||||
if (state->n_entries >= state->cap_entries) {
|
||||
state->cap_entries = state->cap_entries ? state->cap_entries * 2 : 64;
|
||||
state->entries = g_realloc(state->entries,
|
||||
state->cap_entries * sizeof(ff_index_entry_t));
|
||||
}
|
||||
state->entries[state->n_entries].byte_offset = pos;
|
||||
state->entries[state->n_entries].timestamp_epoch = g_date_time_to_unix(dt);
|
||||
state->n_entries++;
|
||||
g_date_time_unref(dt);
|
||||
}
|
||||
g_free(ts_str);
|
||||
}
|
||||
}
|
||||
msg_count++;
|
||||
free(buf);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
state->stamp.mtime = st.st_mtime;
|
||||
state->stamp.size = st.st_size;
|
||||
state->stamp.inode = st.st_ino;
|
||||
|
||||
log_debug("flatfile: index extended for %s (%zu entries, %zu lines)",
|
||||
state->filepath, state->n_entries, state->total_lines);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
ff_state_ensure_fresh(ff_contact_state_t* state)
|
||||
{
|
||||
if (!state || !state->filepath)
|
||||
return FALSE;
|
||||
|
||||
struct stat st;
|
||||
if (stat(state->filepath, &st) != 0) {
|
||||
state->total_lines = 0;
|
||||
state->n_entries = 0;
|
||||
state->stamp.size = 0;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (state->stamp.size == 0 && state->n_entries == 0)
|
||||
return _ff_state_build(state);
|
||||
|
||||
if (st.st_mtime == state->stamp.mtime
|
||||
&& st.st_size == state->stamp.size
|
||||
&& st.st_ino == state->stamp.inode) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (st.st_ino == state->stamp.inode && st.st_size > state->stamp.size)
|
||||
return _ff_state_extend(state);
|
||||
|
||||
state->cursor_offset = -1;
|
||||
return _ff_state_build(state);
|
||||
}
|
||||
|
||||
off_t
|
||||
ff_state_offset_for_time(ff_contact_state_t* state, const char* iso_time)
|
||||
{
|
||||
if (!state || !iso_time || state->n_entries == 0)
|
||||
return state ? (off_t)state->bom_len : 0;
|
||||
|
||||
GDateTime* dt = g_date_time_new_from_iso8601(iso_time, NULL);
|
||||
if (!dt)
|
||||
return state->bom_len;
|
||||
|
||||
gint64 target = g_date_time_to_unix(dt);
|
||||
g_date_time_unref(dt);
|
||||
|
||||
size_t lo = 0, hi = state->n_entries;
|
||||
while (lo < hi) {
|
||||
size_t mid = lo + (hi - lo) / 2;
|
||||
if (state->entries[mid].timestamp_epoch <= target)
|
||||
lo = mid + 1;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
|
||||
if (lo == 0)
|
||||
return state->bom_len;
|
||||
return state->entries[lo - 1].byte_offset;
|
||||
}
|
||||
|
||||
static ff_contact_state_t*
|
||||
_ff_get_state(const char* contact_barejid)
|
||||
{
|
||||
if (!g_contact_states || !contact_barejid)
|
||||
return NULL;
|
||||
|
||||
ff_contact_state_t* state = g_hash_table_lookup(g_contact_states, contact_barejid);
|
||||
if (state)
|
||||
return state;
|
||||
|
||||
auto_gchar gchar* filepath = ff_get_log_path(contact_barejid);
|
||||
if (!filepath)
|
||||
return NULL;
|
||||
|
||||
state = ff_state_new(filepath);
|
||||
g_hash_table_insert(g_contact_states, g_strdup(contact_barejid), state);
|
||||
return state;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Core write logic
|
||||
@@ -79,6 +336,7 @@ _ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
|
||||
|
||||
GDateTime* dt = timestamp ? g_date_time_ref(timestamp) : g_date_time_new_now_local();
|
||||
auto_gchar gchar* date_fmt = g_date_time_format_iso8601(dt);
|
||||
g_date_time_unref(dt);
|
||||
|
||||
// Determine which JID to use for the log file path (the "other" party)
|
||||
const char* contact = NULL;
|
||||
@@ -89,8 +347,7 @@ _ff_add_message(const char* type, const char* stanza_id, const char* archive_id,
|
||||
contact = from_barejid;
|
||||
}
|
||||
|
||||
auto_gchar gchar* log_path = ff_get_log_path(contact, dt);
|
||||
g_date_time_unref(dt);
|
||||
auto_gchar gchar* log_path = ff_get_log_path(contact);
|
||||
|
||||
if (!log_path) {
|
||||
log_error("flatfile: could not determine log path for %s", contact);
|
||||
@@ -170,6 +427,12 @@ _flatfile_init(ProfAccount* account)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (g_contact_states) {
|
||||
g_hash_table_destroy(g_contact_states);
|
||||
}
|
||||
g_contact_states = g_hash_table_new_full(g_str_hash, g_str_equal,
|
||||
g_free, (GDestroyNotify)ff_state_free);
|
||||
|
||||
log_info("Initialized flat-file database backend: %s", base_dir);
|
||||
return TRUE;
|
||||
}
|
||||
@@ -177,6 +440,10 @@ _flatfile_init(ProfAccount* account)
|
||||
static void
|
||||
_flatfile_close(void)
|
||||
{
|
||||
if (g_contact_states) {
|
||||
g_hash_table_destroy(g_contact_states);
|
||||
g_contact_states = NULL;
|
||||
}
|
||||
g_free(g_flatfile_account_jid);
|
||||
g_flatfile_account_jid = NULL;
|
||||
log_debug("flatfile: closed");
|
||||
@@ -235,113 +502,6 @@ _flatfile_add_outgoing_muc_pm(const char* const id, const char* const barejid,
|
||||
// Read logic
|
||||
// =========================================================================
|
||||
|
||||
// List all log files for a contact, sorted by filename (date order)
|
||||
static GSList*
|
||||
_ff_list_log_files(const char* contact_barejid)
|
||||
{
|
||||
auto_gchar gchar* contact_dir = ff_get_contact_dir(contact_barejid);
|
||||
if (!contact_dir || !g_file_test(contact_dir, G_FILE_TEST_IS_DIR)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
GDir* dir = g_dir_open(contact_dir, 0, NULL);
|
||||
if (!dir)
|
||||
return NULL;
|
||||
|
||||
GSList* files = NULL;
|
||||
const gchar* fname;
|
||||
while ((fname = g_dir_read_name(dir)) != NULL) {
|
||||
if (g_str_has_suffix(fname, ".log")) {
|
||||
files = g_slist_insert_sorted(files,
|
||||
g_strdup_printf("%s/%s", contact_dir, fname),
|
||||
(GCompareFunc)g_strcmp0);
|
||||
}
|
||||
}
|
||||
g_dir_close(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
// Read all parsed lines from a file, applying filters
|
||||
static GSList*
|
||||
_ff_read_file_messages(const char* filepath, const char* contact_barejid,
|
||||
const char* start_time, const char* end_time,
|
||||
const Jid* myjid)
|
||||
{
|
||||
GSList* messages = NULL;
|
||||
|
||||
FILE* fp = fopen(filepath, "r");
|
||||
if (!fp)
|
||||
return NULL;
|
||||
|
||||
// BOM detection
|
||||
int c1 = fgetc(fp);
|
||||
int c2 = fgetc(fp);
|
||||
int c3 = fgetc(fp);
|
||||
if (!(c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)) {
|
||||
// Not a BOM, rewind
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
char* buf = NULL;
|
||||
gboolean truncated = FALSE;
|
||||
while ((buf = ff_readline(fp, &truncated)) != NULL) {
|
||||
if (truncated) {
|
||||
log_warning("flatfile: truncated line at EOF in %s, skipping", filepath);
|
||||
free(buf);
|
||||
break;
|
||||
}
|
||||
|
||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||
free(buf);
|
||||
if (!pl)
|
||||
continue;
|
||||
|
||||
// Check timestamp bounds
|
||||
if (start_time) {
|
||||
GDateTime* start_dt = g_date_time_new_from_iso8601(start_time, NULL);
|
||||
if (start_dt) {
|
||||
if (g_date_time_compare(pl->timestamp, start_dt) <= 0) {
|
||||
g_date_time_unref(start_dt);
|
||||
ff_parsed_line_free(pl);
|
||||
continue;
|
||||
}
|
||||
g_date_time_unref(start_dt);
|
||||
}
|
||||
}
|
||||
if (end_time) {
|
||||
GDateTime* end_dt = g_date_time_new_from_iso8601(end_time, NULL);
|
||||
if (end_dt) {
|
||||
if (g_date_time_compare(pl->timestamp, end_dt) >= 0) {
|
||||
g_date_time_unref(end_dt);
|
||||
ff_parsed_line_free(pl);
|
||||
continue;
|
||||
}
|
||||
g_date_time_unref(end_dt);
|
||||
}
|
||||
}
|
||||
|
||||
// Check JID filter: message must involve contact and our JID
|
||||
gboolean matches = FALSE;
|
||||
if (myjid && myjid->barejid && contact_barejid) {
|
||||
if ((g_strcmp0(pl->from_jid, contact_barejid) == 0) || (g_strcmp0(pl->from_jid, myjid->barejid) == 0)) {
|
||||
matches = TRUE;
|
||||
}
|
||||
} else {
|
||||
matches = TRUE; // no filter
|
||||
}
|
||||
|
||||
if (!matches) {
|
||||
ff_parsed_line_free(pl);
|
||||
continue;
|
||||
}
|
||||
|
||||
messages = g_slist_append(messages, pl);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Apply LMC: for messages with corrects:X, find original and replace its text
|
||||
static void
|
||||
_ff_apply_lmc(GSList* parsed_lines, GSList** result)
|
||||
@@ -435,52 +595,160 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
return DB_RESPONSE_ERROR;
|
||||
}
|
||||
|
||||
// If no end_time, use now
|
||||
auto_gchar gchar* effective_end = NULL;
|
||||
if (end_time) {
|
||||
effective_end = g_strdup(end_time);
|
||||
} else {
|
||||
GDateTime* now = g_date_time_new_now_local();
|
||||
effective_end = g_date_time_format_iso8601(now);
|
||||
g_date_time_unref(now);
|
||||
}
|
||||
// Get or create per-contact state with sparse index
|
||||
ff_contact_state_t* state = _ff_get_state(contact_barejid);
|
||||
if (!state)
|
||||
return DB_RESPONSE_ERROR;
|
||||
|
||||
// Get all log files for this contact
|
||||
GSList* files = _ff_list_log_files(contact_barejid);
|
||||
if (!files) {
|
||||
// Ensure index is up-to-date (stat check -> build/extend if file changed)
|
||||
if (!ff_state_ensure_fresh(state))
|
||||
return DB_RESPONSE_EMPTY;
|
||||
if (state->total_lines == 0)
|
||||
return DB_RESPONSE_EMPTY;
|
||||
|
||||
// Reset cursor for filtered (non-Page-Up) queries
|
||||
if (start_time)
|
||||
state->cursor_offset = -1;
|
||||
|
||||
// Determine read byte-range using the sparse index
|
||||
off_t read_from = state->bom_len;
|
||||
off_t read_to = state->stamp.size;
|
||||
|
||||
// Use stored cursor for sequential Page Up (skip bisect)
|
||||
if (!start_time && end_time && state->cursor_offset >= 0) {
|
||||
read_to = state->cursor_offset;
|
||||
} else if (end_time) {
|
||||
// Upper-bound: find first index entry AFTER end_time
|
||||
GDateTime* edt = g_date_time_new_from_iso8601(end_time, NULL);
|
||||
if (edt) {
|
||||
gint64 end_epoch = g_date_time_to_unix(edt);
|
||||
g_date_time_unref(edt);
|
||||
size_t lo = 0, hi = state->n_entries;
|
||||
while (lo < hi) {
|
||||
size_t mid = lo + (hi - lo) / 2;
|
||||
if (state->entries[mid].timestamp_epoch <= end_epoch)
|
||||
lo = mid + 1;
|
||||
else
|
||||
hi = mid;
|
||||
}
|
||||
// lo = first entry with timestamp > end_epoch
|
||||
// Use next entry's offset as read_to (+ 1 entry margin)
|
||||
if (lo + 1 < state->n_entries)
|
||||
read_to = state->entries[lo + 1].byte_offset;
|
||||
// else read_to stays at EOF
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all matching parsed lines across files
|
||||
if (start_time) {
|
||||
read_from = ff_state_offset_for_time(state, start_time);
|
||||
}
|
||||
|
||||
// For "last N messages": back up from read_to using index
|
||||
if (!from_start) {
|
||||
int margin_entries = (MESSAGES_TO_RETRIEVE * 3) / FF_INDEX_STEP + 2;
|
||||
|
||||
// Find index entry closest to (but before) read_to
|
||||
size_t entry_idx = 0;
|
||||
for (size_t i = 0; i < state->n_entries; i++) {
|
||||
if (state->entries[i].byte_offset >= read_to)
|
||||
break;
|
||||
entry_idx = i;
|
||||
}
|
||||
|
||||
size_t start_entry = entry_idx > (size_t)margin_entries
|
||||
? entry_idx - margin_entries
|
||||
: 0;
|
||||
off_t backed = state->entries[start_entry].byte_offset;
|
||||
if (backed > read_from)
|
||||
read_from = backed;
|
||||
}
|
||||
|
||||
// Open file and read lines in [read_from, read_to)
|
||||
FILE* fp = fopen(state->filepath, "r");
|
||||
if (!fp)
|
||||
return DB_RESPONSE_EMPTY;
|
||||
|
||||
fseek(fp, read_from, SEEK_SET);
|
||||
|
||||
// Pre-parse time filter boundaries once (avoid per-line allocation)
|
||||
GDateTime* start_dt = start_time ? g_date_time_new_from_iso8601(start_time, NULL) : NULL;
|
||||
GDateTime* end_dt = end_time ? g_date_time_new_from_iso8601(end_time, NULL) : NULL;
|
||||
|
||||
GSList* all_parsed = NULL;
|
||||
for (GSList* f = files; f; f = f->next) {
|
||||
GSList* file_msgs = _ff_read_file_messages(f->data, contact_barejid,
|
||||
start_time, effective_end, myjid);
|
||||
all_parsed = g_slist_concat(all_parsed, file_msgs);
|
||||
while (1) {
|
||||
off_t pos = ftell(fp);
|
||||
if (pos < 0 || pos >= read_to)
|
||||
break;
|
||||
|
||||
gboolean truncated = FALSE;
|
||||
char* buf = ff_readline(fp, &truncated);
|
||||
if (!buf)
|
||||
break;
|
||||
if (truncated) {
|
||||
free(buf);
|
||||
break;
|
||||
}
|
||||
|
||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||
free(buf);
|
||||
if (!pl)
|
||||
continue;
|
||||
|
||||
pl->file_offset = pos;
|
||||
|
||||
// Time filters
|
||||
if (start_dt && g_date_time_compare(pl->timestamp, start_dt) <= 0) {
|
||||
ff_parsed_line_free(pl);
|
||||
continue;
|
||||
}
|
||||
if (end_dt && g_date_time_compare(pl->timestamp, end_dt) >= 0) {
|
||||
ff_parsed_line_free(pl);
|
||||
continue;
|
||||
}
|
||||
|
||||
// JID filter
|
||||
gboolean matches = FALSE;
|
||||
if (myjid->barejid && contact_barejid) {
|
||||
if ((g_strcmp0(pl->from_jid, contact_barejid) == 0)
|
||||
|| (g_strcmp0(pl->from_jid, myjid->barejid) == 0)) {
|
||||
matches = TRUE;
|
||||
}
|
||||
} else {
|
||||
matches = TRUE;
|
||||
}
|
||||
if (!matches) {
|
||||
ff_parsed_line_free(pl);
|
||||
continue;
|
||||
}
|
||||
|
||||
all_parsed = g_slist_append(all_parsed, pl);
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
g_slist_free_full(files, g_free);
|
||||
if (start_dt)
|
||||
g_date_time_unref(start_dt);
|
||||
if (end_dt)
|
||||
g_date_time_unref(end_dt);
|
||||
|
||||
if (!all_parsed) {
|
||||
if (!all_parsed)
|
||||
return DB_RESPONSE_EMPTY;
|
||||
}
|
||||
|
||||
// Update cursor: byte offset of oldest parsed message in this batch
|
||||
ff_parsed_line_t* oldest = all_parsed->data;
|
||||
state->cursor_offset = oldest->file_offset;
|
||||
|
||||
// Apply LMC corrections and build ProfMessage list
|
||||
GSList* pre_result = NULL;
|
||||
_ff_apply_lmc(all_parsed, &pre_result);
|
||||
|
||||
// Free parsed lines
|
||||
g_slist_free_full(all_parsed, (GDestroyNotify)ff_parsed_line_free);
|
||||
|
||||
if (!pre_result) {
|
||||
if (!pre_result)
|
||||
return DB_RESPONSE_EMPTY;
|
||||
}
|
||||
|
||||
// Apply pagination: take first or last MESSAGES_TO_RETRIEVE
|
||||
// Paginate: keep first or last MESSAGES_TO_RETRIEVE
|
||||
guint total = g_slist_length(pre_result);
|
||||
if (total > MESSAGES_TO_RETRIEVE) {
|
||||
if (from_start) {
|
||||
// Keep first N, free rest
|
||||
GSList* nth = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE);
|
||||
if (nth) {
|
||||
GSList* prev = g_slist_nth(pre_result, MESSAGES_TO_RETRIEVE - 1);
|
||||
@@ -489,7 +757,6 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
g_slist_free_full(nth, (GDestroyNotify)message_free);
|
||||
}
|
||||
} else {
|
||||
// Keep last N, free first (total - N)
|
||||
guint skip = total - MESSAGES_TO_RETRIEVE;
|
||||
GSList* keep_start = g_slist_nth(pre_result, skip);
|
||||
GSList* prev = g_slist_nth(pre_result, skip - 1);
|
||||
@@ -500,7 +767,6 @@ _flatfile_get_previous_chat(const gchar* const contact_barejid, const gchar* sta
|
||||
}
|
||||
}
|
||||
|
||||
// Apply flip (reverse order)
|
||||
if (flip) {
|
||||
pre_result = g_slist_reverse(pre_result);
|
||||
}
|
||||
@@ -520,39 +786,32 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
||||
return msg;
|
||||
}
|
||||
|
||||
GSList* files = _ff_list_log_files(contact_barejid);
|
||||
if (!files) {
|
||||
ff_contact_state_t* state = _ff_get_state(contact_barejid);
|
||||
if (!state) {
|
||||
if (is_last)
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
return msg;
|
||||
}
|
||||
|
||||
// For first message: read first non-comment line of first file
|
||||
// For last message: read last non-comment line of last file
|
||||
const char* target_file = is_last ? (const char*)g_slist_last(files)->data
|
||||
: (const char*)files->data;
|
||||
if (!ff_state_ensure_fresh(state) || state->total_lines == 0) {
|
||||
if (is_last)
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
return msg;
|
||||
}
|
||||
|
||||
FILE* fp = fopen(target_file, "r");
|
||||
FILE* fp = fopen(state->filepath, "r");
|
||||
if (!fp) {
|
||||
g_slist_free_full(files, g_free);
|
||||
if (is_last)
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
return msg;
|
||||
}
|
||||
|
||||
// BOM skip
|
||||
int c1 = fgetc(fp);
|
||||
int c2 = fgetc(fp);
|
||||
int c3 = fgetc(fp);
|
||||
if (!(c1 == 0xEF && c2 == 0xBB && c3 == 0xBF)) {
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
}
|
||||
|
||||
char* buf = NULL;
|
||||
ff_parsed_line_t* found = NULL;
|
||||
|
||||
if (!is_last) {
|
||||
// Find first valid line
|
||||
// First message: start from beginning (skip BOM)
|
||||
fseek(fp, state->bom_len, SEEK_SET);
|
||||
char* buf;
|
||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||
found = ff_parse_line(buf);
|
||||
free(buf);
|
||||
@@ -560,7 +819,13 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Find last valid line — read all and keep last
|
||||
// Last message: seek near end using index for efficiency
|
||||
off_t seek_pos = 0;
|
||||
if (state->n_entries > 0)
|
||||
seek_pos = state->entries[state->n_entries - 1].byte_offset;
|
||||
fseek(fp, seek_pos, SEEK_SET);
|
||||
|
||||
char* buf;
|
||||
while ((buf = ff_readline(fp, NULL)) != NULL) {
|
||||
ff_parsed_line_t* pl = ff_parse_line(buf);
|
||||
free(buf);
|
||||
@@ -581,7 +846,6 @@ _flatfile_get_limits_info(const gchar* const contact_barejid, gboolean is_last)
|
||||
msg->timestamp = g_date_time_new_now_utc();
|
||||
}
|
||||
|
||||
g_slist_free_full(files, g_free);
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user