This commit is contained in:
Jessica765 2026-02-04 09:18:12 +11:00 committed by GitHub
commit f794ce4242
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 187 additions and 107 deletions

5
.gitignore vendored
View File

@ -9,3 +9,8 @@ build
**/__pycache__/ **/__pycache__/
/compile_commands.json /compile_commands.json
/.cache /.cache
.history/
node_modules/
package.json
package-lock.json
mise.toml

View File

@ -706,19 +706,39 @@ void CrossPointWebServer::handleCreateFolder() const {
} }
void CrossPointWebServer::handleDelete() const { void CrossPointWebServer::handleDelete() const {
// Get path from form data // Check if 'paths' argument is provided
if (!server->hasArg("path")) { if (!server->hasArg("paths")) {
server->send(400, "text/plain", "Missing path"); server->send(400, "text/plain", "Missing paths");
return; return;
} }
String itemPath = server->arg("path"); // Parse paths
const String itemType = server->hasArg("type") ? server->arg("type") : "file"; String pathsArg = server->arg("paths");
DynamicJsonDocument doc(2048);
DeserializationError error = deserializeJson(doc, pathsArg);
if (error) {
server->send(400, "text/plain", "Invalid paths format");
return;
}
JsonArray paths = doc.as<JsonArray>();
if (paths.isNull() || paths.size() == 0) {
server->send(400, "text/plain", "No paths provided");
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 // Validate path
if (itemPath.isEmpty() || itemPath == "/") { if (itemPath.isEmpty() || itemPath == "/") {
server->send(400, "text/plain", "Cannot delete root directory"); failedItems += itemPath + " (cannot delete root); ";
return; allSuccess = false;
continue;
} }
// Ensure path starts with / // Ensure path starts with /
@ -729,61 +749,65 @@ void CrossPointWebServer::handleDelete() const {
// Security check: prevent deletion of protected items // Security check: prevent deletion of protected items
const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1); const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1);
// Check if item starts with a dot (hidden/system file) // Hidden/system files are protected
if (itemName.startsWith(".")) { if (itemName.startsWith(".")) {
Serial.printf("[%lu] [WEB] Delete rejected - hidden/system item: %s\n", millis(), itemPath.c_str()); failedItems += itemPath + " (hidden/system file); ";
server->send(403, "text/plain", "Cannot delete system files"); allSuccess = false;
return; continue;
} }
// Check against explicitly protected items // Check against explicitly protected items
bool isProtected = false;
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) { for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
if (itemName.equals(HIDDEN_ITEMS[i])) { if (itemName.equals(HIDDEN_ITEMS[i])) {
Serial.printf("[%lu] [WEB] Delete rejected - protected item: %s\n", millis(), itemPath.c_str()); isProtected = true;
server->send(403, "text/plain", "Cannot delete protected items"); break;
return;
} }
} }
if (isProtected) {
failedItems += itemPath + " (protected file); ";
allSuccess = false;
continue;
}
// Check if item exists // Check if item exists
if (!SdMan.exists(itemPath.c_str())) { if (!SdMan.exists(itemPath.c_str())) {
Serial.printf("[%lu] [WEB] Delete failed - item not found: %s\n", millis(), itemPath.c_str()); failedItems += itemPath + " (not found); ";
server->send(404, "text/plain", "Item not found"); allSuccess = false;
return; continue;
} }
Serial.printf("[%lu] [WEB] Attempting to delete %s: %s\n", millis(), itemType.c_str(), itemPath.c_str()); // Decide whether it's a directory or file by opening it
bool success = false; bool success = false;
FsFile f = SdMan.open(itemPath.c_str());
if (itemType == "folder") { if (f && f.isDirectory()) {
// For folders, try to remove (will fail if not empty) // For folders, ensure empty before removing
FsFile dir = SdMan.open(itemPath.c_str()); FsFile entry = f.openNextFile();
if (dir && dir.isDirectory()) {
// Check if folder is empty
FsFile entry = dir.openNextFile();
if (entry) { if (entry) {
// Folder is not empty
entry.close(); entry.close();
dir.close(); f.close();
Serial.printf("[%lu] [WEB] Delete failed - folder not empty: %s\n", millis(), itemPath.c_str()); failedItems += itemPath + " (folder not empty); ";
server->send(400, "text/plain", "Folder is not empty. Delete contents first."); allSuccess = false;
return; continue;
}
dir.close();
} }
f.close();
success = SdMan.rmdir(itemPath.c_str()); success = SdMan.rmdir(itemPath.c_str());
} else { } else {
// For files, use remove // It's a file (or couldn't open as dir) — remove file
if (f) f.close();
success = SdMan.remove(itemPath.c_str()); success = SdMan.remove(itemPath.c_str());
} }
if (success) { if (!success) {
Serial.printf("[%lu] [WEB] Successfully deleted: %s\n", millis(), itemPath.c_str()); failedItems += itemPath + " (deletion failed); ";
server->send(200, "text/plain", "Deleted successfully"); allSuccess = false;
}
}
if (allSuccess) {
server->send(200, "text/plain", "All items deleted successfully");
} else { } else {
Serial.printf("[%lu] [WEB] Failed to delete: %s\n", millis(), itemPath.c_str()); server->send(500, "text/plain", "Failed to delete some items: " + failedItems);
server->send(500, "text/plain", "Failed to delete item");
} }
} }

