diff --git a/example.mjs b/example.mjs index 2c3bd33..9816e4b 100644 --- a/example.mjs +++ b/example.mjs @@ -10,4 +10,4 @@ const filemanager = new Livefiles({ }) await filemanager.ready() -await filemanager.close() +await filemanager.close() \ No newline at end of file diff --git a/index.js b/index.js index 9822fc1..59cd32d 100644 --- a/index.js +++ b/index.js @@ -68,21 +68,22 @@ class Livefiles extends ReadyResource { } } - handleRequest (req, res) { - const urlPath = decodeURIComponent(req.url) - const fullPath = path.join(this.path, urlPath) + handleRequest(req, res) { + const parsedUrl = new URL(req.url, `http://${req.headers.host}`); + const urlPath = decodeURIComponent(parsedUrl.pathname); + const fullPath = path.join(this.path, urlPath); // Basic authentication check if (!this.authenticate(req)) { - res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Filemanager"' }) - res.end('Authentication required.') - return + res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Filemanager"' }); + res.end('Authentication required.'); + return; } if (req.method === 'GET') { - this.handleGetRequest(fullPath, urlPath, res, req) + this.handleGetRequest(fullPath, urlPath, res, req); } else if (req.method === 'POST') { - this.handlePostRequest(req, res, urlPath) + this.handlePostRequest(req, res, urlPath); } } @@ -104,7 +105,7 @@ class Livefiles extends ReadyResource { const stats = await stat(fullPath) if (stats.isDirectory()) { - await this.listDirectory(fullPath, urlPath, res) + await this.listDirectory(fullPath, urlPath, res, req) } else if (stats.isFile()) { this.serveFile(fullPath, req, res) } @@ -222,68 +223,101 @@ class Livefiles extends ReadyResource { } } - async listDirectory (fullPath, urlPath, res) { + async listDirectory (fullPath, urlPath, res, req) { try { const files = await readdir(fullPath, { withFileTypes: true }) // Separate and sort directories and files - const folders = files.filter(file => file.isDirectory()) - const normalFiles = files.filter(file => !file.isDirectory()) - const allFiles = [...folders, ...normalFiles] - - // Process files in batches to avoid blocking - const directoryItems = [] - - for (let i = 0; i < allFiles.length; i += 5) { - const batch = allFiles.slice(i, i + 5) - - const batchResults = await Promise.all(batch.map(async (file) => { - try { - // Check if file is readable - await access(path.join(fullPath, file.name), fs.constants.R_OK) - - const filePath = path.join(urlPath, file.name) - const safeFileName = this.escapeHtml(file.name) - const iconHtml = file.isDirectory() - ? '' - : '' - - const downloadButton = file.isDirectory() - ? `Enter` - : `Download` - - // Get file or folder size (optimized) - let size - if (file.isDirectory()) { - // For directories, calculate size asynchronously - const dirSize = await this.calculateDirectorySize(path.join(fullPath, file.name)) - size = this.formatBytes(dirSize) - } else { - const stats = await stat(path.join(fullPath, file.name)) - size = this.formatBytes(stats.size) + const urlObj = new URL(req.url, `http://${req.headers.host}`); + const sortBy = urlObj.searchParams.get('sort') || 'type'; + const sortOrder = urlObj.searchParams.get('order') || 'asc'; + + const fileItems = await Promise.all(files.map(async (file) => { + try { + const filePath = path.join(fullPath, file.name); + const stats = await stat(filePath); + + return { + name: file.name, + isDirectory: file.isDirectory(), + size: file.isDirectory() ? + await this.calculateDirectorySize(filePath) : + stats.size, + mtime: stats.mtime, + type: this.getFileType(file.name), + displayPath: path.join(urlPath, file.name) + }; + } catch (error) { + return null; + } + })); + + const validFiles = fileItems.filter(item => item !== null); + + + validFiles.sort((a, b) => { + const direction = sortOrder === 'desc' ? -1 : 1; + switch (sortBy) { + case 'name': + return direction * a.name.localeCompare(b.name); + case 'size': + return direction * (a.size - b.size); + case 'date': + return direction * (a.mtime - b.mtime); + case 'type': + if (a.isDirectory !== b.isDirectory) { + return a.isDirectory ? -1 : 1; } - - return `${iconHtml}${safeFileName}${downloadButton}${size}` - } catch { - return null // Skip if not readable - } - })) - - directoryItems.push(...batchResults.filter(item => item !== null)) - - // Yield control to event loop between batches - if (i + 5 < allFiles.length) { - await new Promise(resolve => setImmediate(resolve)) + return direction * a.type.localeCompare(b.type); + default: + return 0; } - } - - const directoryList = directoryItems.join('') - - let createFormHtml = '' + }); + + const fileRows = validFiles.map(file => { + const icon = file.isDirectory ? + '' : + ''; + + const downloadButton = file.isDirectory ? + `
+ Enter + + + + .zip + +
` : + `Download`; + + return ` + + ${icon}${this.escapeHtml(file.name)} +

${this.formatBytes(file.size)}

+ ${downloadButton} + `; + }).join(''); + + // Add sorting controls + const sortingControls = ` +
+ + +
+ `; + + let createFormHtml = ''; if (this.role === 'admin') { createFormHtml = `
-
+