import http from 'node:http'
import path from 'node:path'
import picgo from '@core/picgo'
import logger from '@core/picgo/logger'
import fs from 'fs-extra'
import type { IStringKeyMap } from '#/types/types'
import { encodeFilePath } from '~/utils/common'
import { configPaths } from '~/utils/configPaths'
const defaultPath = process.platform === 'win32' ? 'C:\\Users' : '/'
function getFileIcon(fileName: string, isDirectory: boolean): string {
if (isDirectory)
return ``
const ext = path.extname(fileName).toLowerCase()
if (['.txt', '.md', '.readme'].includes(ext)) {
return ``
}
if (['.js', '.jsx', '.ts', '.tsx', '.html', '.css', '.json'].includes(ext)) {
return ``
}
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp'].includes(ext)) {
return ``
}
if (['.mp4', '.avi', '.mov', '.wmv', '.mkv'].includes(ext)) {
return ``
}
if (['.mp3', '.wav', '.flac', '.aac'].includes(ext)) {
return ``
}
if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
return ``
}
if (ext === '.pdf') {
return ``
}
if (['.exe', '.msi', '.app', '.deb', '.rpm'].includes(ext)) {
return ``
}
return ``
}
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 = `
${requestPath || 'Home'} - File Browser
File Browser - ${requestPath || 'Home'}
`
if (requestPath !== '/' && requestPath !== '') {
const parentPath = path.dirname(requestPath)
html += `
.. (Up to parent directory)
`
}
if (sortedFiles.length === 0) {
html += `
`
} 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 += `
${icon}
${fileInfo.name}
${modifiedDate}
${sizeDisplay}
`
})
}
const itemCount = sortedFiles.length + (requestPath !== '/' && requestPath !== '' ? 1 : 0)
html += `
${itemCount} items
`
return html
}
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 `
Error ${errorCode}
${errorCode === 404 ? '🔍' : errorCode === 500 ? '⚠️' : errorCode === 403 ? '🚫' : '❌'}
${errorCode}
${error.title}
${error.description}
🏠 Go Home
`
}
function serveDirectory(res: http.ServerResponse, filePath: fs.PathLike, requestPath: string) {
fs.readdir(filePath, (err, files) => {
if (err) {
logger.error(`Error reading directory ${filePath}:`, err)
res.writeHead(500, { 'Content-Type': 'text/html' })
res.end(generateErrorPage(500, 'Error listing directory contents'))
} else {
res.writeHead(200, { 'Content-Type': 'text/html' })
res.end(generateDirectoryListingHtml(files, requestPath, filePath.toString()))
}
})
}
function serveFile(res: http.ServerResponse, filePath: fs.PathLike) {
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.on('error', err => {
logger.error(`Error reading file ${filePath}:`, err)
res.writeHead(500, { 'Content-Type': 'text/html' })
res.end(generateErrorPage(500, 'Error reading file'))
})
}
class WebServer {
#server!: http.Server
#config!: IStringKeyMap
constructor() {
this.loadConfig()
this.initServer()
}
loadConfig(): void {
this.#config = {
enableWebServer: picgo.getConfig(configPaths.settings.enableWebServer) || false,
webServerHost: picgo.getConfig(configPaths.settings.webServerHost) || '0.0.0.0',
webServerPort: picgo.getConfig(configPaths.settings.webServerPort) || 37777,
webServerPath: picgo.getConfig(configPaths.settings.webServerPath) || defaultPath
}
}
initServer(): void {
this.#server = http.createServer((req, res) => {
const requestPath = req.url?.split('?')[0] || '/'
const filePath = path.join(this.#config.webServerPath, decodeURIComponent(requestPath))
try {
const stats = fs.statSync(filePath)
if (stats.isDirectory()) {
serveDirectory(res, filePath, requestPath)
} else {
serveFile(res, filePath)
}
} catch (err) {
logger.error(`File not found: ${filePath}`)
res.writeHead(404, { 'Content-Type': 'text/html' })
res.end(generateErrorPage(404, 'The requested file or directory was not found'))
}
})
}
start() {
if (this.#config.enableWebServer) {
this.#server
.listen(
this.#config.webServerPort === 36699 ? 37777 : this.#config.webServerPort,
this.#config.webServerHost,
() => {
logger.info(
`Web server is running at http://${this.#config.webServerHost}:${this.#config.webServerPort}, root path is ${this.#config.webServerPath}`
)
}
)
.on('error', err => {
logger.error(err)
})
} else {
logger.info('Web server is not enabled')
}
}
stop() {
this.#server.close(() => {
logger.info('Web server is stopped')
})
}
restart() {
this.stop()
this.loadConfig()
this.initServer()
this.start()
}
}
export default new WebServer()