diff --git a/docs/webserver-api-reference.md b/docs/webserver-api-reference.md new file mode 100644 index 0000000..edd8d60 --- /dev/null +++ b/docs/webserver-api-reference.md @@ -0,0 +1,262 @@ +# CrossPointWebServer API Reference + +Source: `src/network/CrossPointWebServer.cpp` and `CrossPointWebServer.h` + +## Server Configuration + +- HTTP port: 80 (default) +- WebSocket port: 81 (default) +- WiFi sleep disabled for responsiveness +- Supports both STA (station) and AP (access point) modes + +## HTTP Endpoints + +### GET / +**Handler:** `handleRoot()` +**Response:** HTML homepage from `HomePageHtml` (generated from `html/HomePage.html`) +**Content-Type:** text/html + +### GET /files +**Handler:** `handleFileList()` +**Response:** HTML file browser page from `FilesPageHtml` (generated from `html/FilesPage.html`) +**Content-Type:** text/html + +### GET /api/status +**Handler:** `handleStatus()` +**Response:** JSON device status +**Content-Type:** application/json +```json +{ + "version": "CROSSPOINT_VERSION", + "ip": "192.168.x.x", + "mode": "AP" | "STA", + "rssi": -50, // 0 in AP mode + "freeHeap": 123456, + "uptime": 3600 // seconds +} +``` + +### GET /api/files +**Handler:** `handleFileListData()` +**Query params:** +- `path` (optional): Directory path, defaults to "/" +- `showHidden` (optional): "true" to show dot-files (except .crosspoint) +**Response:** JSON array of files +**Content-Type:** application/json +```json +[ + {"name": "book.epub", "size": 123456, "isDirectory": false, "isEpub": true}, + {"name": "folder", "size": 0, "isDirectory": true, "isEpub": false} +] +``` +**Notes:** +- Hidden by default: files starting with ".", "System Volume Information", "XTCache" +- Always hidden: ".crosspoint" (internal cache folder) +- Streamed response (chunked encoding) to reduce memory usage + +### GET /api/archived +**Handler:** `handleArchivedList()` +**Response:** JSON array of archived books +**Content-Type:** application/json +```json +[ + {"filename": "archived_file.epub", "originalPath": "/Books/archived_file.epub"} +] +``` +**Notes:** Uses `BookManager::listArchivedBooks()` and `BookManager::getArchivedBookOriginalPath()` + +### GET /download +**Handler:** `handleDownload()` +**Query params:** +- `path` (required): File path to download +**Response:** File binary with Content-Disposition attachment header +**Content-Type:** application/octet-stream +**Errors:** +- 400: Missing path, path is directory +- 403: Hidden/system file, protected item +- 404: File not found +**Notes:** +- Streams in 4KB chunks +- Updates `totalBytesDownloaded` and `totalFilesDownloaded` stats +- Security: rejects paths with "..", files starting with ".", protected items + +### POST /upload +**Handler:** `handleUpload()` (multipart handler), `handleUploadPost()` (response handler) +**Query params:** +- `path` (optional): Upload directory, defaults to "/" +**Form data:** multipart/form-data with file +**Response:** "File uploaded successfully: filename" or error message +**Notes:** +- Uses 4KB write buffer for SD card efficiency +- Overwrites existing files +- Clears epub cache after upload via `clearEpubCacheIfNeeded()` +- Updates `totalBytesUploaded` and `totalFilesUploaded` stats +- Logs progress every 100KB + +### POST /mkdir +**Handler:** `handleCreateFolder()` +**Form params:** +- `name` (required): Folder name +- `path` (optional): Parent directory, defaults to "/" +**Response:** "Folder created: foldername" or error +**Errors:** +- 400: Missing name, empty name, folder exists + +### POST /delete +**Handler:** `handleDelete()` +**Form params:** +- `path` (required): Item path to delete +- `type` (optional): "file" (default) or "folder" +- `archived` (optional): "true" for archived books +**Response:** "Deleted successfully" or error +**Errors:** +- 400: Missing path, root directory, folder not empty +- 403: Hidden/system file, protected item +- 404: Item not found +- 500: Delete failed +**Notes:** +- For files: uses `BookManager::deleteBook()` which handles cache and recent books cleanup +- For folders: must be empty first +- For archived: passes filename to `BookManager::deleteBook(filename, true)` + +### POST /archive +**Handler:** `handleArchive()` +**Form params:** +- `path` (required): Book path to archive +**Response:** "Book archived successfully" or error +**Notes:** Uses `BookManager::archiveBook()` + +### POST /unarchive +**Handler:** `handleUnarchive()` +**Form params:** +- `filename` (required): Archived book filename +**Response:** JSON with original path +**Content-Type:** application/json +```json +{"success": true, "originalPath": "/Books/book.epub"} +``` +**Notes:** Uses `BookManager::unarchiveBook()` and `BookManager::getArchivedBookOriginalPath()` + +### POST /rename +**Handler:** `handleRename()` +**Form params:** +- `path` (required): Current item path +- `newName` (required): New name (filename only, no path separators) +**Response:** "Renamed successfully" or error +**Errors:** +- 400: Missing params, empty name, name contains "/" or "\\", root directory, destination exists +- 403: System file, protected item +- 404: Source not found +- 500: Rename failed +**Notes:** +- Renames in place (same directory, new name) +- Uses `SdMan.rename()` +- Clears epub cache after rename via `clearEpubCacheIfNeeded()` + +### POST /copy +**Handler:** `handleCopy()` +**Form params:** +- `srcPath` (required): Source path +- `destPath` (required): Full destination path (including new name) +**Response:** "Copied successfully" or error +**Errors:** +- 400: Missing params, root directory, destination exists, copy into self +- 403: System file, protected item +- 404: Source not found +- 500: Copy failed +**Notes:** +- Uses `copyFile()` for files (4KB buffer chunks) +- Uses `copyFolder()` for recursive directory copy +- Skips hidden files in folder copy + +### POST /move +**Handler:** `handleMove()` +**Form params:** +- `srcPath` (required): Source path +- `destPath` (required): Full destination path (including new name) +**Response:** "Moved successfully" or error +**Errors:** Same as copy +**Notes:** +- First attempts atomic `SdMan.rename()` (fast) +- Falls back to copy+delete if rename fails +- Uses `deleteFolderRecursive()` for folder cleanup + +## WebSocket Protocol (port 81) + +**Handler:** `onWebSocketEvent()` via `wsEventCallback()` trampoline + +### Upload Protocol + +1. Client connects +2. Server: (implicit connection acknowledgment) +3. Client TEXT: `START:::` +4. Server TEXT: `READY` or `ERROR:` +5. Client BIN: file data chunks (any size, recommend 64KB) +6. Server TEXT: `PROGRESS::` (every 64KB or at end) +7. Server TEXT: `DONE` or `ERROR:` + +### Events +- `WStype_CONNECTED`: Client connected, logs connection +- `WStype_DISCONNECTED`: Cleanup incomplete upload, delete partial file +- `WStype_TEXT`: Parse control messages (START) +- `WStype_BIN`: Write file data, send progress, complete upload + +### Notes +- Faster than HTTP multipart for large files +- Direct binary writes to SD card +- Clears epub cache after upload +- Updates traffic statistics + +## Security + +### Protected Items (HIDDEN_ITEMS[]) +- "System Volume Information" +- "XTCache" + +### Always Hidden +- ".crosspoint" (internal cache) + +### Security Checks Applied To +- `/delete`: Rejects dot-files (unless archived), protected items +- `/download`: Rejects dot-files, protected items, path traversal (..) +- `/rename`: Rejects dot-files, protected items +- `/copy`: Rejects dot-files, protected items +- `/move`: Rejects dot-files, protected items + +## Helper Functions + +### clearEpubCacheIfNeeded(filePath) +- Location: anonymous namespace at top of file +- Clears epub cache if file ends with ".epub" +- Uses `Epub(filePath, "/.crosspoint").clearCache()` +- Called by: upload, WebSocket upload, rename + +### scanFiles(path, callback, showHidden) +- Iterates directory, calls callback for each FileInfo +- Yields and resets watchdog during iteration +- Filters hidden items based on showHidden flag + +### copyFile(srcPath, destPath) / copyFolder(srcPath, destPath) +- 4KB buffer for file copy +- Recursive for folders +- Returns bool success + +### deleteFolderRecursive(path) +- Static helper for move fallback +- Recursively deletes contents then directory + +## Traffic Statistics (mutable, updated from const handlers) +- `totalBytesUploaded` +- `totalBytesDownloaded` +- `totalFilesUploaded` +- `totalFilesDownloaded` +- `serverStartTime` (for uptime calculation) + +## Dependencies +- `` - ESP32 HTTP server +- `` - WebSocket support +- `` - JSON serialization +- `` - SD card operations (SdMan singleton) +- `` - Epub cache management +- `BookManager.h` - Book deletion, archiving, recent books +- `StringUtils.h` - File extension checking diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 0e5efae..24ecd84 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -77,7 +77,7 @@ void CrossPointWebServerActivity::onEnter() { updateRequired = true; xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask", - 2048, // Stack size + 6144, // Stack size (increased: QR code + string ops need ~4KB) this, // Parameters 1, // Priority &displayTaskHandle // Task handle diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 7d5f337..d6b8f92 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -1143,6 +1143,11 @@ void CrossPointWebServer::handleRename() const { esp_task_wdt_reset(); if (SdMan.rename(itemPath.c_str(), newPath.c_str())) { Serial.printf("[%lu] [WEB] Rename successful\n", millis()); + + // Clear epub cache for both old and new paths to prevent stale metadata + clearEpubCacheIfNeeded(itemPath); // Old path cache is now invalid + clearEpubCacheIfNeeded(newPath); // Ensure clean cache for new path + server->send(200, "text/plain", "Renamed successfully"); } else { Serial.printf("[%lu] [WEB] Rename failed\n", millis());