#include "BookManager.h" #include #include #include #include #include "RecentBooksStore.h" namespace { constexpr const char* LOG_TAG = "BM"; // Supported book extensions const char* SUPPORTED_EXTENSIONS[] = {".epub", ".txt", ".xtc", ".xtch"}; constexpr size_t SUPPORTED_EXTENSIONS_COUNT = sizeof(SUPPORTED_EXTENSIONS) / sizeof(SUPPORTED_EXTENSIONS[0]); } // namespace std::string BookManager::getFilename(const std::string& path) { const size_t lastSlash = path.find_last_of('/'); if (lastSlash == std::string::npos) { return path; } return path.substr(lastSlash + 1); } std::string BookManager::getExtension(const std::string& path) { const size_t lastDot = path.find_last_of('.'); if (lastDot == std::string::npos) { return ""; } std::string ext = path.substr(lastDot); // Convert to lowercase std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower); return ext; } size_t BookManager::computePathHash(const std::string& path) { return std::hash{}(path); } std::string BookManager::getCachePrefix(const std::string& path) { const std::string ext = getExtension(path); if (ext == ".epub") { return "epub_"; } else if (ext == ".txt") { return "txt_"; } else if (ext == ".xtc" || ext == ".xtch") { return "xtc_"; } return ""; } std::string BookManager::getCacheDir(const std::string& bookPath) { const std::string prefix = getCachePrefix(bookPath); if (prefix.empty()) { return ""; } const size_t hash = computePathHash(bookPath); return std::string(CROSSPOINT_DIR) + "/" + prefix + std::to_string(hash); } bool BookManager::writeMetaFile(const std::string& archivedPath, const std::string& originalPath) { const std::string metaPath = archivedPath + ".meta"; FsFile metaFile; if (!SdMan.openFileForWrite(LOG_TAG, metaPath, metaFile)) { Serial.printf("[%lu] [%s] Failed to create meta file: %s\n", millis(), LOG_TAG, metaPath.c_str()); return false; } metaFile.print(originalPath.c_str()); metaFile.close(); return true; } std::string BookManager::readMetaFile(const std::string& archivedPath) { const std::string metaPath = archivedPath + ".meta"; FsFile metaFile; if (!SdMan.openFileForRead(LOG_TAG, metaPath, metaFile)) { Serial.printf("[%lu] [%s] Failed to read meta file: %s\n", millis(), LOG_TAG, metaPath.c_str()); return ""; } std::string originalPath; originalPath.reserve(256); while (metaFile.available()) { char c = metaFile.read(); if (c == '\n' || c == '\r') { break; // Stop at newline } originalPath += c; } metaFile.close(); return originalPath; } bool BookManager::ensureParentDirExists(const std::string& path) { // Find the last slash to get the parent directory const size_t lastSlash = path.find_last_of('/'); if (lastSlash == std::string::npos || lastSlash == 0) { return true; // Root or no parent directory } const std::string parentDir = path.substr(0, lastSlash); if (SdMan.exists(parentDir.c_str())) { return true; } // Create parent directories recursively return SdMan.mkdir(parentDir.c_str()); } bool BookManager::isSupportedBookFormat(const std::string& path) { const std::string ext = getExtension(path); for (size_t i = 0; i < SUPPORTED_EXTENSIONS_COUNT; i++) { if (ext == SUPPORTED_EXTENSIONS[i]) { return true; } } return false; } bool BookManager::archiveBook(const std::string& bookPath) { Serial.printf("[%lu] [%s] Archiving book: %s\n", millis(), LOG_TAG, bookPath.c_str()); // Validate the book exists if (!SdMan.exists(bookPath.c_str())) { Serial.printf("[%lu] [%s] Book not found: %s\n", millis(), LOG_TAG, bookPath.c_str()); return false; } // Create archive directories if needed SdMan.mkdir(ARCHIVE_DIR); SdMan.mkdir(ARCHIVE_CACHE_DIR); const std::string filename = getFilename(bookPath); const std::string archivedBookPath = std::string(ARCHIVE_DIR) + "/" + filename; // Check if already archived (same filename exists) if (SdMan.exists(archivedBookPath.c_str())) { Serial.printf("[%lu] [%s] A book with this name is already archived: %s\n", millis(), LOG_TAG, filename.c_str()); return false; } // Write meta file with original path if (!writeMetaFile(archivedBookPath, bookPath)) { Serial.printf("[%lu] [%s] Failed to write meta file\n", millis(), LOG_TAG); return false; } // Move the book file if (!SdMan.rename(bookPath.c_str(), archivedBookPath.c_str())) { Serial.printf("[%lu] [%s] Failed to move book file\n", millis(), LOG_TAG); // Clean up meta file SdMan.remove((archivedBookPath + ".meta").c_str()); return false; } // Move cache directory if it exists const std::string cacheDir = getCacheDir(bookPath); if (!cacheDir.empty() && SdMan.exists(cacheDir.c_str())) { const std::string prefix = getCachePrefix(bookPath); const size_t hash = computePathHash(bookPath); const std::string archivedCacheDir = std::string(ARCHIVE_CACHE_DIR) + "/" + prefix + std::to_string(hash); if (!SdMan.rename(cacheDir.c_str(), archivedCacheDir.c_str())) { Serial.printf("[%lu] [%s] Warning: Failed to move cache directory (book still archived)\n", millis(), LOG_TAG); // Don't fail the archive operation, the cache can be regenerated } else { Serial.printf("[%lu] [%s] Moved cache to: %s\n", millis(), LOG_TAG, archivedCacheDir.c_str()); } } // Remove from recent books RECENT_BOOKS.removeBook(bookPath); Serial.printf("[%lu] [%s] Successfully archived: %s\n", millis(), LOG_TAG, bookPath.c_str()); return true; } bool BookManager::unarchiveBook(const std::string& archivedFilename) { Serial.printf("[%lu] [%s] Unarchiving book: %s\n", millis(), LOG_TAG, archivedFilename.c_str()); const std::string archivedBookPath = std::string(ARCHIVE_DIR) + "/" + archivedFilename; // Validate archived book exists if (!SdMan.exists(archivedBookPath.c_str())) { Serial.printf("[%lu] [%s] Archived book not found: %s\n", millis(), LOG_TAG, archivedBookPath.c_str()); return false; } // Read original path from meta file const std::string originalPath = readMetaFile(archivedBookPath); if (originalPath.empty()) { Serial.printf("[%lu] [%s] Failed to read original path from meta file\n", millis(), LOG_TAG); return false; } // Check if original location already has a file if (SdMan.exists(originalPath.c_str())) { Serial.printf("[%lu] [%s] A file already exists at original location: %s\n", millis(), LOG_TAG, originalPath.c_str()); return false; } // Ensure parent directory exists if (!ensureParentDirExists(originalPath)) { Serial.printf("[%lu] [%s] Failed to create parent directory for: %s\n", millis(), LOG_TAG, originalPath.c_str()); return false; } // Move the book file back if (!SdMan.rename(archivedBookPath.c_str(), originalPath.c_str())) { Serial.printf("[%lu] [%s] Failed to move book file back\n", millis(), LOG_TAG); return false; } // Move cache directory back if it exists const std::string prefix = getCachePrefix(archivedFilename); if (!prefix.empty()) { const size_t hash = computePathHash(originalPath); const std::string archivedCacheDir = std::string(ARCHIVE_CACHE_DIR) + "/" + prefix + std::to_string(hash); const std::string originalCacheDir = std::string(CROSSPOINT_DIR) + "/" + prefix + std::to_string(hash); if (SdMan.exists(archivedCacheDir.c_str())) { if (!SdMan.rename(archivedCacheDir.c_str(), originalCacheDir.c_str())) { Serial.printf("[%lu] [%s] Warning: Failed to restore cache directory\n", millis(), LOG_TAG); // Don't fail, cache can be regenerated } else { Serial.printf("[%lu] [%s] Restored cache to: %s\n", millis(), LOG_TAG, originalCacheDir.c_str()); } } } // Delete the meta file SdMan.remove((archivedBookPath + ".meta").c_str()); Serial.printf("[%lu] [%s] Successfully unarchived to: %s\n", millis(), LOG_TAG, originalPath.c_str()); return true; } bool BookManager::deleteBook(const std::string& bookPath, bool isArchived) { Serial.printf("[%lu] [%s] Deleting book (archived=%d): %s\n", millis(), LOG_TAG, isArchived, bookPath.c_str()); std::string actualPath; std::string originalPath; if (isArchived) { // bookPath is just the filename in /.archive/ actualPath = std::string(ARCHIVE_DIR) + "/" + bookPath; // Read original path to compute correct cache hash originalPath = readMetaFile(actualPath); } else { actualPath = bookPath; originalPath = bookPath; } // Validate the book exists if (!SdMan.exists(actualPath.c_str())) { Serial.printf("[%lu] [%s] Book not found: %s\n", millis(), LOG_TAG, actualPath.c_str()); return false; } // Delete the book file if (!SdMan.remove(actualPath.c_str())) { Serial.printf("[%lu] [%s] Failed to delete book file\n", millis(), LOG_TAG); return false; } // Delete cache directory // Use originalPath for hash calculation (for archived books, this is the path before archiving) const std::string pathForCache = originalPath.empty() ? actualPath : originalPath; std::string cacheDir; if (isArchived && !originalPath.empty()) { // For archived books, cache is in /.archive/.cache/ const std::string prefix = getCachePrefix(pathForCache); if (!prefix.empty()) { const size_t hash = computePathHash(originalPath); cacheDir = std::string(ARCHIVE_CACHE_DIR) + "/" + prefix + std::to_string(hash); } } else { // For regular books, cache is in /.crosspoint/ cacheDir = getCacheDir(pathForCache); } if (!cacheDir.empty() && SdMan.exists(cacheDir.c_str())) { if (SdMan.removeDir(cacheDir.c_str())) { Serial.printf("[%lu] [%s] Deleted cache directory: %s\n", millis(), LOG_TAG, cacheDir.c_str()); } else { Serial.printf("[%lu] [%s] Warning: Failed to delete cache directory\n", millis(), LOG_TAG); } } // Delete meta file if archived if (isArchived) { SdMan.remove((actualPath + ".meta").c_str()); } // Remove from recent books (use original path) if (!originalPath.empty()) { RECENT_BOOKS.removeBook(originalPath); } else { RECENT_BOOKS.removeBook(bookPath); } Serial.printf("[%lu] [%s] Successfully deleted book\n", millis(), LOG_TAG); return true; } std::vector BookManager::listArchivedBooks() { std::vector archivedBooks; FsFile archiveDir = SdMan.open(ARCHIVE_DIR); if (!archiveDir || !archiveDir.isDirectory()) { if (archiveDir) { archiveDir.close(); } return archivedBooks; } char name[128]; FsFile entry; while ((entry = archiveDir.openNextFile())) { if (!entry.isDirectory()) { entry.getName(name, sizeof(name)); const std::string filename(name); // Skip .meta files and hidden files if (filename[0] != '.' && filename.find(".meta") == std::string::npos) { // Only include supported book formats if (isSupportedBookFormat(filename)) { archivedBooks.push_back(filename); } } } entry.close(); } archiveDir.close(); // Sort alphabetically std::sort(archivedBooks.begin(), archivedBooks.end()); return archivedBooks; } std::string BookManager::getArchivedBookOriginalPath(const std::string& archivedFilename) { const std::string archivedBookPath = std::string(ARCHIVE_DIR) + "/" + archivedFilename; return readMetaFile(archivedBookPath); }