mirror of
https://github.com/Kuingsmile/PicList.git
synced 2026-05-06 20:42:57 +08:00
✨ Feature(custom): rewrite the webserver ui
This commit is contained in:
@@ -11,34 +11,640 @@ import { configPaths } from '~/utils/configPaths'
|
|||||||
|
|
||||||
const defaultPath = process.platform === 'win32' ? 'C:\\Users' : '/'
|
const defaultPath = process.platform === 'win32' ? 'C:\\Users' : '/'
|
||||||
|
|
||||||
function generateDirectoryListingHtml(files: any[], requestPath: any) {
|
function getFileIcon(fileName: string, isDirectory: boolean): string {
|
||||||
let html = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body><h1>Directory Listing</h1><ul>'
|
if (isDirectory)
|
||||||
files.forEach((file: string) => {
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 4v8a1 1 0 001 1h10a1 1 0 001-1V5a1 1 0 00-1-1H7L6 3H3a1 1 0 00-1 1z" fill="#FFCB47"/></svg>`
|
||||||
const filePath = path.join(requestPath, file)
|
|
||||||
html += `<li><a href="${encodeFilePath(filePath)}">${file}</a></li>`
|
const ext = path.extname(fileName).toLowerCase()
|
||||||
})
|
|
||||||
html += '</ul></body></html>'
|
if (['.txt', '.md', '.readme'].includes(ext)) {
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 2v12h8V6l-4-4H4z" fill="#6B73FF" stroke="#5A67D8"/><path d="M8 2v4h4" stroke="#5A67D8" fill="none"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.json'].includes(ext)) {
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 2v12h8V6l-4-4H4z" fill="#FF6B6B" stroke="#E53E3E"/><path d="M8 2v4h4" stroke="#E53E3E" fill="none"/><text x="8" y="10" font-family="monospace" font-size="4" text-anchor="middle" fill="#fff"></></text></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp'].includes(ext)) {
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 3v10h12V3H2z" fill="#4ECDC4" stroke="#319795"/><circle cx="5" cy="6" r="1" fill="#fff"/><path d="m2 10 3-3 2 2 4-4 3 3v2H2v-1z" fill="#fff" opacity="0.8"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.mp4', '.avi', '.mov', '.wmv', '.mkv'].includes(ext)) {
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 3v10h12V3H2z" fill="#FF6B6B" stroke="#E53E3E"/><path d="m6 5 4 3-4 3V5z" fill="#fff"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.mp3', '.wav', '.flac', '.aac'].includes(ext)) {
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 2v12h8V6l-4-4H4z" fill="#9F7AEA" stroke="#805AD5"/><path d="M8 2v4h4" stroke="#805AD5" fill="none"/><circle cx="8" cy="10" r="2" fill="#fff"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 2v12h8V6l-4-4H4z" fill="#F6AD55" stroke="#DD6B20"/><path d="M8 2v4h4" stroke="#DD6B20" fill="none"/><rect x="6" y="8" width="4" height="2" fill="#fff" rx="0.5"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ext === '.pdf') {
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 2v12h8V6l-4-4H4z" fill="#E53E3E" stroke="#C53030"/><path d="M8 2v4h4" stroke="#C53030" fill="none"/><text x="8" y="10" font-family="Arial" font-size="3" text-anchor="middle" fill="#fff">PDF</text></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.exe', '.msi', '.app', '.deb', '.rpm'].includes(ext)) {
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 2v12h8V6l-4-4H4z" fill="#38A169" stroke="#2F855A"/><path d="M8 2v4h4" stroke="#2F855A" fill="none"/><rect x="6" y="8" width="4" height="4" fill="#fff" rx="1"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 2v12h8V6l-4-4H4z" fill="#A0ADB8" stroke="#718096"/><path d="M8 2v4h4" stroke="#718096" fill="none"/></svg>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDirectoryListingHtml(files: any[], requestPath: string, fullPath: string) {
|
||||||
|
const sortedFiles = sortFiles(files, fullPath)
|
||||||
|
|
||||||
|
let html = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>${requestPath || 'Home'} - File Browser</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #212121;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titlebar {
|
||||||
|
height: 32px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #dadce0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
color: #5f6368;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
height: 40px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid #e8eaed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-color: #c4c7c5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 24px;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin: 0 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #202124;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-bar:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 200px;
|
||||||
|
height: 24px;
|
||||||
|
border: 1px solid #dadce0;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
height: 24px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-bottom: 1px solid #e8eaed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #5f6368;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-modified {
|
||||||
|
width: 140px;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-size {
|
||||||
|
width: 80px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #202124;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:active {
|
||||||
|
background: #e8f0fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-modified {
|
||||||
|
width: 140px;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5f6368;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size {
|
||||||
|
width: 80px;
|
||||||
|
text-align: right;
|
||||||
|
padding: 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5f6368;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory .file-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent-dir {
|
||||||
|
border-bottom: 1px solid #e8eaed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parent-dir .file-name {
|
||||||
|
color: #1a73e8;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 200px;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
height: 22px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #e8eaed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #5f6368;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-modified, .file-modified {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-size, .file-size {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="window">
|
||||||
|
<div class="titlebar">
|
||||||
|
File Browser - ${requestPath || 'Home'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<button class="nav-button" onclick="history.back()" title="Back">←</button>
|
||||||
|
<button class="nav-button" onclick="history.forward()" title="Forward">→</button>
|
||||||
|
<button class="nav-button" onclick="location.reload()" title="Refresh">↻</button>
|
||||||
|
<input type="text" class="address-bar" value="${requestPath || '/'}" readonly>
|
||||||
|
<input type="text" class="search-box" placeholder="Search..." id="searchBox">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="file-list-container">
|
||||||
|
<div class="list-header">
|
||||||
|
<div class="header-name">Name</div>
|
||||||
|
<div class="header-modified">Date modified</div>
|
||||||
|
<div class="header-size">Size</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="file-list" id="fileList">
|
||||||
|
`
|
||||||
|
|
||||||
|
if (requestPath !== '/' && requestPath !== '') {
|
||||||
|
const parentPath = path.dirname(requestPath)
|
||||||
|
html += `
|
||||||
|
<a href="${encodeFilePath(parentPath === '.' ? '/' : parentPath)}" class="file-item parent-dir">
|
||||||
|
<div class="file-icon">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<path d="M2 6h12l-1.5-1.5H3.5L2 6z" fill="#1a73e8"/>
|
||||||
|
<path d="M2 6v6a1 1 0 001 1h10a1 1 0 001-1V6H2z" fill="#4285f4"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="file-name">.. (Up to parent directory)</div>
|
||||||
|
<div class="file-modified"></div>
|
||||||
|
<div class="file-size"></div>
|
||||||
|
</a>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortedFiles.length === 0) {
|
||||||
|
html += `
|
||||||
|
<div class="empty-state">
|
||||||
|
<svg class="empty-state-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2h-8l-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
<div>This folder is empty</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
} else {
|
||||||
|
sortedFiles.forEach((fileInfo: any) => {
|
||||||
|
const filePath = path.join(requestPath, fileInfo.name)
|
||||||
|
const icon = getFileIcon(fileInfo.name, fileInfo.isDirectory)
|
||||||
|
const sizeDisplay = fileInfo.isDirectory ? '' : formatFileSize(fileInfo.size || 0)
|
||||||
|
const modifiedDate = fileInfo.mtime
|
||||||
|
? new Date(fileInfo.mtime).toLocaleDateString('en-US', {
|
||||||
|
year: '2-digit',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric'
|
||||||
|
}) +
|
||||||
|
' ' +
|
||||||
|
new Date(fileInfo.mtime).toLocaleTimeString('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<a href="${encodeFilePath(filePath)}" class="file-item ${fileInfo.isDirectory ? 'directory' : ''}">
|
||||||
|
<div class="file-icon">${icon}</div>
|
||||||
|
<div class="file-name">${fileInfo.name}</div>
|
||||||
|
<div class="file-modified">${modifiedDate}</div>
|
||||||
|
<div class="file-size">${sizeDisplay}</div>
|
||||||
|
</a>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemCount = sortedFiles.length + (requestPath !== '/' && requestPath !== '' ? 1 : 0)
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-bar">
|
||||||
|
${itemCount} items
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Search functionality
|
||||||
|
document.getElementById('searchBox').addEventListener('input', function(e) {
|
||||||
|
const searchTerm = e.target.value.toLowerCase();
|
||||||
|
const fileItems = document.querySelectorAll('.file-item:not(.parent-dir)');
|
||||||
|
|
||||||
|
fileItems.forEach(item => {
|
||||||
|
const fileName = item.querySelector('.file-name').textContent.toLowerCase();
|
||||||
|
if (fileName.includes(searchTerm)) {
|
||||||
|
item.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
item.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle keyboard navigation
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'F5') {
|
||||||
|
e.preventDefault();
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveDirectory(res: http.ServerResponse, filePath: fs.PathLike, requestPath: any) {
|
function sortFiles(files: string[], fullPath: string): any[] {
|
||||||
|
return files
|
||||||
|
.map(file => {
|
||||||
|
const filePath = path.join(fullPath, file)
|
||||||
|
let stats
|
||||||
|
try {
|
||||||
|
stats = fs.statSync(filePath)
|
||||||
|
} catch (err) {
|
||||||
|
stats = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: file,
|
||||||
|
isDirectory: stats ? stats.isDirectory() : false,
|
||||||
|
size: stats ? stats.size : 0,
|
||||||
|
mtime: stats ? stats.mtime : null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
// Directories first, then files
|
||||||
|
if (a.isDirectory && !b.isDirectory) return -1
|
||||||
|
if (!a.isDirectory && b.isDirectory) return 1
|
||||||
|
// Then alphabetical by name
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateErrorPage(errorCode: number, errorMessage: string): string {
|
||||||
|
const errorDescriptions: { [key: number]: { title: string; description: string } } = {
|
||||||
|
404: {
|
||||||
|
title: 'Page Not Found',
|
||||||
|
description: 'The file or directory you are looking for could not be found.'
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
title: 'Server Error',
|
||||||
|
description: 'An internal server error occurred while processing your request.'
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
title: 'Access Denied',
|
||||||
|
description: 'You do not have permission to access this resource.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = errorDescriptions[errorCode] || {
|
||||||
|
title: 'Unknown Error',
|
||||||
|
description: 'An unexpected error occurred.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Error ${errorCode}</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 60px 40px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e74c3c;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #7f8c8d;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 25px rgba(79, 172, 254, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.error-container {
|
||||||
|
padding: 40px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="error-container">
|
||||||
|
<div class="error-icon">${errorCode === 404 ? '🔍' : errorCode === 500 ? '⚠️' : errorCode === 403 ? '🚫' : '❌'}</div>
|
||||||
|
<div class="error-code">${errorCode}</div>
|
||||||
|
<div class="error-title">${error.title}</div>
|
||||||
|
<div class="error-description">${error.description}</div>
|
||||||
|
<a href="/" class="back-button">🏠 Go Home</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function serveDirectory(res: http.ServerResponse, filePath: fs.PathLike, requestPath: string) {
|
||||||
fs.readdir(filePath, (err, files) => {
|
fs.readdir(filePath, (err, files) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.writeHead(500)
|
logger.error(`Error reading directory ${filePath}:`, err)
|
||||||
res.end('Error listing directory contents')
|
res.writeHead(500, { 'Content-Type': 'text/html' })
|
||||||
|
res.end(generateErrorPage(500, 'Error listing directory contents'))
|
||||||
} else {
|
} else {
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||||
res.end(generateDirectoryListingHtml(files, requestPath))
|
res.end(generateDirectoryListingHtml(files, requestPath, filePath.toString()))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function serveFile(res: http.ServerResponse, filePath: fs.PathLike) {
|
function serveFile(res: http.ServerResponse, filePath: fs.PathLike) {
|
||||||
const readStream = fs.createReadStream(filePath)
|
const readStream = fs.createReadStream(filePath)
|
||||||
|
|
||||||
|
const ext = path.extname(filePath.toString()).toLowerCase()
|
||||||
|
const contentTypes: { [key: string]: string } = {
|
||||||
|
'.html': 'text/html',
|
||||||
|
'.css': 'text/css',
|
||||||
|
'.js': 'application/javascript',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.pdf': 'application/pdf',
|
||||||
|
'.txt': 'text/plain',
|
||||||
|
'.md': 'text/markdown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = contentTypes[ext] || 'application/octet-stream'
|
||||||
|
res.setHeader('Content-Type', contentType)
|
||||||
|
|
||||||
readStream.pipe(res)
|
readStream.pipe(res)
|
||||||
readStream.on('error', () => {
|
readStream.on('error', err => {
|
||||||
res.writeHead(500)
|
logger.error(`Error reading file ${filePath}:`, err)
|
||||||
res.end('Error reading file')
|
res.writeHead(500, { 'Content-Type': 'text/html' })
|
||||||
|
res.end(generateErrorPage(500, 'Error reading file'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +668,8 @@ class WebServer {
|
|||||||
|
|
||||||
initServer(): void {
|
initServer(): void {
|
||||||
this.#server = http.createServer((req, res) => {
|
this.#server = http.createServer((req, res) => {
|
||||||
const requestPath = req.url?.split('?')[0]
|
const requestPath = req.url?.split('?')[0] || '/'
|
||||||
const filePath = path.join(this.#config.webServerPath, decodeURIComponent(requestPath || ''))
|
const filePath = path.join(this.#config.webServerPath, decodeURIComponent(requestPath))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats = fs.statSync(filePath)
|
const stats = fs.statSync(filePath)
|
||||||
@@ -73,8 +679,9 @@ class WebServer {
|
|||||||
serveFile(res, filePath)
|
serveFile(res, filePath)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.writeHead(404)
|
logger.error(`File not found: ${filePath}`)
|
||||||
res.end('404 Not Found')
|
res.writeHead(404, { 'Content-Type': 'text/html' })
|
||||||
|
res.end(generateErrorPage(404, 'The requested file or directory was not found'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user