#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, std::string* unarchivedPath) { 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()); if (unarchivedPath) *unarchivedPath = destPath; 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; } void cleanupEmptyArchiveDirs(const std::string& bookPath) { if (!isArchived(bookPath)) return; // Walk up from the book's parent directory, removing empty dirs std::string dir = bookPath.substr(0, bookPath.find_last_of('/')); const std::string archiveRoot(ARCHIVE_ROOT); while (dir.length() > archiveRoot.length()) { auto d = Storage.open(dir.c_str()); if (!d || !d.isDirectory()) { if (d) d.close(); break; } auto child = d.openNextFile(); const bool empty = !child; if (child) child.close(); d.close(); if (!empty) break; Storage.rmdir(dir.c_str()); LOG_DBG("BKMGR", "Removed empty archive dir: %s", dir.c_str()); auto slash = dir.find_last_of('/'); if (slash == std::string::npos || slash == 0) break; dir = dir.substr(0, slash); } } } // namespace BookManager