refactor: extract helpers for verify print, scan loop, and per-contact export

- extract _show_integrity_issues() from cmd_history verify block
- extract _ff_scan_lines() shared by _ff_state_build and _ff_state_extend
- extract _export_one_contact() from log_database_export_to_flatfile loop
This commit is contained in:
2026-03-28 12:58:27 +03:00
parent a96a4ad41e
commit 7ba7e53291
3 changed files with 265 additions and 271 deletions

View File

@@ -6729,6 +6729,54 @@ cmd_logging(ProfWin* window, const char* const command, gchar** args)
return TRUE; return TRUE;
} }
static void
_show_integrity_issues(GSList* issues)
{
int errors = 0, warnings = 0, infos = 0;
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* issue = l->data;
const char* level_str;
switch (issue->level) {
case INTEGRITY_ERROR:
level_str = "ERROR";
errors++;
break;
case INTEGRITY_WARNING:
level_str = "WARN";
warnings++;
break;
case INTEGRITY_INFO:
level_str = "INFO";
infos++;
break;
default:
level_str = "???";
break;
}
if (issue->line > 0) {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
} else {
cons_show("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
}
} else {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s — %s", level_str, issue->file, issue->message);
} else {
cons_show("[%s] %s — %s", level_str, issue->file, issue->message);
}
}
}
if (!issues) {
cons_show("Verification complete: no issues found.");
} else {
cons_show("Verification complete: %d error(s), %d warning(s), %d info(s).",
errors, warnings, infos);
g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free);
}
}
gboolean gboolean
cmd_history(ProfWin* window, const char* const command, gchar** args) cmd_history(ProfWin* window, const char* const command, gchar** args)
{ {
@@ -6773,50 +6821,7 @@ cmd_history(ProfWin* window, const char* const command, gchar** args)
cons_show("Verifying history integrity..."); cons_show("Verifying history integrity...");
GSList* issues = log_database_verify_integrity(contact_jid); GSList* issues = log_database_verify_integrity(contact_jid);
_show_integrity_issues(issues);
int errors = 0, warnings = 0, infos = 0;
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* issue = l->data;
const char* level_str;
switch (issue->level) {
case INTEGRITY_ERROR:
level_str = "ERROR";
errors++;
break;
case INTEGRITY_WARNING:
level_str = "WARN";
warnings++;
break;
case INTEGRITY_INFO:
level_str = "INFO";
infos++;
break;
default:
level_str = "???";
break;
}
if (issue->line > 0) {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
} else {
cons_show("[%s] %s:%d — %s", level_str, issue->file, issue->line, issue->message);
}
} else {
if (issue->level == INTEGRITY_ERROR) {
cons_show_error("[%s] %s — %s", level_str, issue->file, issue->message);
} else {
cons_show("[%s] %s — %s", level_str, issue->file, issue->message);
}
}
}
if (!issues) {
cons_show("Verification complete: no issues found.");
} else {
cons_show("Verification complete: %d error(s), %d warning(s), %d info(s).",
errors, warnings, infos);
g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free);
}
return TRUE; return TRUE;
} }

View File

