mirror of
https://git.jabber.space/devs/cproof.git
synced 2026-07-20 08:46:21 +00:00
fix(review): address PR #94 reviewer comments
Blockers: - SPDX/CProof copyright headers (7 files) - stanza-id non-uniqueness: warn-only, don't skip (matches SQLite) - fopen → open(O_EXCL|O_NOFOLLOW, 0600) + fdopen in export Medium: - log_info → log_warning for SQLite fallback - remove prefs_save() auto-save from backend switch - vtable null → log_warning in 4 dispatch functions - log_debug → log_warning + null error handling in export - rename failure → cons_show_error to user - remove unused total_skipped variable - LMC respects PREF_CORRECTION_ALLOW Low nits: - man page .SS fix + jabber.space URL - profrc.example simplified - show flatfile path on /logging flatfile - EXPORT_PROGRESS_INTERVAL constant (magic 500) - FF_META_PREFIX_ID/AID constants + FF_INDEX_STEP comment - buffer overflow guard on close[1]/close[2] - for-loop → memchr for JID resource scan - BOM check extracted to ff_skip_bom() (DRY) - index building extracted to _ff_maybe_index_line() - O_APPEND+O_EXCL comment rewritten - Makefile.am spaces → tabs
This commit is contained in:
@@ -1,24 +1,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// This file is part of CProof.
|
||||
// See LICENSE for the full GPLv3 text and the special OpenSSL linking exception.
|
||||
|
||||
/*
|
||||
* database_export.c
|
||||
* vim: expandtab:ts=4:sts=4:sw=4
|
||||
*
|
||||
* Copyright (C) 2026 Profanity Contributors
|
||||
*
|
||||
* This file is part of Profanity.
|
||||
*
|
||||
* Profanity is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Profanity is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Profanity. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Cross-backend export/import for message history.
|
||||
* Reads from one backend, writes to the other, with merge + dedup.
|
||||
*/
|
||||
@@ -30,6 +18,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/file.h>
|
||||
@@ -54,6 +43,9 @@
|
||||
|
||||
#ifdef HAVE_SQLITE
|
||||
|
||||
// Print progress to console every N messages during export/import
|
||||
#define EXPORT_PROGRESS_INTERVAL 500
|
||||
|
||||
// =========================================================================
|
||||
// Dedup key: stanza_id if present, else hash of timestamp+from+body
|
||||
// =========================================================================
|
||||
@@ -106,8 +98,10 @@ _ff_list_contacts(void)
|
||||
GDir* dir = g_dir_open(base_dir, 0, &err);
|
||||
if (!dir) {
|
||||
if (err) {
|
||||
log_debug("export: cannot open flatlog dir %s: %s", base_dir, err->message);
|
||||
log_warning("export: cannot open flatlog dir %s: %s", base_dir, err->message);
|
||||
g_error_free(err);
|
||||
} else {
|
||||
log_warning("export: cannot open flatlog dir %s (no error details)", base_dir);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -237,7 +231,6 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
}
|
||||
|
||||
int total_exported = 0;
|
||||
int total_skipped = 0;
|
||||
|
||||
for (GSList* c = contacts; c; c = c->next) {
|
||||
const char* contact = c->data;
|
||||
@@ -277,14 +270,34 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
ff_ensure_dir(dir);
|
||||
|
||||
auto_gchar gchar* tmp_path = g_strdup_printf("%s.export.tmp", log_path);
|
||||
FILE* fp = fopen(tmp_path, "w");
|
||||
if (!fp) {
|
||||
|
||||
// Use open() with O_NOFOLLOW|O_EXCL to prevent symlink attacks,
|
||||
// and explicit 0600 permissions (chat history is private).
|
||||
int tmp_fd = open(tmp_path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, S_IRUSR | S_IWUSR);
|
||||
if (tmp_fd < 0) {
|
||||
if (errno == EEXIST) {
|
||||
// Stale temp file from previous failed export — remove and retry
|
||||
unlink(tmp_path);
|
||||
tmp_fd = open(tmp_path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, S_IRUSR | S_IWUSR);
|
||||
}
|
||||
}
|
||||
if (tmp_fd < 0) {
|
||||
log_error("export: cannot create %s: %s", tmp_path, strerror(errno));
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
continue;
|
||||
}
|
||||
FILE* fp = fdopen(tmp_fd, "w");
|
||||
if (!fp) {
|
||||
log_error("export: fdopen failed for %s: %s", tmp_path, strerror(errno));
|
||||
close(tmp_fd);
|
||||
unlink(tmp_path);
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Lock the temp file
|
||||
int fd = fileno(fp);
|
||||
@@ -372,7 +385,7 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
pl->to_jid, pl->to_resource, pl->marked_read,
|
||||
pl->message);
|
||||
written++;
|
||||
if (written % 500 == 0) {
|
||||
if (written % EXPORT_PROGRESS_INTERVAL == 0) {
|
||||
cons_show(" ... %s: %d/%d written so far", contact, written, (int)g_slist_length(merged));
|
||||
}
|
||||
}
|
||||
@@ -386,14 +399,14 @@ log_database_export_to_flatfile(const gchar* const contact_jid)
|
||||
// Atomic rename
|
||||
if (rename(tmp_path, log_path) != 0) {
|
||||
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));
|
||||
unlink(tmp_path);
|
||||
} else {
|
||||
cons_show("Exported %s: %d new, %d skipped (already existed: %d)",
|
||||
contact, contact_exported, contact_skipped, existing_count);
|
||||
total_exported += contact_exported;
|
||||
}
|
||||
|
||||
cons_show("Exported %s: %d new, %d skipped (already existed: %d)",
|
||||
contact, contact_exported, contact_skipped, existing_count);
|
||||
total_exported += contact_exported;
|
||||
total_skipped += contact_skipped;
|
||||
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free);
|
||||
g_slist_free_full(sqlite_lines, (GDestroyNotify)message_free);
|
||||
@@ -439,7 +452,6 @@ log_database_import_from_flatfile(const gchar* const contact_jid)
|
||||
}
|
||||
|
||||
int total_imported = 0;
|
||||
int total_skipped = 0;
|
||||
|
||||
for (GSList* c = contacts; c; c = c->next) {
|
||||
const char* contact = c->data;
|
||||
@@ -531,7 +543,7 @@ log_database_import_from_flatfile(const gchar* const contact_jid)
|
||||
}
|
||||
contact_imported++;
|
||||
|
||||
if (contact_imported % 500 == 0) {
|
||||
if (contact_imported % EXPORT_PROGRESS_INTERVAL == 0) {
|
||||
cons_show(" ... %s: %d/%d imported so far", contact, contact_imported, total_lines);
|
||||
}
|
||||
}
|
||||
@@ -546,7 +558,6 @@ log_database_import_from_flatfile(const gchar* const contact_jid)
|
||||
cons_show("Imported %s: %d new, %d skipped (already in SQLite)",
|
||||
contact, contact_imported, contact_skipped);
|
||||
total_imported += contact_imported;
|
||||
total_skipped += contact_skipped;
|
||||
|
||||
g_hash_table_destroy(seen_keys);
|
||||
g_slist_free_full(ff_lines, (GDestroyNotify)ff_parsed_line_free);
|
||||
|
||||
Reference in New Issue
Block a user