A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: invalidate cache on web uploads and opds downloads and add Clear Cache action (#393)

## Summary

When uploading or downloading an updated ebook from SD/WebUI/OPDS with
same the filename the `.crosspoint` cache is not cleared. This can lead
to issues with the Table of Contents and hangs when switching between
chapters.

I encountered this issue in two places:
- When I need to do further ePub cleaning using Calibre after I load an
ePub and find that some of its formatting should be cleaned up. When I
reprocess the same book and want to place it back in the same location I
need a way to invalidate the cache.
- When syncing RSS feed generated epubs. I generate news ePubs with
filenames like `news-outlet.epub` and so every day when I fetch new news
the crosspoint cache needs to be cleared to load that file.

This change offers the following features:
- On web uploads, if the file already exists, the cache for that file is
cleared
- On OPDS downloads, if the file already exists, the cache for that file
is cleared
- There's now an action for `Clear Cache` in the Settings page which can
clear the cache for all books


Addresses
https://github.com/crosspoint-reader/crosspoint-reader/issues/281

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? PARTIALLY

---------

Co-authored-by: Dave Allie <dave@daveallie.com>

authored by

Logan Garbarini
Dave Allie
and committed by
GitHub
d399afb5 83899325

+271 -2
+7
src/activities/browser/OpdsBookBrowserActivity.cpp
··· 1 1 #include "OpdsBookBrowserActivity.h" 2 2 3 + #include <Epub.h> 3 4 #include <GfxRenderer.h> 4 5 #include <HardwareSerial.h> 5 6 #include <WiFi.h> ··· 355 356 356 357 if (result == HttpDownloader::OK) { 357 358 Serial.printf("[%lu] [OPDS] Download complete: %s\n", millis(), filename.c_str()); 359 + 360 + // Invalidate any existing cache for this file to prevent stale metadata issues 361 + Epub epub(filename, "/.crosspoint"); 362 + epub.clearCache(); 363 + Serial.printf("[%lu] [OPDS] Cleared cache for: %s\n", millis(), filename.c_str()); 364 + 358 365 state = BrowserState::BROWSING; 359 366 updateRequired = true; 360 367 } else {
+9
src/activities/settings/CategorySettingsActivity.cpp
··· 6 6 #include <cstring> 7 7 8 8 #include "CalibreSettingsActivity.h" 9 + #include "ClearCacheActivity.h" 9 10 #include "CrossPointSettings.h" 10 11 #include "KOReaderSettingsActivity.h" 11 12 #include "MappedInputManager.h" ··· 106 107 xSemaphoreTake(renderingMutex, portMAX_DELAY); 107 108 exitActivity(); 108 109 enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { 110 + exitActivity(); 111 + updateRequired = true; 112 + })); 113 + xSemaphoreGive(renderingMutex); 114 + } else if (strcmp(setting.name, "Clear Cache") == 0) { 115 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 116 + exitActivity(); 117 + enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { 109 118 exitActivity(); 110 119 updateRequired = true; 111 120 }));
+178
src/activities/settings/ClearCacheActivity.cpp
··· 1 + #include "ClearCacheActivity.h" 2 + 3 + #include <GfxRenderer.h> 4 + #include <HardwareSerial.h> 5 + #include <SDCardManager.h> 6 + 7 + #include "MappedInputManager.h" 8 + #include "fontIds.h" 9 + 10 + void ClearCacheActivity::taskTrampoline(void* param) { 11 + auto* self = static_cast<ClearCacheActivity*>(param); 12 + self->displayTaskLoop(); 13 + } 14 + 15 + void ClearCacheActivity::onEnter() { 16 + ActivityWithSubactivity::onEnter(); 17 + 18 + renderingMutex = xSemaphoreCreateMutex(); 19 + state = WARNING; 20 + updateRequired = true; 21 + 22 + xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask", 23 + 4096, // Stack size 24 + this, // Parameters 25 + 1, // Priority 26 + &displayTaskHandle // Task handle 27 + ); 28 + } 29 + 30 + void ClearCacheActivity::onExit() { 31 + ActivityWithSubactivity::onExit(); 32 + 33 + // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 34 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 35 + if (displayTaskHandle) { 36 + vTaskDelete(displayTaskHandle); 37 + displayTaskHandle = nullptr; 38 + } 39 + vSemaphoreDelete(renderingMutex); 40 + renderingMutex = nullptr; 41 + } 42 + 43 + void ClearCacheActivity::displayTaskLoop() { 44 + while (true) { 45 + if (updateRequired) { 46 + updateRequired = false; 47 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 48 + render(); 49 + xSemaphoreGive(renderingMutex); 50 + } 51 + vTaskDelay(10 / portTICK_PERIOD_MS); 52 + } 53 + } 54 + 55 + void ClearCacheActivity::render() { 56 + const auto pageHeight = renderer.getScreenHeight(); 57 + 58 + renderer.clearScreen(); 59 + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Clear Cache", true, EpdFontFamily::BOLD); 60 + 61 + if (state == WARNING) { 62 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true); 63 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 30, "All reading progress will be lost!", true, 64 + EpdFontFamily::BOLD); 65 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Books will need to be re-indexed", true); 66 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 30, "when opened again.", true); 67 + 68 + const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", ""); 69 + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 70 + renderer.displayBuffer(); 71 + return; 72 + } 73 + 74 + if (state == CLEARING) { 75 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Clearing cache...", true, EpdFontFamily::BOLD); 76 + renderer.displayBuffer(); 77 + return; 78 + } 79 + 80 + if (state == SUCCESS) { 81 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Cache Cleared", true, EpdFontFamily::BOLD); 82 + String resultText = String(clearedCount) + " items removed"; 83 + if (failedCount > 0) { 84 + resultText += ", " + String(failedCount) + " failed"; 85 + } 86 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, resultText.c_str()); 87 + 88 + const auto labels = mappedInput.mapLabels("« Back", "", "", ""); 89 + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 90 + renderer.displayBuffer(); 91 + return; 92 + } 93 + 94 + if (state == FAILED) { 95 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Failed to clear cache", true, EpdFontFamily::BOLD); 96 + renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Check serial output for details"); 97 + 98 + const auto labels = mappedInput.mapLabels("« Back", "", "", ""); 99 + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 100 + renderer.displayBuffer(); 101 + return; 102 + } 103 + } 104 + 105 + void ClearCacheActivity::clearCache() { 106 + Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis()); 107 + 108 + // Open .crosspoint directory 109 + auto root = SdMan.open("/.crosspoint"); 110 + if (!root || !root.isDirectory()) { 111 + Serial.printf("[%lu] [CLEAR_CACHE] Failed to open cache directory\n", millis()); 112 + if (root) root.close(); 113 + state = FAILED; 114 + updateRequired = true; 115 + return; 116 + } 117 + 118 + clearedCount = 0; 119 + failedCount = 0; 120 + char name[128]; 121 + 122 + // Iterate through all entries in the directory 123 + for (auto file = root.openNextFile(); file; file = root.openNextFile()) { 124 + file.getName(name, sizeof(name)); 125 + String itemName(name); 126 + 127 + // Only delete directories starting with epub_ or xtc_ 128 + if (file.isDirectory() && (itemName.startsWith("epub_") || itemName.startsWith("xtc_"))) { 129 + String fullPath = "/.crosspoint/" + itemName; 130 + Serial.printf("[%lu] [CLEAR_CACHE] Removing cache: %s\n", millis(), fullPath.c_str()); 131 + 132 + file.close(); // Close before attempting to delete 133 + 134 + if (SdMan.removeDir(fullPath.c_str())) { 135 + clearedCount++; 136 + } else { 137 + Serial.printf("[%lu] [CLEAR_CACHE] Failed to remove: %s\n", millis(), fullPath.c_str()); 138 + failedCount++; 139 + } 140 + } else { 141 + file.close(); 142 + } 143 + } 144 + root.close(); 145 + 146 + Serial.printf("[%lu] [CLEAR_CACHE] Cache cleared: %d removed, %d failed\n", millis(), clearedCount, failedCount); 147 + 148 + state = SUCCESS; 149 + updateRequired = true; 150 + } 151 + 152 + void ClearCacheActivity::loop() { 153 + if (state == WARNING) { 154 + if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 155 + Serial.printf("[%lu] [CLEAR_CACHE] User confirmed, starting cache clear\n", millis()); 156 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 157 + state = CLEARING; 158 + xSemaphoreGive(renderingMutex); 159 + updateRequired = true; 160 + vTaskDelay(10 / portTICK_PERIOD_MS); 161 + 162 + clearCache(); 163 + } 164 + 165 + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 166 + Serial.printf("[%lu] [CLEAR_CACHE] User cancelled\n", millis()); 167 + goBack(); 168 + } 169 + return; 170 + } 171 + 172 + if (state == SUCCESS || state == FAILED) { 173 + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 174 + goBack(); 175 + } 176 + return; 177 + } 178 + }
+37
src/activities/settings/ClearCacheActivity.h
··· 1 + #pragma once 2 + 3 + #include <freertos/FreeRTOS.h> 4 + #include <freertos/semphr.h> 5 + #include <freertos/task.h> 6 + 7 + #include <functional> 8 + 9 + #include "activities/ActivityWithSubactivity.h" 10 + 11 + class ClearCacheActivity final : public ActivityWithSubactivity { 12 + public: 13 + explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 14 + const std::function<void()>& goBack) 15 + : ActivityWithSubactivity("ClearCache", renderer, mappedInput), goBack(goBack) {} 16 + 17 + void onEnter() override; 18 + void onExit() override; 19 + void loop() override; 20 + 21 + private: 22 + enum State { WARNING, CLEARING, SUCCESS, FAILED }; 23 + 24 + State state = WARNING; 25 + TaskHandle_t displayTaskHandle = nullptr; 26 + SemaphoreHandle_t renderingMutex = nullptr; 27 + bool updateRequired = false; 28 + const std::function<void()> goBack; 29 + 30 + int clearedCount = 0; 31 + int failedCount = 0; 32 + 33 + static void taskTrampoline(void* param); 34 + [[noreturn]] void displayTaskLoop(); 35 + void render(); 36 + void clearCache(); 37 + };
+2 -2
src/activities/settings/SettingsActivity.cpp
··· 44 44 SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), 45 45 SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; 46 46 47 - constexpr int systemSettingsCount = 4; 47 + constexpr int systemSettingsCount = 5; 48 48 const SettingInfo systemSettings[systemSettingsCount] = { 49 49 SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, 50 50 {"1 min", "5 min", "10 min", "15 min", "30 min"}), 51 - SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), 51 + SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"), 52 52 SettingInfo::Action("Check for updates")}; 53 53 } // namespace 54 54
+23
src/network/CrossPointWebServer.cpp
··· 1 1 #include "CrossPointWebServer.h" 2 2 3 3 #include <ArduinoJson.h> 4 + #include <Epub.h> 4 5 #include <FsHelpers.h> 5 6 #include <SDCardManager.h> 6 7 #include <WiFi.h> ··· 10 11 11 12 #include "html/FilesPageHtml.generated.h" 12 13 #include "html/HomePageHtml.generated.h" 14 + #include "util/StringUtils.h" 13 15 14 16 namespace { 15 17 // Folders/files to hide from the web interface file browser ··· 28 30 size_t wsUploadReceived = 0; 29 31 unsigned long wsUploadStartTime = 0; 30 32 bool wsUploadInProgress = false; 33 + 34 + // Helper function to clear epub cache after upload 35 + void clearEpubCacheIfNeeded(const String& filePath) { 36 + // Only clear cache for .epub files 37 + if (StringUtils::checkFileExtension(filePath, ".epub")) { 38 + Epub(filePath.c_str(), "/.crosspoint").clearCache(); 39 + Serial.printf("[%lu] [WEB] Cleared epub cache for: %s\n", millis(), filePath.c_str()); 40 + } 41 + } 31 42 } // namespace 32 43 33 44 // File listing page template - now using generated headers: ··· 500 511 uploadFileName.c_str(), uploadSize, elapsed, avgKbps); 501 512 Serial.printf("[%lu] [WEB] [UPLOAD] Diagnostics: %d writes, total write time: %lu ms (%.1f%%)\n", millis(), 502 513 writeCount, totalWriteTime, writePercent); 514 + 515 + // Clear epub cache to prevent stale metadata issues when overwriting files 516 + String filePath = uploadPath; 517 + if (!filePath.endsWith("/")) filePath += "/"; 518 + filePath += uploadFileName; 519 + clearEpubCacheIfNeeded(filePath); 503 520 } 504 521 } 505 522 } else if (upload.status == UPLOAD_FILE_ABORTED) { ··· 786 803 787 804 Serial.printf("[%lu] [WS] Upload complete: %s (%d bytes in %lu ms, %.1f KB/s)\n", millis(), 788 805 wsUploadFileName.c_str(), wsUploadSize, elapsed, kbps); 806 + 807 + // Clear epub cache to prevent stale metadata issues when overwriting files 808 + String filePath = wsUploadPath; 809 + if (!filePath.endsWith("/")) filePath += "/"; 810 + filePath += wsUploadFileName; 811 + clearEpubCacheIfNeeded(filePath); 789 812 790 813 wsServer->sendTXT(num, "DONE"); 791 814 lastProgressSent = 0;
+12
src/util/StringUtils.cpp
··· 49 49 return true; 50 50 } 51 51 52 + bool checkFileExtension(const String& fileName, const char* extension) { 53 + if (fileName.length() < strlen(extension)) { 54 + return false; 55 + } 56 + 57 + String localFile(fileName); 58 + String localExtension(extension); 59 + localFile.toLowerCase(); 60 + localExtension.toLowerCase(); 61 + return localFile.endsWith(localExtension); 62 + } 63 + 52 64 size_t utf8RemoveLastChar(std::string& str) { 53 65 if (str.empty()) return 0; 54 66 size_t pos = str.size() - 1;
+3
src/util/StringUtils.h
··· 1 1 #pragma once 2 2 3 + #include <WString.h> 4 + 3 5 #include <string> 4 6 5 7 namespace StringUtils { ··· 15 17 * Check if the given filename ends with the specified extension (case-insensitive). 16 18 */ 17 19 bool checkFileExtension(const std::string& fileName, const char* extension); 20 + bool checkFileExtension(const String& fileName, const char* extension); 18 21 19 22 // UTF-8 safe string truncation - removes one character from the end 20 23 // Returns the new size after removing one UTF-8 character