mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-05 07:07:38 +03:00
improve file manager
This commit is contained in:
parent
14972b34cb
commit
ec82ff209d
@ -3,6 +3,38 @@ import re
|
||||
|
||||
SRC_DIR = "src"
|
||||
|
||||
|
||||
def bytes_to_cpp_byte_array(input_file_bytes: bytes, input_file_path: str, output_header_file: str, variable_name: str):
|
||||
# Format bytes into C++ byte array initialiser format (hex values)
|
||||
hex_values = [f"0x{b:02x}" for b in input_file_bytes]
|
||||
bytes_array_declaration = ", ".join(hex_values)
|
||||
data_length = len(input_file_bytes)
|
||||
|
||||
# Generate the C++ header file content
|
||||
header_content = f"""
|
||||
#ifndef {variable_name.upper()}_H
|
||||
#define {variable_name.upper()}_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Embedded file: {os.path.basename(input_file_path)}
|
||||
constexpr uint8_t {variable_name}_data[] = {{
|
||||
{bytes_array_declaration}
|
||||
}};
|
||||
constexpr size_t {variable_name}_size = {data_length};
|
||||
|
||||
#endif // {variable_name.upper()}_H
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(output_header_file, 'w') as f:
|
||||
f.write(header_content)
|
||||
print(f"Successfully generated C++ header file: {output_header_file}")
|
||||
except IOError as e:
|
||||
print(f"Error writing header file: {e}")
|
||||
exit(1)
|
||||
|
||||
def minify_html(html: str) -> str:
|
||||
# Tags where whitespace should be preserved
|
||||
preserve_tags = ['pre', 'code', 'textarea', 'script', 'style']
|
||||
@ -31,21 +63,37 @@ def minify_html(html: str) -> str:
|
||||
|
||||
return html.strip()
|
||||
|
||||
def read_src_file(root: str, file: str) -> str:
|
||||
file_path = os.path.join(root, file)
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def build_static(src_path: str, dest_dir: str):
|
||||
filename = os.path.basename(src_path)
|
||||
base_name, extension = os.path.splitext(filename)
|
||||
postfix = extension[1:].capitalize()
|
||||
base_name = f"{os.path.splitext(filename)[0]}{postfix}"
|
||||
output_cpp_header = os.path.join(dest_dir, f"{base_name}.generated.h")
|
||||
cpp_variable_name = base_name
|
||||
if not os.path.exists(dest_dir):
|
||||
os.makedirs(dest_dir)
|
||||
|
||||
if extension == ".html":
|
||||
# minify HTML content
|
||||
src_string = open(src_path, "r").read()
|
||||
minified_str = minify_html(src_string)
|
||||
src_bytes = minified_str.encode("utf-8")
|
||||
else:
|
||||
src_bytes = open(src_path, "rb").read()
|
||||
|
||||
bytes_to_cpp_byte_array(
|
||||
input_file_bytes=src_bytes,
|
||||
input_file_path=src_path,
|
||||
output_header_file=output_cpp_header,
|
||||
variable_name=cpp_variable_name
|
||||
)
|
||||
|
||||
for root, _, files in os.walk(SRC_DIR):
|
||||
for file in files:
|
||||
if file.endswith(".html"):
|
||||
html_path = os.path.join(root, file)
|
||||
with open(html_path, "r", encoding="utf-8") as f:
|
||||
html_content = f.read()
|
||||
|
||||
# minified = regex.sub("\g<1>", html_content)
|
||||
minified = minify_html(html_content)
|
||||
base_name = f"{os.path.splitext(file)[0]}Html"
|
||||
header_path = os.path.join(root, f"{base_name}.generated.h")
|
||||
|
||||
with open(header_path, "w", encoding="utf-8") as h:
|
||||
h.write(f"// THIS FILE IS AUTOGENERATED, DO NOT EDIT MANUALLY\n\n")
|
||||
h.write(f"#pragma once\n")
|
||||
h.write(f'constexpr char {base_name}[] PROGMEM = R"rawliteral({minified})rawliteral";\n')
|
||||
|
||||
print(f"Generated: {header_path}")
|
||||
if file.endswith((".html", ".css", ".js")):
|
||||
build_static(os.path.join(root, file), root)
|
||||
|
||||
@ -67,20 +67,23 @@ void CrossPointWebServer::begin() {
|
||||
|
||||
// Setup routes
|
||||
Serial.printf("[%lu] [WEB] Setting up routes...\n", millis());
|
||||
server->on("/", HTTP_GET, [this] { handleRoot(); });
|
||||
server->on("/", HTTP_GET, [this] { handleStatusPage(); });
|
||||
server->on("/files", HTTP_GET, [this] { handleFileList(); });
|
||||
|
||||
server->on("/api/status", HTTP_GET, [this] { handleStatus(); });
|
||||
server->on("/api/files", HTTP_GET, [this] { handleFileListData(); });
|
||||
|
||||
// Upload endpoint with special handling for multipart form data
|
||||
server->on("/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); });
|
||||
server->on("/api/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); });
|
||||
|
||||
// Create folder endpoint
|
||||
server->on("/mkdir", HTTP_POST, [this] { handleCreateFolder(); });
|
||||
server->on("/api/mkdir", HTTP_POST, [this] { handleCreateFolder(); });
|
||||
|
||||
// Move file/folder endpoint
|
||||
server->on("/api/move", HTTP_POST, [this] { handleMove(); });
|
||||
|
||||
// Delete file/folder endpoint
|
||||
server->on("/delete", HTTP_POST, [this] { handleDelete(); });
|
||||
server->on("/api/delete", HTTP_POST, [this] { handleDelete(); });
|
||||
|
||||
server->onNotFound([this] { handleNotFound(); });
|
||||
Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap());
|
||||
@ -150,8 +153,10 @@ void CrossPointWebServer::handleClient() const {
|
||||
server->handleClient();
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleRoot() const {
|
||||
server->send(200, "text/html", HomePageHtml);
|
||||
void CrossPointWebServer::handleStatusPage() const {
|
||||
server->setContentLength(HomePageHtml_size);
|
||||
server->send(200, "text/html", "");
|
||||
server->sendContent((char*)HomePageHtml_data, HomePageHtml_size);
|
||||
Serial.printf("[%lu] [WEB] Served root page\n", millis());
|
||||
}
|
||||
|
||||
@ -241,7 +246,11 @@ bool CrossPointWebServer::isEpubFile(const String& filename) const {
|
||||
return lower.endsWith(".epub");
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleFileList() const { server->send(200, "text/html", FilesPageHtml); }
|
||||
void CrossPointWebServer::handleFileList() const {
|
||||
server->setContentLength(FilesPageHtml_size);
|
||||
server->send(200, "text/html", "");
|
||||
server->sendContent((char*)FilesPageHtml_data, FilesPageHtml_size);
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleFileListData() const {
|
||||
// Get current path from query string (default to root)
|
||||
@ -555,3 +564,69 @@ void CrossPointWebServer::handleDelete() const {
|
||||
server->send(500, "text/plain", "Failed to delete item");
|
||||
}
|
||||
}
|
||||
|
||||
void CrossPointWebServer::handleMove() const {
|
||||
// Get path from form data
|
||||
if (!server->hasArg("path")) {
|
||||
server->send(400, "text/plain", "Missing path");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!server->hasArg("new_path")) {
|
||||
server->send(400, "text/plain", "Missing new path");
|
||||
return;
|
||||
}
|
||||
|
||||
String itemPath = server->arg("path");
|
||||
String newItemPath = server->arg("new_path");
|
||||
const String itemType = server->hasArg("type") ? server->arg("type") : "file";
|
||||
|
||||
// Validate path
|
||||
if (itemPath.isEmpty() || itemPath == "/") {
|
||||
server->send(400, "text/plain", "Cannot move root directory");
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure path starts with /
|
||||
if (!itemPath.startsWith("/")) {
|
||||
itemPath = "/" + itemPath;
|
||||
}
|
||||
|
||||
// Security check: prevent renaming 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] Move rejected - hidden/system item: %s\n", millis(), itemPath.c_str());
|
||||
server->send(403, "text/plain", "Cannot move system files");
|
||||
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] Move rejected - protected item: %s\n", millis(), itemPath.c_str());
|
||||
server->send(403, "text/plain", "Cannot move protected items");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if item exists
|
||||
if (!SdMan.exists(itemPath.c_str())) {
|
||||
Serial.printf("[%lu] [WEB] Move failed - item not found: %s\n", millis(), itemPath.c_str());
|
||||
server->send(404, "text/plain", "Item not found");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [WEB] Attempting to move %s: %s\n", millis(), itemType.c_str(), itemPath.c_str());
|
||||
|
||||
bool success = SdMan.rename(itemPath.c_str(), newItemPath.c_str());
|
||||
|
||||
if (success) {
|
||||
Serial.printf("[%lu] [WEB] Successfully moved: %s\n", millis(), itemPath.c_str());
|
||||
server->send(200, "text/plain", "Moved successfully");
|
||||
} else {
|
||||
Serial.printf("[%lu] [WEB] Failed to move: %s\n", millis(), itemPath.c_str());
|
||||
server->send(500, "text/plain", "Failed to move item");
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ class CrossPointWebServer {
|
||||
bool isEpubFile(const String& filename) const;
|
||||
|
||||
// Request handlers
|
||||
void handleRoot() const;
|
||||
void handleStatusPage() const;
|
||||
void handleNotFound() const;
|
||||
void handleStatus() const;
|
||||
void handleFileList() const;
|
||||
@ -53,4 +53,5 @@ class CrossPointWebServer {
|
||||
void handleUploadPost() const;
|
||||
void handleCreateFolder() const;
|
||||
void handleDelete() const;
|
||||
void handleMove() const;
|
||||
};
|
||||
|
||||
@ -52,6 +52,7 @@
|
||||
.breadcrumb-inline a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.breadcrumb-inline a:hover {
|
||||
text-decoration: underline;
|
||||
@ -667,7 +668,7 @@
|
||||
|
||||
<script>
|
||||
// get current path from query parameter
|
||||
const currentPath = decodeURIComponent(new URLSearchParams(window.location.search).get('path') || '/');
|
||||
let currentPath = decodeURIComponent(new URLSearchParams(window.location.search).get('path') || '/');
|
||||
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
@ -686,32 +687,45 @@
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)).toLocaleString() + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
// Close modals when clicking overlay
|
||||
document.querySelectorAll('.modal-overlay').forEach(function(overlay) {
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) {
|
||||
overlay.classList.remove('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
async function goToPath(path) {
|
||||
window.history.pushState({}, '', '?path=' + encodeURIComponent(path))
|
||||
currentPath = path;
|
||||
updateBreadcrumbs();
|
||||
fillFilesTable();
|
||||
}
|
||||
|
||||
function reloadContent() {
|
||||
goToPath(currentPath);
|
||||
}
|
||||
|
||||
async function updateBreadcrumbs() {
|
||||
const breadcrumbs = document.getElementById('directory-breadcrumbs');
|
||||
const fileTable = document.getElementById('file-table');
|
||||
|
||||
let breadcrumbContent = '<span class="sep">/</span>';
|
||||
if (currentPath === '/') {
|
||||
breadcrumbContent += '<span class="current">🏠</span>';
|
||||
} else {
|
||||
breadcrumbContent += '<a href="/files">🏠</a>';
|
||||
breadcrumbContent += `<a onclick="goToPath('/')">🏠</a>`;
|
||||
const pathSegments = currentPath.split('/');
|
||||
pathSegments.slice(1, pathSegments.length - 1).forEach(function(segment, index) {
|
||||
breadcrumbContent += '<span class="sep">/</span><a href="/files?path=' + encodeURIComponent(pathSegments.slice(0, index + 2).join('/')) + '">' + escapeHtml(segment) + '</a>';
|
||||
breadcrumbContent += `<span class="sep">/</span><a onclick="goToPath('${pathSegments.slice(0, index + 2).join('/')}')">${escapeHtml(segment)}</a>`;
|
||||
});
|
||||
breadcrumbContent += '<span class="sep">/</span>';
|
||||
breadcrumbContent += '<span class="current">' + escapeHtml(pathSegments[pathSegments.length - 1]) + '</span>';
|
||||
}
|
||||
breadcrumbs.innerHTML = breadcrumbContent;
|
||||
}
|
||||
|
||||
async function fillFilesTable() {
|
||||
const fileTable = document.getElementById('file-table');
|
||||
const folderSummary = document.getElementById('folder-summary');
|
||||
|
||||
folderSummary.innerHTML = "";
|
||||
fileTable.innerHTML = `
|
||||
<div class="loader-container">
|
||||
<span class="loader"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let files = [];
|
||||
try {
|
||||
@ -733,7 +747,7 @@
|
||||
totalSize += file.size;
|
||||
});
|
||||
|
||||
document.getElementById('folder-summary').innerHTML = `${folderCount} folders, ${files.length - folderCount} files, ${formatFileSize(totalSize)}`;
|
||||
folderSummary.innerHTML = `${folderCount} folders, ${files.length - folderCount} files, ${formatFileSize(totalSize)}`;
|
||||
|
||||
if (files.length === 0) {
|
||||
fileTable.innerHTML = '<div class="no-files">This folder is empty</div>';
|
||||
@ -756,25 +770,37 @@
|
||||
if (!folderPath.endsWith("/")) folderPath += "/";
|
||||
folderPath += file.name;
|
||||
|
||||
fileTableContent += '<tr class="folder-row">';
|
||||
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>';
|
||||
fileTableContent += `<td class="actions-col"><button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${folderPath.replaceAll("'", "\\'")}', true)" title="Delete folder">🗑️</button></td>`;
|
||||
fileTableContent += '</tr>';
|
||||
|
||||
fileTableContent += `
|
||||
<tr class="folder-row">
|
||||
<td>
|
||||
<span class="file-icon">📁</span><a onclick="goToPath('${folderPath}')" class="folder-link">${escapeHtml(file.name)}</a><span class="folder-badge">FOLDER</span>
|
||||
</td>
|
||||
<td>Folder</td>
|
||||
<td>-</td>
|
||||
<td class="actions-col">
|
||||
<button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${folderPath.replaceAll("'", "\\'")}', true)" title="Delete folder">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
} else {
|
||||
let filePath = currentPath;
|
||||
if (!filePath.endsWith("/")) filePath += "/";
|
||||
filePath += file.name;
|
||||
|
||||
fileTableContent += `<tr class="${file.isEpub ? 'epub-file' : ''}">`;
|
||||
fileTableContent += `<td><span class="file-icon">${file.isEpub ? '📗' : '📄'}</span>${escapeHtml(file.name)}`;
|
||||
if (file.isEpub) fileTableContent += '<span class="epub-badge">EPUB</span>';
|
||||
fileTableContent += '</td>';
|
||||
fileTableContent += `<td>${file.name.split('.').pop().toUpperCase()}</td>`;
|
||||
fileTableContent += `<td>${formatFileSize(file.size)}</td>`;
|
||||
fileTableContent += `<td class="actions-col"><button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}', false)" title="Delete file">🗑️</button></td>`;
|
||||
fileTableContent += '</tr>';
|
||||
|
||||
fileTableContent += `
|
||||
<tr class="${file.isEpub ? 'epub-file' : ''}">
|
||||
<td>
|
||||
<span class="file-icon">${file.isEpub ? '📗' : '📄'}</span>${escapeHtml(file.name)}
|
||||
${file.isEpub ? '<span class="epub-badge">EPUB</span>' : ''}
|
||||
</td>
|
||||
<td>${file.name.split('.').pop().toUpperCase()}</td>
|
||||
<td>${formatFileSize(file.size)}</td>
|
||||
<td class="actions-col">
|
||||
<button class="delete-btn" onclick="openDeleteModal('${file.name.replaceAll("'", "\\'")}', '${filePath.replaceAll("'", "\\'")}', false)" title="Delete file">🗑️</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
@ -783,6 +809,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function hydrate() {
|
||||
// Close modals when clicking overlay
|
||||
document.querySelectorAll('.modal-overlay').forEach(function(overlay) {
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) {
|
||||
overlay.classList.remove('open');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await updateBreadcrumbs();
|
||||
await fillFilesTable();
|
||||
}
|
||||
|
||||
// Modal functions
|
||||
function openUploadModal() {
|
||||
document.getElementById('uploadPathDisplay').textContent = currentPath === '/' ? '/ 🏠' : currentPath;
|
||||
@ -845,7 +885,8 @@ function uploadFile() {
|
||||
progressText.textContent = 'All uploads complete!';
|
||||
setTimeout(() => {
|
||||
closeUploadModal();
|
||||
hydrate(); // Refresh file list instead of reloading
|
||||
// hydrate(); // Refresh file list instead of reloading
|
||||
reloadContent();
|
||||
}, 1000);
|
||||
} else {
|
||||
progressFill.style.backgroundColor = '#e74c3c';
|
||||
@ -858,7 +899,8 @@ function uploadFile() {
|
||||
setTimeout(() => {
|
||||
closeUploadModal();
|
||||
showFailedUploadsBanner();
|
||||
hydrate(); // Refresh file list to show successfully uploaded files
|
||||
// hydrate(); // Refresh file list to show successfully uploaded files
|
||||
reloadContent();
|
||||
}, 2000);
|
||||
}
|
||||
return;
|
||||
@ -871,7 +913,7 @@ function uploadFile() {
|
||||
const xhr = new XMLHttpRequest();
|
||||
// Include path as query parameter since multipart form data doesn't make
|
||||
// form fields available until after file upload completes
|
||||
xhr.open('POST', '/upload?path=' + encodeURIComponent(currentPath), true);
|
||||
xhr.open('POST', '/api/upload?path=' + encodeURIComponent(currentPath), true);
|
||||
|
||||
progressFill.style.width = '0%';
|
||||
progressFill.style.backgroundColor = '#4caf50';
|
||||
@ -1008,11 +1050,12 @@ function retryAllFailedUploads() {
|
||||
formData.append('path', currentPath);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/mkdir', true);
|
||||
xhr.open('POST', '/api/mkdir', true);
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
window.location.reload();
|
||||
reloadContent();
|
||||
closeFolderModal();
|
||||
} else {
|
||||
alert('Failed to create folder: ' + xhr.responseText);
|
||||
}
|
||||
@ -1046,15 +1089,15 @@ function retryAllFailedUploads() {
|
||||
formData.append('type', itemType);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/delete', true);
|
||||
xhr.open('POST', '/api/delete', true);
|
||||
|
||||
xhr.onload = function() {
|
||||
if (xhr.status === 200) {
|
||||
window.location.reload();
|
||||
reloadContent();
|
||||
} else {
|
||||
alert('Failed to delete: ' + xhr.responseText);
|
||||
closeDeleteModal();
|
||||
}
|
||||
closeDeleteModal();
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user