adds delete and archive abilities
This commit is contained in:
349
src/BookManager.cpp
Normal file
349
src/BookManager.cpp
Normal file
@@ -0,0 +1,349 @@
|
||||
#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);
|
||||
}
|
||||
Reference in New Issue
Block a user