350 lines
11 KiB
C++
350 lines
11 KiB
C++
|
|
#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);
|
||
|
|
}
|