crosspoint-reader/src/BookManager.cpp

350 lines
11 KiB
C++
Raw Normal View History

2026-01-22 15:45:07 -05:00
#include "BookManager.h"
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <algorithm>
#include <functional>
#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<std::string>{}(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<std::string> BookManager::listArchivedBooks() {
std::vector<std::string> 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);
}