From 29954a368347653ad24fab6c14246d3f9a48891f Mon Sep 17 00:00:00 2001 From: cottongin Date: Sat, 21 Feb 2026 02:52:38 -0500 Subject: [PATCH] feat: add BookManager utility and RecentBooksStore::clear() BookManager provides static functions for archive/unarchive/delete/ deleteCache/reindex operations on books, centralizing cache path computation and file operations. Archive preserves directory structure under /.archive/ and renames cache dirs to match new path hashes. RecentBooksStore: :clear() added for bulk cache clearing use case. Co-authored-by: Cursor --- src/RecentBooksStore.cpp | 5 + src/RecentBooksStore.h | 3 + src/util/BookManager.cpp | 225 +++++++++++++++++++++++++++++++++++++++ src/util/BookManager.h | 34 ++++++ 4 files changed, 267 insertions(+) create mode 100644 src/util/BookManager.cpp create mode 100644 src/util/BookManager.h diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 9d7adf3e..b8428873 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -47,6 +47,11 @@ void RecentBooksStore::removeBook(const std::string& path) { } } +void RecentBooksStore::clear() { + recentBooks.clear(); + saveToFile(); +} + void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author, const std::string& coverBmpPath) { auto it = diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index bea11d44..c2240af7 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -33,6 +33,9 @@ class RecentBooksStore { // Remove a book from the recent list by path void removeBook(const std::string& path); + // Clear all recent books + void clear(); + // Get the list of recent books (most recent first) const std::vector& getBooks() const { return recentBooks; } diff --git a/src/util/BookManager.cpp b/src/util/BookManager.cpp new file mode 100644 index 00000000..3edb9fcc --- /dev/null +++ b/src/util/BookManager.cpp @@ -0,0 +1,225 @@ +#include "BookManager.h" + +#include +#include + +#include + +#include "RecentBooksStore.h" +#include "StringUtils.h" + +namespace { + +constexpr char ARCHIVE_ROOT[] = "/.archive"; +constexpr char CACHE_ROOT[] = "/.crosspoint"; + +std::string getCachePrefix(const std::string& bookPath) { + if (StringUtils::checkFileExtension(bookPath, ".epub")) return "epub_"; + if (StringUtils::checkFileExtension(bookPath, ".xtc") || StringUtils::checkFileExtension(bookPath, ".xtch")) + return "xtc_"; + if (StringUtils::checkFileExtension(bookPath, ".txt") || StringUtils::checkFileExtension(bookPath, ".md")) + return "txt_"; + return "epub_"; +} + +std::string computeCachePath(const std::string& bookPath) { + const auto prefix = getCachePrefix(bookPath); + return std::string(CACHE_ROOT) + "/" + prefix + std::to_string(std::hash{}(bookPath)); +} + +// Ensure all parent directories of a path exist. +void ensureParentDirs(const std::string& path) { + for (size_t i = 1; i < path.length(); i++) { + if (path[i] == '/') { + Storage.mkdir(path.substr(0, i).c_str()); + } + } +} + +// Delete cover and thumbnail BMP files from a cache directory. +void deleteCoverFiles(const std::string& cachePath) { + auto dir = Storage.open(cachePath.c_str()); + if (!dir || !dir.isDirectory()) { + if (dir) dir.close(); + return; + } + dir.rewindDirectory(); + + char name[256]; + for (auto file = dir.openNextFile(); file; file = dir.openNextFile()) { + file.getName(name, sizeof(name)); + const std::string fname(name); + file.close(); + + const bool isCover = (fname == "cover.bmp" || fname == "cover_crop.bmp"); + const bool isThumb = (fname.rfind("thumb_", 0) == 0 && StringUtils::checkFileExtension(fname, ".bmp")); + + if (isCover || isThumb) { + const std::string fullPath = cachePath + "/" + fname; + Storage.remove(fullPath.c_str()); + } + } + dir.close(); +} + +} // namespace + +namespace BookManager { + +std::string getBookCachePath(const std::string& bookPath) { return computeCachePath(bookPath); } + +bool isArchived(const std::string& bookPath) { + return bookPath.rfind(std::string(ARCHIVE_ROOT) + "/", 0) == 0; +} + +bool archiveBook(const std::string& bookPath) { + if (isArchived(bookPath)) { + LOG_ERR("BKMGR", "Book is already archived: %s", bookPath.c_str()); + return false; + } + + const std::string destPath = std::string(ARCHIVE_ROOT) + bookPath; + ensureParentDirs(destPath); + + if (!Storage.rename(bookPath.c_str(), destPath.c_str())) { + LOG_ERR("BKMGR", "Failed to move book to archive: %s -> %s", bookPath.c_str(), destPath.c_str()); + return false; + } + + // Rename cache directory to match the new book path hash + const std::string oldCache = computeCachePath(bookPath); + const std::string newCache = computeCachePath(destPath); + if (oldCache != newCache && Storage.exists(oldCache.c_str())) { + if (!Storage.rename(oldCache.c_str(), newCache.c_str())) { + LOG_ERR("BKMGR", "Failed to rename cache dir: %s -> %s", oldCache.c_str(), newCache.c_str()); + } + } + + RECENT_BOOKS.removeBook(bookPath); + LOG_DBG("BKMGR", "Archived: %s -> %s", bookPath.c_str(), destPath.c_str()); + return true; +} + +bool unarchiveBook(const std::string& archivePath) { + if (!isArchived(archivePath)) { + LOG_ERR("BKMGR", "Book is not in archive: %s", archivePath.c_str()); + return false; + } + + // Strip "/.archive" prefix to get original path + std::string destPath = archivePath.substr(strlen(ARCHIVE_ROOT)); + if (destPath.empty() || destPath[0] != '/') { + destPath = "/" + destPath; + } + + // Check if original parent directory exists, fall back to root + const auto lastSlash = destPath.find_last_of('/'); + std::string parentDir = (lastSlash != std::string::npos && lastSlash > 0) ? destPath.substr(0, lastSlash) : "/"; + if (!Storage.exists(parentDir.c_str())) { + const auto filename = destPath.substr(lastSlash + 1); + destPath = "/" + filename; + LOG_DBG("BKMGR", "Original dir gone, unarchiving to root: %s", destPath.c_str()); + } + + ensureParentDirs(destPath); + + if (!Storage.rename(archivePath.c_str(), destPath.c_str())) { + LOG_ERR("BKMGR", "Failed to move book from archive: %s -> %s", archivePath.c_str(), destPath.c_str()); + return false; + } + + // Rename cache directory + const std::string oldCache = computeCachePath(archivePath); + const std::string newCache = computeCachePath(destPath); + if (oldCache != newCache && Storage.exists(oldCache.c_str())) { + if (!Storage.rename(oldCache.c_str(), newCache.c_str())) { + LOG_ERR("BKMGR", "Failed to rename cache dir: %s -> %s", oldCache.c_str(), newCache.c_str()); + } + } + + RECENT_BOOKS.removeBook(archivePath); + LOG_DBG("BKMGR", "Unarchived: %s -> %s", archivePath.c_str(), destPath.c_str()); + return true; +} + +bool deleteBook(const std::string& bookPath) { + // Delete the book file + if (Storage.exists(bookPath.c_str())) { + if (!Storage.remove(bookPath.c_str())) { + LOG_ERR("BKMGR", "Failed to delete book file: %s", bookPath.c_str()); + return false; + } + } + + // Delete cache directory + const std::string cachePath = computeCachePath(bookPath); + if (Storage.exists(cachePath.c_str())) { + Storage.removeDir(cachePath.c_str()); + } + + RECENT_BOOKS.removeBook(bookPath); + LOG_DBG("BKMGR", "Deleted book: %s", bookPath.c_str()); + return true; +} + +bool deleteBookCache(const std::string& bookPath) { + const std::string cachePath = computeCachePath(bookPath); + if (Storage.exists(cachePath.c_str())) { + if (!Storage.removeDir(cachePath.c_str())) { + LOG_ERR("BKMGR", "Failed to delete cache: %s", cachePath.c_str()); + return false; + } + } + + RECENT_BOOKS.removeBook(bookPath); + LOG_DBG("BKMGR", "Deleted cache for: %s", bookPath.c_str()); + return true; +} + +bool reindexBook(const std::string& bookPath, bool alsoRegenerateCovers) { + const std::string cachePath = computeCachePath(bookPath); + if (!Storage.exists(cachePath.c_str())) { + LOG_DBG("BKMGR", "No cache to reindex for: %s", bookPath.c_str()); + RECENT_BOOKS.removeBook(bookPath); + return true; + } + + const auto prefix = getCachePrefix(bookPath); + + if (prefix == "epub_") { + // Delete sections directory + const std::string sectionsPath = cachePath + "/sections"; + if (Storage.exists(sectionsPath.c_str())) { + Storage.removeDir(sectionsPath.c_str()); + } + // Delete book.bin (spine/TOC metadata) + const std::string bookBin = cachePath + "/book.bin"; + if (Storage.exists(bookBin.c_str())) { + Storage.remove(bookBin.c_str()); + } + // Delete CSS cache + const std::string cssCache = cachePath + "/css_rules.cache"; + if (Storage.exists(cssCache.c_str())) { + Storage.remove(cssCache.c_str()); + } + } else if (prefix == "txt_") { + // Delete page index + const std::string indexBin = cachePath + "/index.bin"; + if (Storage.exists(indexBin.c_str())) { + Storage.remove(indexBin.c_str()); + } + } else if (prefix == "xtc_") { + // XTC is pre-indexed; only covers/thumbs are cached + // Nothing to delete for sections + } + + if (alsoRegenerateCovers) { + deleteCoverFiles(cachePath); + } + + RECENT_BOOKS.removeBook(bookPath); + LOG_DBG("BKMGR", "Reindexed (covers=%d): %s", alsoRegenerateCovers, bookPath.c_str()); + return true; +} + +} // namespace BookManager diff --git a/src/util/BookManager.h b/src/util/BookManager.h new file mode 100644 index 00000000..47057232 --- /dev/null +++ b/src/util/BookManager.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace BookManager { + +// Compute the cache directory path for a book (e.g. "/.crosspoint/epub_12345") +std::string getBookCachePath(const std::string& bookPath); + +// Move a book to /.archive/ preserving directory structure. +// Renames the cache dir to match the new path hash. Removes from recents. +// Returns true on success. +bool archiveBook(const std::string& bookPath); + +// Move a book from /.archive/ back to its original location. +// Falls back to "/" if the original directory no longer exists. +// Renames the cache dir to match the restored path hash. Returns true on success. +bool unarchiveBook(const std::string& archivePath); + +// Delete a book file, its cache directory, and remove from recents. +bool deleteBook(const std::string& bookPath); + +// Delete only the cache directory for a book and remove from recents. +bool deleteBookCache(const std::string& bookPath); + +// Clear indexed data from cache, preserving progress. +// If alsoRegenerateCovers is true, also deletes cover/thumbnail BMPs. +// Removes from recents. +bool reindexBook(const std::string& bookPath, bool alsoRegenerateCovers); + +// Returns true if the book path is inside the /.archive/ folder. +bool isArchived(const std::string& bookPath); + +} // namespace BookManager