@@ -195,46 +195,11 @@ typedef struct
int skipped; // counter int skipped; // counter
} export_ctx_t; } export_ctx_t;
int // Export a single contact's messages from SQLite to flatfile.
log_database_export_to_flatfile(const gchar* const contact_jid) // Returns the number of newly exported messages, or -1 on error/skip.
static int
_export_one_contact(const char* contact)
{ {
const Jid* myjid = connection_get_jid();
if (!myjid || !myjid->barejid) {
cons_show_error("Export failed: not connected.");
return -1;
}
if (!g_flatfile_account_jid) {
// Flatfile backend not initialized — init it temporarily
// We need g_flatfile_account_jid for path construction
g_flatfile_account_jid = g_strdup(myjid->barejid);
}
// Get the SQLite backend
db_backend_t* sqlite_be = db_backend_sqlite();
if (!sqlite_be) {
cons_show_error("Export failed: SQLite backend not available.");
return -1;
}
// Determine contact list
GSList* contacts = NULL;
if (contact_jid) {
contacts = g_slist_append(contacts, g_strdup(contact_jid));
} else {
contacts = db_sqlite_list_contacts();
if (!contacts) {
cons_show("No contacts found in SQLite database.");
return 0;
}
cons_show("Exporting %d contact(s) to flat-file format...", g_slist_length(contacts));
}
int total_exported = 0;
for (GSList* c = contacts; c; c = c->next) {
const char* contact = c->data;
// 1. Read existing flatfile lines for dedup // 1. Read existing flatfile lines for dedup
GHashTable* seen_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); GHashTable* seen_keys = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
@@ -253,7 +218,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
log_debug("export: no SQLite messages for %s", contact); log_debug("export: no SQLite messages for %s", contact);
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
continue; return -1;
} }
// 3. Merge: write existing flatfile lines + new SQLite lines to temp file // 3. Merge: write existing flatfile lines + new SQLite lines to temp file
@@ -262,7 +227,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free); g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
continue; return -1;
} }
// Ensure directory // Ensure directory
@@ -286,7 +251,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free); g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
continue; return -1;
} }
FILE* fp = fdopen(tmp_fd, "w"); FILE* fp = fdopen(tmp_fd, "w");
if (!fp) { if (!fp) {
@@ -296,7 +261,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free); g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
continue; return -1;
} }
// Lock the temp file // Lock the temp file
@@ -397,6 +362,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
fclose(fp); // also releases flock fclose(fp); // also releases flock
// Atomic rename // Atomic rename
int result = -1;
if (rename(tmp_path, log_path) != 0) { if (rename(tmp_path, log_path) != 0) {
log_error("export: rename %s -> %s failed: %s", tmp_path, log_path, strerror(errno)); log_error("export: rename %s -> %s failed: %s", tmp_path, log_path, strerror(errno));
cons_show_error("Export failed for %s: could not rename temp file (%s)", contact, strerror(errno)); cons_show_error("Export failed for %s: could not rename temp file (%s)", contact, strerror(errno));
@@ -404,12 +370,57 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
} else { } else {
cons_show("Exported %s: %d new, %d skipped (already existed: %d)", cons_show("Exported %s: %d new, %d skipped (already existed: %d)",
contact, contact_exported, contact_skipped, existing_count); contact, contact_exported, contact_skipped, existing_count);
total_exported += contact_exported; result = contact_exported;
} }
g_hash_table_destroy(seen_keys); g_hash_table_destroy(seen_keys);
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free); g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
return result;
}
int
log_database_export_to_flatfile(const gchar* const contact_jid)
{
const Jid* myjid = connection_get_jid();
if (!myjid || !myjid->barejid) {
cons_show_error("Export failed: not connected.");
return -1;
}
if (!g_flatfile_account_jid) {
// Flatfile backend not initialized — init it temporarily
// We need g_flatfile_account_jid for path construction
g_flatfile_account_jid = g_strdup(myjid->barejid);
}
// Get the SQLite backend
db_backend_t* sqlite_be = db_backend_sqlite();
if (!sqlite_be) {
cons_show_error("Export failed: SQLite backend not available.");
return -1;
}
// Determine contact list
GSList* contacts = NULL;
if (contact_jid) {
contacts = g_slist_append(contacts, g_strdup(contact_jid));
} else {
contacts = db_sqlite_list_contacts();
if (!contacts) {
cons_show("No contacts found in SQLite database.");
return 0;
}
cons_show("Exporting %d contact(s) to flat-file format...", g_slist_length(contacts));
}
int total_exported = 0;
for (GSList* c = contacts; c; c = c->next) {
const char* contact = c->data;
int exported = _export_one_contact(contact);
if (exported > 0)
total_exported += exported;
} }
g_slist_free_full(contacts, g_free); g_slist_free_full(contacts, g_free);

View File

@@ -163,23 +163,12 @@ _ff_maybe_index_line(ff_contact_state_t* state, const char* buf, off_t pos)
g_date_time_unref(dt); g_date_time_unref(dt);
} }
static gboolean // Scan lines from an already-positioned file pointer, caching IDs and
_ff_state_build(ff_contact_state_t* state) // building index entries. Returns the number of messages scanned.
static size_t
_ff_scan_lines(ff_contact_state_t* state, FILE* fp)
{ {
FILE* fp = fopen(state->filepath, "r"); size_t count = 0;
if (!fp)
return FALSE;
state->bom_len = ff_skip_bom(fp);
state->n_entries = 0;
state->total_lines = 0;
size_t msg_count = 0;
// Clear ID caches for full rebuild
g_hash_table_remove_all(state->archive_ids);
g_hash_table_remove_all(state->stanza_senders);
while (1) { while (1) {
off_t pos = ftell(fp); off_t pos = ftell(fp);
gboolean truncated = FALSE; gboolean truncated = FALSE;
@@ -197,16 +186,34 @@ _ff_state_build(ff_contact_state_t* state)
} }
state->total_lines++; state->total_lines++;
// Extract IDs for O(1) dedup/LMC cache
_ff_cache_line_ids(state, buf); _ff_cache_line_ids(state, buf);
if (msg_count % FF_INDEX_STEP == 0) { if (count % FF_INDEX_STEP == 0) {
_ff_maybe_index_line(state, buf, pos); _ff_maybe_index_line(state, buf, pos);
} }
msg_count++; count++;
free(buf); free(buf);
} }
return count;
}
static gboolean
_ff_state_build(ff_contact_state_t* state)
{
FILE* fp = fopen(state->filepath, "r");
if (!fp)
return FALSE;
state->bom_len = ff_skip_bom(fp);
state->n_entries = 0;
state->total_lines = 0;
// Clear ID caches for full rebuild
g_hash_table_remove_all(state->archive_ids);
g_hash_table_remove_all(state->stanza_senders);
_ff_scan_lines(state, fp);
fclose(fp); fclose(fp);
@@ -247,37 +254,8 @@ _ff_state_extend(ff_contact_state_t* state)
} }
} }
// Use a local counter for new messages so index step alignment // Use _ff_scan_lines for the shared read/index/cache loop.
// doesn't depend on the total from the initial build. _ff_scan_lines(state, fp);
size_t new_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++;
// Extract IDs for O(1) dedup/LMC cache
_ff_cache_line_ids(state, buf);
if (new_count % FF_INDEX_STEP == 0) {
_ff_maybe_index_line(state, buf, pos);
}
new_count++;
free(buf);
}
fclose(fp); fclose(fp);