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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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 =
|
||||
|
||||
@@ -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<RecentBook>& getBooks() const { return recentBooks; }
|
||||
|
||||
|
||||
225
src/util/BookManager.cpp
Normal file
225
src/util/BookManager.cpp
Normal file
@@ -0,0 +1,225 @@
|
||||
#include "BookManager.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#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<std::string>{}(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
|
||||
34
src/util/BookManager.h
Normal file
34
src/util/BookManager.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user