View File

@ -586,6 +586,7 @@
<div class="action-buttons"> <div class="action-buttons">
<button class="action-btn upload-action-btn" onclick="openUploadModal()">📤 Upload</button> <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 folder-action-btn" onclick="openFolderModal()">📁 New Folder</button>
<button class="action-btn" style="background-color:#e74c3c" onclick="openDeleteSelectedModal()">🗑️ Delete Selected</button>
</div> </div>
</div> </div>
@ -652,13 +653,11 @@
<div class="modal-overlay" id="deleteModal"> <div class="modal-overlay" id="deleteModal">
<div class="modal"> <div class="modal">
<button class="modal-close" onclick="closeDeleteModal()">&times;</button> <button class="modal-close" onclick="closeDeleteModal()">&times;</button>
<h3>🗑️ Delete Item</h3> <h3>🗑️ Delete Item(s)</h3>
<div class="folder-form"> <div class="folder-form">
<p class="delete-warning">⚠️ This action cannot be undone!</p> <p class="delete-warning">⚠️ This action cannot be undone!</p>
<p class="file-info">Are you sure you want to delete:</p> <p class="file-info">Are you sure you want to delete the following item(s)?</p>
<p class="delete-item-name" id="deleteItemName"></p> <div id="deleteItemList" style="max-height:240px; overflow:auto; margin-bottom:10px;"></div>
<input type="hidden" id="deleteItemPath">
<input type="hidden" id="deleteItemType">
<button class="delete-btn-confirm" onclick="confirmDelete()">Delete</button> <button class="delete-btn-confirm" onclick="confirmDelete()">Delete</button>
<button class="delete-btn-cancel" onclick="closeDeleteModal()">Cancel</button> <button class="delete-btn-cancel" onclick="closeDeleteModal()">Cancel</button>
</div> </div>
@ -739,7 +738,10 @@
fileTable.innerHTML = '<div class="no-files">This folder is empty</div>'; fileTable.innerHTML = '<div class="no-files">This folder is empty</div>';
} else { } else {
let fileTableContent = '<table class="file-table">'; 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) => { const sortedFiles = files.sort((a, b) => {
// Directories first, then epub files, then other files, alphabetically within each group // Directories first, then epub files, then other files, alphabetically within each group
@ -756,7 +758,9 @@
if (!folderPath.endsWith("/")) folderPath += "/"; if (!folderPath.endsWith("/")) folderPath += "/";
folderPath += file.name; 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><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>Folder</td>';
fileTableContent += '<td>-</td>'; fileTableContent += '<td>-</td>';
@ -767,7 +771,9 @@
if (!filePath.endsWith("/")) filePath += "/"; if (!filePath.endsWith("/")) filePath += "/";
filePath += file.name; filePath += file.name;
// Checkbox cell + file row
fileTableContent += `<tr class="${file.isEpub ? 'epub-file' : ''}">`; 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)}`; fileTableContent += `<td><span class="file-icon">${file.isEpub ? '📗' : '📄'}</span>${escapeHtml(file.name)}`;
if (file.isEpub) fileTableContent += '<span class="epub-badge">EPUB</span>'; if (file.isEpub) fileTableContent += '<span class="epub-badge">EPUB</span>';
fileTableContent += '</td>'; fileTableContent += '</td>';
@ -808,6 +814,92 @@
document.getElementById('folderModal').classList.remove('open'); 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() { function validateFile() {
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const uploadBtn = document.getElementById('uploadBtn'); const uploadBtn = document.getElementById('uploadBtn');
@ -1174,47 +1266,6 @@ function retryAllFailedUploads() {
xhr.send(formData); 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(); hydrate();
</script> </script>
</body> </body>