feat: enhance file deletion functionality with multi-select (#682)
## Summary * **What is the goal of this PR?** Enhances the file manager with multi-select deletion functionality and improved UI formatting. * **What changes are included?** * Added multi-select capability for file deletion in the web interface * Fixed formatting issues in file table for folder rows * Updated [.gitignore] to exclude additional build artifacts and cache files * Refactored CrossPointWebServer.cpp to support batch file deletion * Enhanced FilesPage.html with improved UI for file selection and deletion ## Additional Context * The file deletion endpoint now handles multiple files in a single request, improving efficiency when removing multiple files * Changes are focused on the web file manager component only --- ### 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: Jessica Harrison <jessica.harrison@entelect.co.za> Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
@@ -914,84 +914,109 @@ void CrossPointWebServer::handleMove() 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");
|
||||
JsonDocument doc;
|
||||
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(".")) {
|
||||
LOG_DBG("WEB", "Delete rejected - hidden/system item: %s", itemPath.c_str());
|
||||
server->send(403, "text/plain", "Cannot delete system files");
|
||||
auto 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])) {
|
||||
LOG_DBG("WEB", "Delete rejected - protected item: %s", 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) {
|
||||
auto itemPath = p.as<String>();
|
||||
|
||||
// Validate path
|
||||
if (itemPath.isEmpty() || itemPath == "/") {
|
||||
failedItems += itemPath + " (cannot delete root); ";
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if item exists
|
||||
if (!Storage.exists(itemPath.c_str())) {
|
||||
LOG_DBG("WEB", "Delete failed - item not found: %s", itemPath.c_str());
|
||||
server->send(404, "text/plain", "Item not found");
|
||||
return;
|
||||
}
|
||||
// Ensure path starts with /
|
||||
if (!itemPath.startsWith("/")) {
|
||||
itemPath = "/" + itemPath;
|
||||
}
|
||||
|
||||
LOG_DBG("WEB", "Attempting to delete %s: %s", 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 = Storage.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();
|
||||
LOG_DBG("WEB", "Delete failed - folder not empty: %s", 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 = Storage.rmdir(itemPath.c_str());
|
||||
} else {
|
||||
// For files, use remove
|
||||
success = Storage.remove(itemPath.c_str());
|
||||
if (isProtected) {
|
||||
failedItems += itemPath + " (protected file); ";
|
||||
allSuccess = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if item exists
|
||||
if (!Storage.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 = Storage.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 = Storage.rmdir(itemPath.c_str());
|
||||
} else {
|
||||
// It's a file (or couldn't open as dir) — remove file
|
||||
if (f) f.close();
|
||||
success = Storage.remove(itemPath.c_str());
|
||||
clearEpubCacheIfNeeded(itemPath);
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
failedItems += itemPath + " (deletion failed); ";
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (success) {
|
||||
LOG_DBG("WEB", "Successfully deleted: %s", itemPath.c_str());
|
||||
server->send(200, "text/plain", "Deleted successfully");
|
||||
if (allSuccess) {
|
||||
server->send(200, "text/plain", "All items deleted successfully");
|
||||
} else {
|
||||
LOG_ERR("WEB", "Failed to delete: %s", itemPath.c_str());
|
||||
server->send(500, "text/plain", "Failed to delete item");
|
||||
server->send(500, "text/plain", "Failed to delete some items: " + failedItems);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user