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..8ca1f78 100644 --- a/index.js +++ b/index.js @@ -4,6 +4,7 @@ const path = require('path') const qs = require('querystring') const ReadyResource = require('ready-resource') const { promisify } = require('util') +const archiver = require('archiver') const { base64Logo } = require('./logo.js') @@ -49,6 +50,57 @@ class Livefiles extends ReadyResource { this.streamBufferSize = opts.streamBufferSize || 64 * 1024 // 64KB chunks } + async createZipArchive(dirPath, outputStream) { + return new Promise((resolve, reject) => { + try { + const archive = archiver('zip', { + zlib: { level: 5 } + }); + + archive.on('error', err => { + this.logger.log({ type: 3, msg: `Archive error: ${err.message}` }); + reject(err); + }); + + archive.on('warning', err => { + if (err.code === 'ENOENT') { + this.logger.log({ type: 2, msg: `Archive warning: ${err.message}` }); + } else { + this.logger.log({ type: 3, msg: `Archive warning: ${err.message}` }); + reject(err); + } + }); + + archive.on('end', () => { + this.logger.log({ type: 1, msg: `Archive created for: ${dirPath}` }); + resolve(); + }); + + outputStream.on('error', err => { + this.logger.log({ type: 3, msg: `Output stream error: ${err.message}` }); + reject(err); + }); + + archive.pipe(outputStream); + + archive.directory(dirPath, false, (entryData) => { + try { + fs.accessSync(path.join(dirPath, entryData.name), fs.constants.R_OK); + return entryData; + } catch (err) { + this.logger.log({ type: 2, msg: `Skipping inaccessible file: ${entryData.name}` }); + return false; + } + }); + + archive.finalize(); + } catch (err) { + this.logger.log({ type: 3, msg: `Error in createZipArchive: ${err.message}` }); + reject(err); + } + }); + } + async _open () { // initialise local http server this.server = http.createServer(this.handleRequest.bind(this)) @@ -69,7 +121,8 @@ class Livefiles extends ReadyResource { } handleRequest (req, res) { - const urlPath = decodeURIComponent(req.url) + const [rawPath] = req.url.split('?') + const urlPath = decodeURIComponent(rawPath || '/') const fullPath = path.join(this.path, urlPath) // Basic authentication check @@ -83,6 +136,9 @@ class Livefiles extends ReadyResource { this.handleGetRequest(fullPath, urlPath, res, req) } else if (req.method === 'POST') { this.handlePostRequest(req, res, urlPath) + } else { + res.writeHead(405, {'Content-Type': 'text/plain'}) + res.end('Method Not Allowed') } } @@ -101,21 +157,35 @@ class Livefiles extends ReadyResource { async handleGetRequest (fullPath, urlPath, res, req) { try { - const stats = await stat(fullPath) + const stats = await stat(fullPath); + const cleanUrl = req.url.split('?')[0]; + const isZipDownload = req.url.includes('?download=zip'); if (stats.isDirectory()) { - await this.listDirectory(fullPath, urlPath, res) + if (isZipDownload) { + try { + await access(fullPath, fs.constants.R_OK); + this.serveFile(fullPath, req, res, true); + } catch (err) { + this.logger.log({ type: 3, msg: `Access error for zip download: ${err.message}` }); + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Directory not accessible'); + } + } else { + await this.listDirectory(fullPath, urlPath, res); + } } else if (stats.isFile()) { this.serveFile(fullPath, req, res) } } catch (err) { if (err.code === 'ENOENT') { - res.writeHead(404, { 'Content-Type': 'text/plain' }) - res.end('File Not Found') + this.logger.log({ type: 3, msg: `File not found: ${fullPath}` }); + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('File Not Found'); } else { - this.logger.log({ type: 3, msg: `Error handling GET request: ${err.message}` }) - res.writeHead(500, { 'Content-Type': 'text/plain' }) - res.end('Internal Server Error') + this.logger.log({ type: 3, msg: `Error handling GET request: ${err.message}` }); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Internal Server Error'); } } } @@ -245,11 +315,15 @@ class Livefiles extends ReadyResource { 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) @@ -339,7 +413,7 @@ class Livefiles extends ReadyResource { font-weight: 700; } .nav--icon{ - width: 60px; + width: 80px; } h1 { color: #444; @@ -367,6 +441,12 @@ class Livefiles extends ReadyResource { border-collapse: collapse; border-radius: 15px; background-color: #fff; +} +.download-buttons{ +display: flex; +flex-direction: row; +gap: 4px; +align-items: center; } .table--container{ border: 0.5px solid #bbb; @@ -587,7 +667,31 @@ class Livefiles extends ReadyResource { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] } - serveFile (fullPath, req, res) { + serveFile (fullPath, req, res, isZip = false) { + if (isZip) { + try { + const dirName = path.basename(fullPath); + res.writeHead(200, { + 'Content-Type': 'application/zip', + 'Content-Disposition': `attachment; filename="${dirName}.zip"` + }); + + this.createZipArchive(fullPath, res) + .catch(err => { + this.logger.log({ type: 3, msg: `Error creating zip: ${err.message}` }); + if (!res.headersSent) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Error creating zip archive'); + } + }); + return; + } catch (err) { + this.logger.log({ type: 3, msg: `Error in zip creation: ${err.message}` }); + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end('Error creating zip archive'); + return; + } + } const extension = path.extname(fullPath).toLowerCase() const contentType = this.getContentType(extension) || 'application/octet-stream' diff --git a/package.json b/package.json index 3d004e1..c4e885e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "license": "GPLv3", "description": "A dead simple Nodejs file manager.", "dependencies": { + "archiver": "^7.0.1", "bare-utils": "^1.5.1", "fs": "npm:bare-fs@^4.2.1", "http": "npm:bare-http1@^4.0.2",