|
|
@@ -0,0 +1,146 @@
|
|
|
+#include "DatabaseManager.hpp"
|
|
|
+#include <stdexcept>
|
|
|
+#include <chrono>
|
|
|
+#include <cstring> // std::strlen
|
|
|
+#include <sys/stat.h>
|
|
|
+
|
|
|
+namespace {
|
|
|
+ inline std::int64_t file_size(const std::string& p) {
|
|
|
+ struct stat st{};
|
|
|
+ return ::stat(p.c_str(), &st) == 0 ? static_cast<std::int64_t>(st.st_size) : 0;
|
|
|
+ }
|
|
|
+ inline std::int64_t file_mtime(const std::string& p) {
|
|
|
+ struct stat st{};
|
|
|
+ return ::stat(p.c_str(), &st) == 0 ? static_cast<std::int64_t>(st.st_mtim.tv_sec) : 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+DatabaseManager::DatabaseManager(std::string db_path)
|
|
|
+ : db_path_(std::move(db_path)) {
|
|
|
+ if (sqlite3_open(db_path_.c_str(), &db_) != SQLITE_OK) {
|
|
|
+ throw std::runtime_error(std::string("sqlite open failed: ") + sqlite3_errmsg(db_));
|
|
|
+ }
|
|
|
+ sqlite3_busy_handler(db_, &DatabaseManager::busy_handler, this);
|
|
|
+ ensure_schema();
|
|
|
+}
|
|
|
+
|
|
|
+DatabaseManager::~DatabaseManager() {
|
|
|
+ // if (stmt_upsert_) sqlite3_finalize(stmt_upsert_);
|
|
|
+ // …
|
|
|
+ if (db_) sqlite3_close(db_);
|
|
|
+}
|
|
|
+
|
|
|
+int DatabaseManager::busy_handler(void*, int prev_ms) {
|
|
|
+ // simple backoff up to ~1s total
|
|
|
+ int sleep_ms = std::min(prev_ms + 50, 200);
|
|
|
+ sqlite3_sleep(sleep_ms);
|
|
|
+ return 1; // retry
|
|
|
+}
|
|
|
+
|
|
|
+void DatabaseManager::ensure_schema() {
|
|
|
+ std::lock_guard lk(mtx_);
|
|
|
+ static const char* ddl =
|
|
|
+ "BEGIN;"
|
|
|
+ "CREATE TABLE IF NOT EXISTS books ("
|
|
|
+ " id TEXT PRIMARY KEY,"
|
|
|
+ " title TEXT NOT NULL,"
|
|
|
+ " author TEXT NOT NULL DEFAULT '',"
|
|
|
+ " file_path TEXT NOT NULL,"
|
|
|
+ " cover_path TEXT NOT NULL DEFAULT '',"
|
|
|
+ " size_bytes INTEGER NOT NULL DEFAULT 0,"
|
|
|
+ " mtime_unix INTEGER NOT NULL DEFAULT 0,"
|
|
|
+ " added_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))"
|
|
|
+ ");"
|
|
|
+ "CREATE INDEX IF NOT EXISTS idx_books_title ON books(title);"
|
|
|
+ "COMMIT;";
|
|
|
+ char* err = nullptr;
|
|
|
+ if (sqlite3_exec(db_, ddl, nullptr, nullptr, &err) != SQLITE_OK) {
|
|
|
+ std::string msg = err ? err : "unknown sqlite error";
|
|
|
+ sqlite3_free(err);
|
|
|
+ throw std::runtime_error("schema failed: " + msg);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+bool DatabaseManager::upsert_book(const Book& b) {
|
|
|
+ std::lock_guard lk(mtx_);
|
|
|
+ static const char* sql =
|
|
|
+ "INSERT INTO books(id,title,author,file_path,cover_path,size_bytes,mtime_unix)"
|
|
|
+ " VALUES(?,?,?,?,?,?,?)"
|
|
|
+ " ON CONFLICT(id) DO UPDATE SET"
|
|
|
+ " title=excluded.title,"
|
|
|
+ " author=excluded.author,"
|
|
|
+ " file_path=excluded.file_path,"
|
|
|
+ " cover_path=excluded.cover_path,"
|
|
|
+ " size_bytes=excluded.size_bytes,"
|
|
|
+ " mtime_unix=excluded.mtime_unix;";
|
|
|
+ sqlite3_stmt* st = nullptr;
|
|
|
+ if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) return false;
|
|
|
+
|
|
|
+ sqlite3_bind_text(st, 1, b.id().c_str(), -1, SQLITE_TRANSIENT);
|
|
|
+ sqlite3_bind_text(st, 2, b.title().c_str(), -1, SQLITE_TRANSIENT);
|
|
|
+ sqlite3_bind_text(st, 3, b.author().c_str(), -1, SQLITE_TRANSIENT);
|
|
|
+ sqlite3_bind_text(st, 4, b.filePath().c_str(), -1, SQLITE_TRANSIENT);
|
|
|
+ sqlite3_bind_text(st, 5, b.coverPath().c_str(), -1, SQLITE_TRANSIENT);
|
|
|
+ sqlite3_bind_int64(st, 6, file_size(b.filePath()));
|
|
|
+ sqlite3_bind_int64(st, 7, file_mtime(b.filePath()));
|
|
|
+
|
|
|
+ bool ok = (sqlite3_step(st) == SQLITE_DONE);
|
|
|
+ sqlite3_finalize(st);
|
|
|
+ return ok;
|
|
|
+}
|
|
|
+
|
|
|
+bool DatabaseManager::remove_book(const std::string& id) {
|
|
|
+ std::lock_guard lk(mtx_);
|
|
|
+ static const char* sql = "DELETE FROM books WHERE id = ?;";
|
|
|
+ sqlite3_stmt* st = nullptr;
|
|
|
+ if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) return false;
|
|
|
+ sqlite3_bind_text(st, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
+ bool ok = (sqlite3_step(st) == SQLITE_DONE);
|
|
|
+ sqlite3_finalize(st);
|
|
|
+ return ok;
|
|
|
+}
|
|
|
+
|
|
|
+std::optional<Book> DatabaseManager::get_book(const std::string& id) const {
|
|
|
+ std::lock_guard lk(mtx_);
|
|
|
+ static const char* sql =
|
|
|
+ "SELECT id,title,author,file_path,cover_path FROM books WHERE id = ?;";
|
|
|
+ sqlite3_stmt* st = nullptr;
|
|
|
+ if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) return std::nullopt;
|
|
|
+ sqlite3_bind_text(st, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
+
|
|
|
+ std::optional<Book> out;
|
|
|
+ if (sqlite3_step(st) == SQLITE_ROW) {
|
|
|
+ Book b(
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,0)),
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,1)),
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,2)),
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,3)),
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,4))
|
|
|
+ );
|
|
|
+ out = std::move(b);
|
|
|
+ }
|
|
|
+ sqlite3_finalize(st);
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
+std::vector<Book> DatabaseManager::load_all_books() const {
|
|
|
+ std::lock_guard lk(mtx_);
|
|
|
+ static const char* sql =
|
|
|
+ "SELECT id,title,author,file_path,cover_path FROM books ORDER BY title,author,id;";
|
|
|
+ sqlite3_stmt* st = nullptr;
|
|
|
+ std::vector<Book> rows;
|
|
|
+
|
|
|
+ if (sqlite3_prepare_v2(db_, sql, -1, &st, nullptr) != SQLITE_OK) return rows;
|
|
|
+
|
|
|
+ while (sqlite3_step(st) == SQLITE_ROW) {
|
|
|
+ rows.emplace_back(
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,0)),
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,1)),
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,2)),
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,3)),
|
|
|
+ reinterpret_cast<const char*>(sqlite3_column_text(st,4))
|
|
|
+ );
|
|
|
+ }
|
|
|
+ sqlite3_finalize(st);
|
|
|
+ return rows;
|
|
|
+}
|