mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2025-12-19 15:47:40 +03:00
Hide hidden folders
This commit is contained in:
parent
1bc30fbf2a
commit
e384bdbfc2
@ -9,6 +9,14 @@
|
|||||||
// Global instance
|
// Global instance
|
||||||
CrossPointWebServer crossPointWebServer;
|
CrossPointWebServer crossPointWebServer;
|
||||||
|
|
||||||
|
// Folders/files to hide from the web interface file browser
|
||||||
|
// Note: Items starting with "." are automatically hidden
|
||||||
|
static const char* HIDDEN_ITEMS[] = {
|
||||||
|
"System Volume Information",
|
||||||
|
"XTCache"
|
||||||
|
};
|
||||||
|
static const size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]);
|
||||||
|
|
||||||
// HTML page template
|
// HTML page template
|
||||||
static const char* HTML_PAGE = R"rawliteral(
|
static const char* HTML_PAGE = R"rawliteral(
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -192,6 +200,12 @@ static const char* FILES_PAGE_HEADER = R"rawliteral(
|
|||||||
.epub-file:hover {
|
.epub-file:hover {
|
||||||
background-color: #d4edda !important;
|
background-color: #d4edda !important;
|
||||||
}
|
}
|
||||||
|
.folder-row {
|
||||||
|
background-color: #fff9e6 !important;
|
||||||
|
}
|
||||||
|
.folder-row:hover {
|
||||||
|
background-color: #fff3cd !important;
|
||||||
|
}
|
||||||
.epub-badge {
|
.epub-badge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@ -201,9 +215,44 @@ static const char* FILES_PAGE_HEADER = R"rawliteral(
|
|||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
.folder-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 8px;
|
||||||
|
background-color: #f39c12;
|
||||||
|
color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.75em;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
.file-icon {
|
.file-icon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
.folder-link {
|
||||||
|
color: #2c3e50;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.folder-link:hover {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.breadcrumb {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.breadcrumb a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.breadcrumb a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.breadcrumb span {
|
||||||
|
color: #7f8c8d;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
.upload-form {
|
.upload-form {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
@ -298,6 +347,30 @@ static const char* FILES_PAGE_HEADER = R"rawliteral(
|
|||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
color: #7f8c8d;
|
color: #7f8c8d;
|
||||||
}
|
}
|
||||||
|
.folder-form {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.folder-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.folder-btn {
|
||||||
|
background-color: #f39c12;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
.folder-btn:hover {
|
||||||
|
background-color: #d68910;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -339,6 +412,7 @@ static const char* FILES_PAGE_FOOTER = R"rawliteral(
|
|||||||
function uploadFile() {
|
function uploadFile() {
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
|
const currentPath = document.getElementById('currentPath').value;
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
alert('Please select a file first!');
|
alert('Please select a file first!');
|
||||||
@ -353,6 +427,7 @@ static const char* FILES_PAGE_FOOTER = R"rawliteral(
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
formData.append('path', currentPath);
|
||||||
|
|
||||||
const progressContainer = document.getElementById('progress-container');
|
const progressContainer = document.getElementById('progress-container');
|
||||||
const progressFill = document.getElementById('progress-fill');
|
const progressFill = document.getElementById('progress-fill');
|
||||||
@ -394,6 +469,44 @@ static const char* FILES_PAGE_FOOTER = R"rawliteral(
|
|||||||
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createFolder() {
|
||||||
|
const folderName = document.getElementById('folderName').value.trim();
|
||||||
|
const currentPath = document.getElementById('currentPath').value;
|
||||||
|
|
||||||
|
if (!folderName) {
|
||||||
|
alert('Please enter a folder name!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate folder name (no special characters except underscore and hyphen)
|
||||||
|
const validName = /^[a-zA-Z0-9_\-]+$/.test(folderName);
|
||||||
|
if (!validName) {
|
||||||
|
alert('Folder name can only contain letters, numbers, underscores, and hyphens.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('name', folderName);
|
||||||
|
formData.append('path', currentPath);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/mkdir', true);
|
||||||
|
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
alert('Failed to create folder: ' + xhr.responseText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = function() {
|
||||||
|
alert('Failed to create folder - network error');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -433,6 +546,9 @@ void CrossPointWebServer::begin() {
|
|||||||
// Upload endpoint with special handling for multipart form data
|
// Upload endpoint with special handling for multipart form data
|
||||||
server->on("/upload", HTTP_POST, [this]() { handleUploadPost(); }, [this]() { handleUpload(); });
|
server->on("/upload", HTTP_POST, [this]() { handleUploadPost(); }, [this]() { handleUpload(); });
|
||||||
|
|
||||||
|
// Create folder endpoint
|
||||||
|
server->on("/mkdir", HTTP_POST, [this]() { handleCreateFolder(); });
|
||||||
|
|
||||||
server->onNotFound([this]() { handleNotFound(); });
|
server->onNotFound([this]() { handleNotFound(); });
|
||||||
|
|
||||||
server->begin();
|
server->begin();
|
||||||
@ -516,19 +632,43 @@ std::vector<FileInfo> CrossPointWebServer::scanFiles(const char* path) {
|
|||||||
|
|
||||||
File file = root.openNextFile();
|
File file = root.openNextFile();
|
||||||
while (file) {
|
while (file) {
|
||||||
if (!file.isDirectory()) {
|
String fileName = String(file.name());
|
||||||
|
|
||||||
|
// Skip hidden items (starting with ".")
|
||||||
|
bool shouldHide = fileName.startsWith(".");
|
||||||
|
|
||||||
|
// Check against explicitly hidden items list
|
||||||
|
if (!shouldHide) {
|
||||||
|
for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) {
|
||||||
|
if (fileName.equals(HIDDEN_ITEMS[i])) {
|
||||||
|
shouldHide = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldHide) {
|
||||||
FileInfo info;
|
FileInfo info;
|
||||||
info.name = String(file.name());
|
info.name = fileName;
|
||||||
info.size = file.size();
|
info.isDirectory = file.isDirectory();
|
||||||
info.isEpub = isEpubFile(info.name);
|
|
||||||
|
if (info.isDirectory) {
|
||||||
|
info.size = 0;
|
||||||
|
info.isEpub = false;
|
||||||
|
} else {
|
||||||
|
info.size = file.size();
|
||||||
|
info.isEpub = isEpubFile(info.name);
|
||||||
|
}
|
||||||
|
|
||||||
files.push_back(info);
|
files.push_back(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
file = root.openNextFile();
|
file = root.openNextFile();
|
||||||
}
|
}
|
||||||
root.close();
|
root.close();
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Found %d files\n", millis(), files.size());
|
Serial.printf("[%lu] [WEB] Found %d items (files and folders)\n", millis(), files.size());
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -551,6 +691,20 @@ bool CrossPointWebServer::isEpubFile(const String& filename) {
|
|||||||
void CrossPointWebServer::handleFileList() {
|
void CrossPointWebServer::handleFileList() {
|
||||||
String html = FILES_PAGE_HEADER;
|
String html = FILES_PAGE_HEADER;
|
||||||
|
|
||||||
|
// Get current path from query string (default to root)
|
||||||
|
String currentPath = "/";
|
||||||
|
if (server->hasArg("path")) {
|
||||||
|
currentPath = server->arg("path");
|
||||||
|
// Ensure path starts with /
|
||||||
|
if (!currentPath.startsWith("/")) {
|
||||||
|
currentPath = "/" + currentPath;
|
||||||
|
}
|
||||||
|
// Remove trailing slash unless it's root
|
||||||
|
if (currentPath.length() > 1 && currentPath.endsWith("/")) {
|
||||||
|
currentPath = currentPath.substring(0, currentPath.length() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get message from query string if present
|
// Get message from query string if present
|
||||||
if (server->hasArg("msg")) {
|
if (server->hasArg("msg")) {
|
||||||
String msg = server->arg("msg");
|
String msg = server->arg("msg");
|
||||||
@ -558,67 +712,146 @@ void CrossPointWebServer::handleFileList() {
|
|||||||
html += "<div class=\"message " + msgType + "\">" + msg + "</div>";
|
html += "<div class=\"message " + msgType + "\">" + msg + "</div>";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hidden input to store current path for JavaScript
|
||||||
|
html += "<input type=\"hidden\" id=\"currentPath\" value=\"" + currentPath + "\">";
|
||||||
|
|
||||||
|
// Breadcrumb navigation
|
||||||
|
html += "<div class=\"card\">";
|
||||||
|
html += "<div class=\"breadcrumb\">";
|
||||||
|
html += "<a href=\"/files\">🏠 Root</a>";
|
||||||
|
|
||||||
|
if (currentPath != "/") {
|
||||||
|
String pathParts = currentPath.substring(1); // Remove leading /
|
||||||
|
String buildPath = "";
|
||||||
|
int start = 0;
|
||||||
|
int end = pathParts.indexOf('/');
|
||||||
|
|
||||||
|
while (start < pathParts.length()) {
|
||||||
|
String part;
|
||||||
|
if (end == -1) {
|
||||||
|
part = pathParts.substring(start);
|
||||||
|
buildPath += "/" + part;
|
||||||
|
html += "<span>/</span><strong>" + part + "</strong>";
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
part = pathParts.substring(start, end);
|
||||||
|
buildPath += "/" + part;
|
||||||
|
html += "<span>/</span><a href=\"/files?path=" + buildPath + "\">" + part + "</a>";
|
||||||
|
start = end + 1;
|
||||||
|
end = pathParts.indexOf('/', start);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += "</div>";
|
||||||
|
html += "</div>";
|
||||||
|
|
||||||
// Upload form
|
// Upload form
|
||||||
html += "<div class=\"card\">";
|
html += "<div class=\"card\">";
|
||||||
html += "<h2>📤 Upload eBook</h2>";
|
html += "<h2>📤 Upload eBook to " + (currentPath == "/" ? "Root" : currentPath) + "</h2>";
|
||||||
html += "<div class=\"upload-form\">";
|
html += "<div class=\"upload-form\">";
|
||||||
html += "<p><strong>Select an .epub file to upload:</strong></p>";
|
html += "<p><strong>Select an .epub file to upload:</strong></p>";
|
||||||
html += "<input type=\"file\" id=\"fileInput\" accept=\".epub\" onchange=\"validateFile()\">";
|
html += "<input type=\"file\" id=\"fileInput\" accept=\".epub\" onchange=\"validateFile()\">";
|
||||||
html += "<div class=\"file-info\">Only .epub files are accepted</div>";
|
html += "<div class=\"file-info\">Only .epub files are accepted. File will be uploaded to: " + currentPath + "</div>";
|
||||||
html += "<button id=\"uploadBtn\" class=\"upload-btn\" onclick=\"uploadFile()\" disabled>Upload</button>";
|
html += "<button id=\"uploadBtn\" class=\"upload-btn\" onclick=\"uploadFile()\" disabled>Upload</button>";
|
||||||
html += "<div id=\"progress-container\">";
|
html += "<div id=\"progress-container\">";
|
||||||
html += "<div id=\"progress-bar\"><div id=\"progress-fill\"></div></div>";
|
html += "<div id=\"progress-bar\"><div id=\"progress-fill\"></div></div>";
|
||||||
html += "<div id=\"progress-text\"></div>";
|
html += "<div id=\"progress-text\"></div>";
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
|
|
||||||
|
// Create folder form
|
||||||
|
html += "<div class=\"folder-form\">";
|
||||||
|
html += "<input type=\"text\" id=\"folderName\" class=\"folder-input\" placeholder=\"New folder name...\">";
|
||||||
|
html += "<button class=\"folder-btn\" onclick=\"createFolder()\">📁 Create Folder</button>";
|
||||||
|
html += "</div>";
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
|
|
||||||
// Scan files
|
// Scan files in current path
|
||||||
std::vector<FileInfo> files = scanFiles("/");
|
std::vector<FileInfo> files = scanFiles(currentPath.c_str());
|
||||||
|
|
||||||
// Count epub files
|
// Count items
|
||||||
int epubCount = 0;
|
int epubCount = 0;
|
||||||
|
int folderCount = 0;
|
||||||
size_t totalSize = 0;
|
size_t totalSize = 0;
|
||||||
for (const auto& file : files) {
|
for (const auto& file : files) {
|
||||||
if (file.isEpub) epubCount++;
|
if (file.isDirectory) {
|
||||||
totalSize += file.size;
|
folderCount++;
|
||||||
|
} else {
|
||||||
|
if (file.isEpub) epubCount++;
|
||||||
|
totalSize += file.size;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// File listing
|
// File listing
|
||||||
html += "<div class=\"card\">";
|
html += "<div class=\"card\">";
|
||||||
html += "<h2>📁 Files on SD Card</h2>";
|
html += "<h2>📁 Contents of " + (currentPath == "/" ? "Root" : currentPath) + "</h2>";
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
html += "<div class=\"summary\">";
|
html += "<div class=\"summary\">";
|
||||||
html += "<div class=\"summary-item\"><div class=\"summary-number\">" + String(files.size()) + "</div><div class=\"summary-label\">Total Files</div></div>";
|
html += "<div class=\"summary-item\"><div class=\"summary-number\">" + String(folderCount) + "</div><div class=\"summary-label\">Folders</div></div>";
|
||||||
|
html += "<div class=\"summary-item\"><div class=\"summary-number\">" + String(files.size() - folderCount) + "</div><div class=\"summary-label\">Files</div></div>";
|
||||||
html += "<div class=\"summary-item\"><div class=\"summary-number\">" + String(epubCount) + "</div><div class=\"summary-label\">eBooks</div></div>";
|
html += "<div class=\"summary-item\"><div class=\"summary-number\">" + String(epubCount) + "</div><div class=\"summary-label\">eBooks</div></div>";
|
||||||
html += "<div class=\"summary-item\"><div class=\"summary-number\">" + formatFileSize(totalSize) + "</div><div class=\"summary-label\">Total Size</div></div>";
|
html += "<div class=\"summary-item\"><div class=\"summary-number\">" + formatFileSize(totalSize) + "</div><div class=\"summary-label\">Total Size</div></div>";
|
||||||
html += "</div>";
|
html += "</div>";
|
||||||
|
|
||||||
if (files.empty()) {
|
if (files.empty()) {
|
||||||
html += "<div class=\"no-files\">No files found on SD card</div>";
|
html += "<div class=\"no-files\">This folder is empty</div>";
|
||||||
} else {
|
} else {
|
||||||
html += "<table class=\"file-table\">";
|
html += "<table class=\"file-table\">";
|
||||||
html += "<tr><th>Filename</th><th>Type</th><th>Size</th></tr>";
|
html += "<tr><th>Name</th><th>Type</th><th>Size</th></tr>";
|
||||||
|
|
||||||
// Sort files: epub files first, then alphabetically
|
// Sort files: folders first, then epub files, then other files, alphabetically within each group
|
||||||
std::sort(files.begin(), files.end(), [](const FileInfo& a, const FileInfo& b) {
|
std::sort(files.begin(), files.end(), [](const FileInfo& a, const FileInfo& b) {
|
||||||
if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub;
|
// Folders come first
|
||||||
|
if (a.isDirectory != b.isDirectory) return a.isDirectory > b.isDirectory;
|
||||||
|
// Then sort by epub status (epubs first among files)
|
||||||
|
if (!a.isDirectory && !b.isDirectory) {
|
||||||
|
if (a.isEpub != b.isEpub) return a.isEpub > b.isEpub;
|
||||||
|
}
|
||||||
|
// Then alphabetically
|
||||||
return a.name < b.name;
|
return a.name < b.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const auto& file : files) {
|
for (const auto& file : files) {
|
||||||
String rowClass = file.isEpub ? "epub-file" : "";
|
String rowClass;
|
||||||
String icon = file.isEpub ? "📗" : "📄";
|
String icon;
|
||||||
String badge = file.isEpub ? "<span class=\"epub-badge\">EPUB</span>" : "";
|
String badge;
|
||||||
String ext = file.name.substring(file.name.lastIndexOf('.') + 1);
|
String typeStr;
|
||||||
ext.toUpperCase();
|
String sizeStr;
|
||||||
|
|
||||||
html += "<tr class=\"" + rowClass + "\">";
|
if (file.isDirectory) {
|
||||||
html += "<td><span class=\"file-icon\">" + icon + "</span>" + file.name + badge + "</td>";
|
rowClass = "folder-row";
|
||||||
html += "<td>" + ext + "</td>";
|
icon = "📁";
|
||||||
html += "<td>" + formatFileSize(file.size) + "</td>";
|
badge = "<span class=\"folder-badge\">FOLDER</span>";
|
||||||
html += "</tr>";
|
typeStr = "Folder";
|
||||||
|
sizeStr = "-";
|
||||||
|
|
||||||
|
// Build the path to this folder
|
||||||
|
String folderPath = currentPath;
|
||||||
|
if (!folderPath.endsWith("/")) folderPath += "/";
|
||||||
|
folderPath += file.name;
|
||||||
|
|
||||||
|
html += "<tr class=\"" + rowClass + "\">";
|
||||||
|
html += "<td><span class=\"file-icon\">" + icon + "</span>";
|
||||||
|
html += "<a href=\"/files?path=" + folderPath + "\" class=\"folder-link\">" + file.name + "</a>" + badge + "</td>";
|
||||||
|
html += "<td>" + typeStr + "</td>";
|
||||||
|
html += "<td>" + sizeStr + "</td>";
|
||||||
|
html += "</tr>";
|
||||||
|
} else {
|
||||||
|
rowClass = file.isEpub ? "epub-file" : "";
|
||||||
|
icon = file.isEpub ? "📗" : "📄";
|
||||||
|
badge = file.isEpub ? "<span class=\"epub-badge\">EPUB</span>" : "";
|
||||||
|
String ext = file.name.substring(file.name.lastIndexOf('.') + 1);
|
||||||
|
ext.toUpperCase();
|
||||||
|
typeStr = ext;
|
||||||
|
sizeStr = formatFileSize(file.size);
|
||||||
|
|
||||||
|
html += "<tr class=\"" + rowClass + "\">";
|
||||||
|
html += "<td><span class=\"file-icon\">" + icon + "</span>" + file.name + badge + "</td>";
|
||||||
|
html += "<td>" + typeStr + "</td>";
|
||||||
|
html += "<td>" + sizeStr + "</td>";
|
||||||
|
html += "</tr>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html += "</table>";
|
html += "</table>";
|
||||||
@ -629,12 +862,13 @@ void CrossPointWebServer::handleFileList() {
|
|||||||
html += FILES_PAGE_FOOTER;
|
html += FILES_PAGE_FOOTER;
|
||||||
|
|
||||||
server->send(200, "text/html", html);
|
server->send(200, "text/html", html);
|
||||||
Serial.printf("[%lu] [WEB] Served file listing page\n", millis());
|
Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static variables for upload handling
|
// Static variables for upload handling
|
||||||
static File uploadFile;
|
static File uploadFile;
|
||||||
static String uploadFileName;
|
static String uploadFileName;
|
||||||
|
static String uploadPath = "/";
|
||||||
static size_t uploadSize = 0;
|
static size_t uploadSize = 0;
|
||||||
static bool uploadSuccess = false;
|
static bool uploadSuccess = false;
|
||||||
static String uploadError = "";
|
static String uploadError = "";
|
||||||
@ -648,7 +882,22 @@ void CrossPointWebServer::handleUpload() {
|
|||||||
uploadSuccess = false;
|
uploadSuccess = false;
|
||||||
uploadError = "";
|
uploadError = "";
|
||||||
|
|
||||||
Serial.printf("[%lu] [WEB] Upload start: %s\n", millis(), uploadFileName.c_str());
|
// Get upload path from form data (defaults to root if not specified)
|
||||||
|
if (server->hasArg("path")) {
|
||||||
|
uploadPath = server->arg("path");
|
||||||
|
// Ensure path starts with /
|
||||||
|
if (!uploadPath.startsWith("/")) {
|
||||||
|
uploadPath = "/" + uploadPath;
|
||||||
|
}
|
||||||
|
// Remove trailing slash unless it's root
|
||||||
|
if (uploadPath.length() > 1 && uploadPath.endsWith("/")) {
|
||||||
|
uploadPath = uploadPath.substring(0, uploadPath.length() - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uploadPath = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [WEB] Upload start: %s to path: %s\n", millis(), uploadFileName.c_str(), uploadPath.c_str());
|
||||||
|
|
||||||
// Validate file extension
|
// Validate file extension
|
||||||
if (!isEpubFile(uploadFileName)) {
|
if (!isEpubFile(uploadFileName)) {
|
||||||
@ -658,7 +907,9 @@ void CrossPointWebServer::handleUpload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create file path
|
// Create file path
|
||||||
String filePath = "/" + uploadFileName;
|
String filePath = uploadPath;
|
||||||
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
|
filePath += uploadFileName;
|
||||||
|
|
||||||
// Check if file already exists
|
// Check if file already exists
|
||||||
if (SD.exists(filePath.c_str())) {
|
if (SD.exists(filePath.c_str())) {
|
||||||
@ -702,7 +953,9 @@ void CrossPointWebServer::handleUpload() {
|
|||||||
if (uploadFile) {
|
if (uploadFile) {
|
||||||
uploadFile.close();
|
uploadFile.close();
|
||||||
// Try to delete the incomplete file
|
// Try to delete the incomplete file
|
||||||
String filePath = "/" + uploadFileName;
|
String filePath = uploadPath;
|
||||||
|
if (!filePath.endsWith("/")) filePath += "/";
|
||||||
|
filePath += uploadFileName;
|
||||||
SD.remove(filePath.c_str());
|
SD.remove(filePath.c_str());
|
||||||
}
|
}
|
||||||
uploadError = "Upload aborted";
|
uploadError = "Upload aborted";
|
||||||
@ -718,3 +971,53 @@ void CrossPointWebServer::handleUploadPost() {
|
|||||||
server->send(400, "text/plain", error);
|
server->send(400, "text/plain", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CrossPointWebServer::handleCreateFolder() {
|
||||||
|
// Get folder name from form data
|
||||||
|
if (!server->hasArg("name")) {
|
||||||
|
server->send(400, "text/plain", "Missing folder name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String folderName = server->arg("name");
|
||||||
|
|
||||||
|
// Validate folder name
|
||||||
|
if (folderName.isEmpty()) {
|
||||||
|
server->send(400, "text/plain", "Folder name cannot be empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parent path
|
||||||
|
String parentPath = "/";
|
||||||
|
if (server->hasArg("path")) {
|
||||||
|
parentPath = server->arg("path");
|
||||||
|
if (!parentPath.startsWith("/")) {
|
||||||
|
parentPath = "/" + parentPath;
|
||||||
|
}
|
||||||
|
if (parentPath.length() > 1 && parentPath.endsWith("/")) {
|
||||||
|
parentPath = parentPath.substring(0, parentPath.length() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build full folder path
|
||||||
|
String folderPath = parentPath;
|
||||||
|
if (!folderPath.endsWith("/")) folderPath += "/";
|
||||||
|
folderPath += folderName;
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [WEB] Creating folder: %s\n", millis(), folderPath.c_str());
|
||||||
|
|
||||||
|
// Check if already exists
|
||||||
|
if (SD.exists(folderPath.c_str())) {
|
||||||
|
server->send(400, "text/plain", "Folder already exists");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the folder
|
||||||
|
if (SD.mkdir(folderPath.c_str())) {
|
||||||
|
Serial.printf("[%lu] [WEB] Folder created successfully: %s\n", millis(), folderPath.c_str());
|
||||||
|
server->send(200, "text/plain", "Folder created: " + folderName);
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [WEB] Failed to create folder: %s\n", millis(), folderPath.c_str());
|
||||||
|
server->send(500, "text/plain", "Failed to create folder");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ struct FileInfo {
|
|||||||
String name;
|
String name;
|
||||||
size_t size;
|
size_t size;
|
||||||
bool isEpub;
|
bool isEpub;
|
||||||
|
bool isDirectory;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CrossPointWebServer {
|
class CrossPointWebServer {
|
||||||
@ -49,6 +50,7 @@ class CrossPointWebServer {
|
|||||||
void handleFileList();
|
void handleFileList();
|
||||||
void handleUpload();
|
void handleUpload();
|
||||||
void handleUploadPost();
|
void handleUploadPost();
|
||||||
|
void handleCreateFolder();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Global instance
|
// Global instance
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user