mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 06:37:38 +03:00
Compare commits
7 Commits
2b3f2d9b7c
...
f794ce4242
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f794ce4242 | ||
|
|
78d6e5931c | ||
|
|
dac11c3fdd | ||
|
|
3a9b8c9b22 | ||
|
|
4f18ec280c | ||
|
|
254f2b4f79 | ||
|
|
5f1c6144a1 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -9,3 +9,8 @@ build
|
||||
**/__pycache__/
|
||||
/compile_commands.json
|
||||
/.cache
|
||||
.history/
|
||||
node_modules/
|
||||
package.json
|
||||
package-lock.json
|
||||
mise.toml
|
||||
@ -102,13 +102,18 @@ After flashing the new features, it’s recommended to capture detailed logs fro
|
||||
First, make sure all required Python packages are installed:
|
||||
|
||||
```python
|
||||
python3 -m pip install serial colorama matplotlib
|
||||
python3 -m pip install pyserial colorama matplotlib
|
||||
```
|
||||
after that run the script:
|
||||
```sh
|
||||
# For Linux
|
||||
# This was tested on Debian and should work on most Linux systems.
|
||||
python3 scripts/debugging_monitor.py
|
||||
|
||||
# For macOS
|
||||
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
|
||||
```
|
||||
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
|
||||
Minor adjustments may be required for Windows.
|
||||
|
||||
## Internals
|
||||
|
||||
|
||||
@ -520,7 +520,7 @@ void WifiSelectionActivity::renderNetworkList() const {
|
||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
const auto top = (pageHeight - height) / 2;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again");
|
||||
} else {
|
||||
// Calculate how many networks we can display
|
||||
constexpr int startY = 60;
|
||||
|
||||
@ -706,84 +706,108 @@ void CrossPointWebServer::handleCreateFolder() const {
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleDelete() const {
|
||||
// Get path from form data
|
||||
if (!server->hasArg("path")) {
|
||||
server->send(400, "text/plain", "Missing path");
|
||||
// Check if 'paths' argument is provided
|
||||
if (!server->hasArg("paths")) {
|
||||
server->send(400, "text/plain", "Missing paths");
|
||||
return;
|
||||
}
|
||||
|
||||
String itemPath = server->arg("path");
|
||||
const String itemType = server->hasArg("type") ? server->arg("type") : "file";
|
||||
|
||||
// Validate path
|
||||
if (itemPath.isEmpty() || itemPath == "/") {
|
||||
server->send(400, "text/plain", "Cannot delete root directory");
|
||||
// Parse paths
|
||||
String pathsArg = server->arg("paths");
|
||||
DynamicJsonDocument doc(2048);
|
||||
DeserializationError error = deserializeJson(doc, pathsArg);
|
||||
if (error) {
|
||||
server->send(400, "text/plain", "Invalid paths format");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure path starts with /
|
||||
if (!itemPath.startsWith("/")) {
|
||||
itemPath = "/" + itemPath;
|
||||
}
|
||||
|
||||
// Security check: prevent deletion of protected items
|
||||
const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
|
||||
|
||||
// Check if item starts with a dot (hidden/system file)
|
||||
if (itemName.startsWith(".")) {
|
||||
Serial.printf("[%lu] [WEB] Delete rejected - hidden/system item: %s\n", millis(), itemPath.c_str());
|
||||
server->send(403, "text/plain", "Cannot delete system files");
|
||||
JsonArray paths = doc.as<JsonArray>();
|
||||
if (paths.isNull() || paths.size() == 0) {
|
||||
server->send(400, "text/plain", "No paths provided");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check against explicitly protected items
|
||||
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
|
||||
if (itemName.equals(HIDDEN_ITEMS[i])) {
|
||||
Serial.printf("[%lu] [WEB] Delete rejected - protected item: %s\n", millis(), itemPath.c_str());
|
||||
server->send(403, "text/plain", "Cannot delete protected items");
|
||||
return;
|
||||
// Iterate over paths and delete each item
|
||||
bool allSuccess = true;
|
||||
String failedItems;
|
||||
|
||||
for (const auto& p : paths) {
|
||||
String itemPath = p.as<String>();
|
||||
|
||||
// Validate path
|
||||
if (itemPath.isEmpty() || itemPath == "/") {
|
||||
failedItems += itemPath + " (cannot delete root); ";
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if item exists
|
||||
if (!SdMan.exists(itemPath.c_str())) {
|
||||
Serial.printf("[%lu] [WEB] Delete failed - item not found: %s\n", millis(), itemPath.c_str());
|
||||
server->send(404, "text/plain", "Item not found");
|
||||
return;
|
||||
}
|
||||
// Ensure path starts with /
|
||||
if (!itemPath.startsWith("/")) {
|
||||
itemPath = "/" + itemPath;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [WEB] Attempting to delete %s: %s\n", millis(), itemType.c_str(), itemPath.c_str());
|
||||
// Security check: prevent deletion of protected items
|
||||
const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
|
||||
|
||||
bool success = false;
|
||||
// Hidden/system files are protected
|
||||
if (itemName.startsWith(".")) {
|
||||
failedItems += itemPath + " (hidden/system file); ";
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (itemType == "folder") {
|
||||
// For folders, try to remove (will fail if not empty)
|
||||
FsFile dir = SdMan.open(itemPath.c_str());
|
||||
if (dir && dir.isDirectory()) {
|
||||
// Check if folder is empty
|
||||
FsFile entry = dir.openNextFile();
|
||||
if (entry) {
|
||||
// Folder is not empty
|
||||
entry.close();
|
||||
dir.close();
|
||||
Serial.printf("[%lu] [WEB] Delete failed - folder not empty: %s\n", millis(), itemPath.c_str());
|
||||
server->send(400, "text/plain", "Folder is not empty. Delete contents first.");
|
||||
return;
|
||||
// Check against explicitly protected items
|
||||
bool isProtected = false;
|
||||
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
|
||||
if (itemName.equals(HIDDEN_ITEMS[i])) {
|
||||
isProtected = true;
|
||||
break;
|
||||
}
|
||||
dir.close();
|
||||
}
|
||||
success = SdMan.rmdir(itemPath.c_str());
|
||||
} else {
|
||||
// For files, use remove
|
||||
success = SdMan.remove(itemPath.c_str());
|
||||
if (isProtected) {
|
||||
failedItems += itemPath + " (protected file); ";
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if item exists
|
||||
if (!SdMan.exists(itemPath.c_str())) {
|
||||
failedItems += itemPath + " (not found); ";
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decide whether it's a directory or file by opening it
|
||||
bool success = false;
|
||||
FsFile f = SdMan.open(itemPath.c_str());
|
||||
if (f && f.isDirectory()) {
|
||||
// For folders, ensure empty before removing
|
||||
FsFile entry = f.openNextFile();
|
||||
if (entry) {
|
||||
entry.close();
|
||||
f.close();
|
||||
failedItems += itemPath + " (folder not empty); ";
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
f.close();
|
||||
success = SdMan.rmdir(itemPath.c_str());
|
||||
} else {
|
||||
// It's a file (or couldn't open as dir) — remove file
|
||||
if (f) f.close();
|
||||
success = SdMan.remove(itemPath.c_str());
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
failedItems += itemPath + " (deletion failed); ";
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
Serial.printf("[%lu] [WEB] Successfully deleted: %s\n", millis(), itemPath.c_str());
|
||||
server->send(200, "text/plain", "Deleted successfully");
|
||||
if (allSuccess) {
|
||||
server->send(200, "text/plain", "All items deleted successfully");
|
||||
} else {
|
||||
Serial.printf("[%lu] [WEB] Failed to delete: %s\n", millis(), itemPath.c_str());
|
||||
server->send(500, "text/plain", "Failed to delete item");
|
||||
server->send(500, "text/plain", "Failed to delete some items: " + failedItems);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -586,6 +586,7 @@
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn upload-action-btn" onclick="openUploadModal()">📤 Upload</button>
|
||||
<button class="action-btn folder-action-btn" onclick="openFolderModal()">📁 New Folder</button>
|
||||
<button class="action-btn" style="background-color:#e74c3c" onclick="openDeleteSelectedModal()">🗑️ Delete Selected</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -652,13 +653,11 @@
|
||||
<div class="modal-overlay" id="deleteModal">
|
||||
<div class="modal">
|
||||
<button class="modal-close" onclick="closeDeleteModal()">×</button>
|
||||
<h3>🗑️ Delete Item</h3>
|
||||
<h3>🗑️ Delete Item(s)</h3>
|
||||
<div class="folder-form">
|
||||
<p class="delete-warning">⚠️ This action cannot be undone!</p>
|
||||
<p class="file-info">Are you sure you want to delete:</p>
|
||||
<p class="delete-item-name" id="deleteItemName"></p>
|
||||
<input type="hidden" id="deleteItemPath">
|
||||
<input type="hidden" id="deleteItemType">
|
||||
<p class="file-info">Are you sure you want to delete the following item(s)?</p>
|
||||
<div id="deleteItemList" style="max-height:240px; overflow:auto; margin-bottom:10px;"></div>
|
||||
<button class="delete-btn-confirm" onclick="confirmDelete()">Delete</button>
|
||||
<button class="delete-btn-cancel" onclick="closeDeleteModal()">Cancel</button>
|
||||
</div>
|
||||
@ -739,7 +738,10 @@
|
||||
fileTable.innerHTML = '<div class="no-files">This folder is empty</div>';
|
||||
} else {
|
||||
let fileTableContent = '<table class="file-table">';
|
||||
fileTableContent += '<tr><th>Name</th><th>Type</th><th>Size</th><th class="actions-col">Actions</th></tr>';
|
||||
|
||||
// Add select-all checkbox column
|
||||
fileTableContent += '<tr><th style="width:40px"><input type="checkbox" id="selectAllCheckbox" onchange="toggleSelectAll(this)"></th><th>Name</th><th>Type</th><th>Size</th><th class="actions-col">Actions</th></tr>';
|
||||
|
||||
|
||||
const sortedFiles = files.sort((a, b) => {
|
||||
// Directories first, then epub files, then other files, alphabetically within each group
|
||||
@ -756,7 +758,9 @@
|
||||
if (!folderPath.endsWith("/")) folderPath += "/";
|
||||
folderPath += file.name;
|
||||
|
||||
fileTableContent += '<tr class="folder-row">';
|
||||
// Checkbox cell + folder row
|
||||
fileTableContent += `<tr class="folder-row">`;
|
||||
fileTableContent += `<td><input type="checkbox" class="select-item" data-path="${encodeURIComponent(folderPath)}" data-name="${escapeHtml(file.name)}" data-type="folder"></td>`;
|
||||
fileTableContent += `<td><span class="file-icon">📁</span><a href="/files?path=${encodeURIComponent(folderPath)}" class="folder-link">${escapeHtml(file.name)}</a><span class="folder-badge">FOLDER</span></td>`;
|
||||
fileTableContent += '<td>Folder</td>';
|
||||
fileTableContent += '<td>-</td>';
|
||||
@ -767,7 +771,9 @@
|
||||
if (!filePath.endsWith("/")) filePath += "/";
|
||||
filePath += file.name;
|
||||
|
||||
// Checkbox cell + file row
|
||||
fileTableContent += `<tr class="${file.isEpub ? 'epub-file' : ''}">`;
|
||||
fileTableContent += `<td><input type="checkbox" class="select-item" data-path="${encodeURIComponent(filePath)}" data-name="${escapeHtml(file.name)}" data-type="file"></td>`;
|
||||
fileTableContent += `<td><span class="file-icon">${file.isEpub ? '📗' : '📄'}</span>${escapeHtml(file.name)}`;
|
||||
if (file.isEpub) fileTableContent += '<span class="epub-badge">EPUB</span>';
|
||||
fileTableContent += '</td>';
|
||||
@ -808,6 +814,92 @@
|
||||
document.getElementById('folderModal').classList.remove('open');
|
||||
}
|
||||
|
||||
// Toggle select-all checkbox
|
||||
function toggleSelectAll(master) {
|
||||
const checked = master.checked;
|
||||
document.querySelectorAll('.select-item').forEach(cb => {
|
||||
cb.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedItems() {
|
||||
const items = [];
|
||||
document.querySelectorAll('.select-item:checked').forEach(cb => {
|
||||
items.push({
|
||||
name: cb.dataset.name || decodeURIComponent(cb.dataset.path).split('/').pop(),
|
||||
path: decodeURIComponent(cb.dataset.path),
|
||||
isFolder: cb.dataset.type === 'folder'
|
||||
});
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
// Open delete modal for currently selected checkboxes
|
||||
function openDeleteSelectedModal() {
|
||||
const items = getSelectedItems();
|
||||
if (items.length === 0) {
|
||||
alert('Please select at least one item to delete.');
|
||||
return;
|
||||
}
|
||||
openDeleteModalForItems(items);
|
||||
}
|
||||
|
||||
// Open delete modal for a single item (keeps backwards compatibility with per-row delete button)
|
||||
function openDeleteModal(name, path, isFolder) {
|
||||
openDeleteModalForItems([{ name: name, path: path, isFolder: !!isFolder }]);
|
||||
}
|
||||
|
||||
let deleteItemsGlobal = [];
|
||||
|
||||
function openDeleteModalForItems(items) {
|
||||
deleteItemsGlobal = items;
|
||||
const listEl = document.getElementById('deleteItemList');
|
||||
listEl.innerHTML = '';
|
||||
items.forEach(it => {
|
||||
const div = document.createElement('div');
|
||||
div.style.marginBottom = '6px';
|
||||
div.textContent = (it.isFolder ? '📁 ' : '📄 ') + it.path;
|
||||
listEl.appendChild(div);
|
||||
});
|
||||
document.getElementById('deleteModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
document.getElementById('deleteModal').classList.remove('open');
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
if (!deleteItemsGlobal || deleteItemsGlobal.length === 0) {
|
||||
closeDeleteModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const paths = deleteItemsGlobal.map(it => {
|
||||
// Ensure path starts with /
|
||||
let p = it.path;
|
||||
if (!p.startsWith('/')) p = '/' + p;
|
||||
return p;
|
||||
});
|
||||
|
||||
const body = 'paths=' + encodeURIComponent(JSON.stringify(paths));
|
||||
fetch('/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: body
|
||||
}).then(async res => {
|
||||
if (res.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const text = await res.text();
|
||||
alert('Failed to delete: ' + text);
|
||||
closeDeleteModal();
|
||||
}
|
||||
}).catch(() => {
|
||||
alert('Failed to delete - network error');
|
||||
closeDeleteModal();
|
||||
});
|
||||
}
|
||||
|
||||
function validateFile() {
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
@ -1174,47 +1266,6 @@ function retryAllFailedUploads() {
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
// Delete functions
|
||||
function openDeleteModal(name, path, isFolder) {
|
||||
document.getElementById('deleteItemName').textContent = (isFolder ? '📁 ' : '📄 ') + name;
|
||||
document.getElementById('deleteItemPath').value = path;
|
||||
document.getElementById('deleteItemType').value = isFolder ? 'folder' : 'file';
|
||||
document.getElementById('deleteModal').classList.add('open');
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
document.getElementById('deleteModal').classList.remove('open');
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
const path = document.getElementById('deleteItemPath').value;
|
||||
const itemType = document.getElementById('deleteItemType').value;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('path', path);
|
||||
formData.append('type', itemType);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/delete', true);
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Failed to delete: ' + xhr.responseText);
|
||||
closeDeleteModal();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
alert('Failed to delete - network error');
|
||||
closeDeleteModal();
|
||||
};
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
hydrate();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user