From 99a074a974d8456325f7d9022750aa4c33663b9b Mon Sep 17 00:00:00 2001 From: gor_down Date: Mon, 14 Oct 2024 21:21:19 -0300 Subject: [PATCH] file browser --- app.js | 6 +- package-lock.json | 17 + package.json | 2 + public/images/icons/small/folder.ase | Bin 581 -> 0 bytes public/styles/public_list/index.css | 4 + src/utils/serve_index.js | 526 +++++++++++++++++++++++++++ views/layouts/main.hbs | 36 +- views/list.hbs | 25 ++ views/partials/head.hbs | 34 ++ views/partials/mypartial.hbs | 1 - 10 files changed, 612 insertions(+), 39 deletions(-) delete mode 100644 public/images/icons/small/folder.ase create mode 100644 public/styles/public_list/index.css create mode 100644 src/utils/serve_index.js create mode 100644 views/list.hbs create mode 100644 views/partials/head.hbs delete mode 100644 views/partials/mypartial.hbs diff --git a/app.js b/app.js index b88f4d1..78b960d 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,7 @@ const express = require('express') const app = express() const port = 3003 -const serveIndex = require('serve-index') +const serveIndex = require('./src/utils/serve_index') const {engine} = require('express-handlebars') const indexRouter = require('./src/router/indexRouter') @@ -9,7 +9,7 @@ const publicPath = "/public" const inUrlPath = "public" const path = require('path'); - +const { template } = require('handlebars') app.engine('.hbs', engine({extname: '.hbs', helpers: { ifDivisibleBy: function (index, divisor, options) { @@ -21,9 +21,9 @@ app.engine('.hbs', engine({extname: '.hbs', helpers: { app.set('view engine', '.hbs'); app.use(publicPath, express.static(inUrlPath), serveIndex(inUrlPath, { - icons: true })) + app.listen(port, () => { console.log(`Listening on port ${port}`) }) diff --git a/package-lock.json b/package-lock.json index 1d2522e..d2bb9e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "byte-size": "^9.0.0", "csv": "^6.3.10", "express": "^4.21.0", "express-handlebars": "^8.0.1", "hbs": "^4.2.0", + "japanese-date-converter": "^2.0.0", "serve-index": "^1.9.1" } }, @@ -121,6 +123,15 @@ "balanced-match": "^1.0.0" } }, + "node_modules/byte-size": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-9.0.0.tgz", + "integrity": "sha512-xrJ8Hki7eQ6xew55mM6TG9zHI852OoAHcPfduWWtR6yxk2upTuIZy13VioRBDyHReHDdbeDPifUboeNkK/sXXA==", + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -718,6 +729,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/japanese-date-converter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/japanese-date-converter/-/japanese-date-converter-2.0.0.tgz", + "integrity": "sha512-O8wNRmrG05giGcAUUOuzplU24wp57kOxRMMOrwOCnFVQBoaOCxmnsVouVgjtcEuQzTj0/Y31Iuy5Hq2qzTWbCw==", + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.0.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", diff --git a/package.json b/package.json index 0847940..30ab3ac 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ "license": "ISC", "description": "", "dependencies": { + "byte-size": "^9.0.0", "csv": "^6.3.10", "express": "^4.21.0", "express-handlebars": "^8.0.1", "hbs": "^4.2.0", + "japanese-date-converter": "^2.0.0", "serve-index": "^1.9.1" } } diff --git a/public/images/icons/small/folder.ase b/public/images/icons/small/folder.ase deleted file mode 100644 index 1321f25cf9c6528f33538b5a5475d517a0391195..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 581 zcmcK1PbhT5lV;`axAyAU>GS?Qa~nmTwhdCmM-_vJ4DihyHS_o{Pv=a2 zeVBe%?^!8P)2qtgO--aswV}JxfdP9h#(fR=I9h>sn*n@3TgBvO9j?4~VCdL_^-E#& z1Z&XiF{9OCN2hZfE&dto2|r_hLc`vx7rb~#p~ewO?C#_2bQFW35O({#7-@>(j@62H zUHy1Fb%^ohHjIr}v=DbO>2FxS9GBn*3M58`UXwVp925@ diff --git a/public/styles/public_list/index.css b/public/styles/public_list/index.css new file mode 100644 index 0000000..15c754f --- /dev/null +++ b/public/styles/public_list/index.css @@ -0,0 +1,4 @@ +ul{ + list-style-type: none; +} + diff --git a/src/utils/serve_index.js b/src/utils/serve_index.js new file mode 100644 index 0000000..5517a12 --- /dev/null +++ b/src/utils/serve_index.js @@ -0,0 +1,526 @@ +/*! + * serve-index + * Copyright(c) 2011 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * Copyright(c) 2014-2015 Douglas Christopher Wilson + * MIT Licensed + */ + +'use strict'; + +/** + * Module dependencies. + * @private + */ + + +var accepts = require('accepts'); +var createError = require('http-errors'); +var debug = require('debug')('serve-index'); +var escapeHtml = require('escape-html'); +var fs = require('fs') + , path = require('path') + , normalize = path.normalize + , sep = path.sep + , extname = path.extname + , join = path.join; +var Batch = require('batch'); +var mime = require('mime-types'); +var parseUrl = require('parseurl'); +const { JapaneseDateConverter } = require('japanese-date-converter'); +var resolve = require('path').resolve; +const byteSize = require('byte-size') + +/** + * Module exports. + * @public + */ + +module.exports = serveIndex; + +/*! + * Icon cache. + */ + +var cache = {}; + +/*! + * Default template. + */ + +var defaultTemplate = join(__dirname, 'public', 'directory.html'); + +/*! + * . + */ + + +/** + * Media types and the map for content negotiation. + */ + +var mediaTypes = [ + 'text/html', + 'text/plain', + 'application/json' +]; + +var mediaType = { + 'text/html': 'html', + 'text/plain': 'plain', + 'application/json': 'json' +}; + +/** + * Serve directory listings with the given `root` path. + * + * See Readme.md for documentation of options. + * + * @param {String} root + * @param {Object} options + * @return {Function} middleware + * @public + */ + +function serveIndex(root, options) { + var opts = options || {}; + + // root required + if (!root) { + throw new TypeError('serveIndex() root path required'); + } + + // resolve root to absolute and normalize + var rootPath = normalize(resolve(root) + sep); + + var filter = opts.filter; + var hidden = opts.hidden; + var icons = opts.icons; + var template = opts.template || defaultTemplate; + var view = opts.view || 'tiles'; + + return function (req, res, next) { + if (req.method !== 'GET' && req.method !== 'HEAD') { + res.statusCode = 'OPTIONS' === req.method ? 200 : 405; + res.setHeader('Allow', 'GET, HEAD, OPTIONS'); + res.setHeader('Content-Length', '0'); + res.end(); + return; + } + + // parse URLs + var url = parseUrl(req); + var originalUrl = parseUrl.original(req); + var dir = decodeURIComponent(url.pathname); + var originalDir = decodeURIComponent(originalUrl.pathname); + + // join / normalize from root dir + var path = normalize(join(rootPath, dir)); + + // null byte(s), bad request + if (~path.indexOf('\0')) return next(createError(400)); + + // malicious path + if ((path + sep).substr(0, rootPath.length) !== rootPath) { + debug('malicious path "%s"', path); + return next(createError(403)); + } + + // determine ".." display + var showUp = normalize(resolve(path) + sep) !== rootPath; + + // check if we have a directory + debug('stat "%s"', path); + fs.stat(path, function(err, stat){ + if (err && err.code === 'ENOENT') { + return next(); + } + + if (err) { + err.status = err.code === 'ENAMETOOLONG' + ? 414 + : 500; + return next(err); + } + + if (!stat.isDirectory()) return next(); + + // fetch files + debug('readdir "%s"', path); + fs.readdir(path, function(err, files){ + if (err) return next(err); + if (!hidden) files = removeHidden(files); + if (filter) files = files.filter(function(filename, index, list) { + return filter(filename, index, list, path); + }); + files.sort(); + + // content-negotiation + var accept = accepts(req); + var type = accept.type(mediaTypes); + + // not acceptable + if (!type) return next(createError(406)); + serveIndex[mediaType[type]](req, res, files, next, originalDir, showUp, icons, path, view, template, ); + }); + }); + }; +}; + +/** + * Respond with text/html. + */ + +serveIndex.html = function _html(req, res, files, next, dir, showUp, _icons, path, view, template, ) { + + if (showUp) { + files.unshift('..'); + } + + // stat all files + stat(path, files, function (err, stats) { + if (err) return next(err); + + // combine the stats into the file list + var fileList = files.map(function (file, i) { + const stat = stats[i] + const date = new Date(stat.mtimeMs) + const japanese_date = new JapaneseDateConverter({ + inputValue: date.toISOString().split('T')[0], + settings: { + format: date.toISOString().split('T')[0], + format: 'yyyy年M月dd日' + } + }).execute() + const time = date.getHours() + '時' + date.getMinutes() + '分' + const date_string = japanese_date + ' - ' + time + const size = stat.isDirectory() ? null : byteSize(stat.size).toString() + return { name: file, + stat, + size, + modificationDate: date_string, + icon: stat.isDirectory() ? { className: 'icon-directory', fileName: icons.folder } : iconLookup(path + file)} + }); + + // sort file list + fileList.sort(fileSort); + + // read + if (err) return next(err); + + // create locals for rendering + var locals = { + directory: dir, + fileList: fileList, + path: path, + }; + + send(res, locals) + }); +}; + +/** + * Respond with application/json. + */ + +// serveIndex.json = function _json(req, res, files) { +// send(res, 'application/json', JSON.stringify(files)) +// }; + +// /** +// * Respond with text/plain. +// */ + +// serveIndex.plain = function _plain(req, res, files) { +// send(res, 'text/plain', (files.join('\n') + '\n')) +// }; + +function fileSort(a, b) { + // sort ".." to the top + if (a.name === '..' || b.name === '..') { + return a.name === b.name ? 0 + : a.name === '..' ? -1 : 1; + } + + return Number(b.stat && b.stat.isDirectory()) - Number(a.stat && a.stat.isDirectory()) || + String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase()); +} + +/** + * Map html `dir`, returning a linked path. + */ + +function htmlPath(dir) { + var parts = dir.split('/'); + var crumb = new Array(parts.length); + + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + + if (part) { + parts[i] = encodeURIComponent(part); + crumb[i] = '' + escapeHtml(part) + ''; + } + } + + return crumb.join(' / '); +} + +/** + * Get the icon data for the file name. + */ + +function iconLookup(filename) { + var ext = extname(filename); + + // try by extension + if (icons[ext]) { + return { + className: 'icon-' + ext.substring(1), + fileName: icons[ext] + }; + } + + var mimetype = mime.lookup(ext); + + // default if no mime type + if (!mimetype) { + return { + className: 'icon-default', + fileName: icons.default + }; + } + + // try by mime type + if (icons[mimetype]) { + return { + className: 'icon-' + mimetype.replace('/', '-'), + fileName: icons[mimetype] + }; + } + + var suffix = mimetype.split('+')[1]; + + if (suffix && icons['+' + suffix]) { + return { + className: 'icon-' + suffix, + fileName: icons['+' + suffix] + }; + } + + var type = mimetype.split('/')[0]; + + // try by type only + if (icons[type]) { + return { + className: 'icon-' + type, + fileName: icons[type] + }; + } + + return { + className: 'icon-default', + fileName: icons.default + }; +} + +/** + * Load icon images, return css string. + */ + +/** + * Load and cache the given `icon`. + * + * @param {String} icon + * @return {String} + * @api private + */ + +/** + * Normalizes the path separator from system separator + * to URL separator, aka `/`. + * + * @param {String} path + * @return {String} + * @api private + */ + +function normalizeSlashes(path) { + return path.split(sep).join('/'); +}; + +/** + * Filter "hidden" `files`, aka files + * beginning with a `.`. + * + * @param {Array} files + * @return {Array} + * @api private + */ + +function removeHidden(files) { + return files.filter(function(file){ + return '.' != file[0]; + }); +} + +/** + * Send a response. + * @private + */ + +function send (res, locals) { + // security header for content sniffing + res.setHeader('X-Content-Type-Options', 'nosniff') + res.render("list", { + locals, + stylesheet: '/public/styles/public_list/index.css', + title: "Listing " + locals.directory + + }) +} + +/** + * Stat all files and return array of stat + * in same order. + */ + +function stat(dir, files, cb) { + var batch = new Batch(); + + batch.concurrency(10); + + files.forEach(function(file){ + batch.push(function(done){ + fs.stat(join(dir, file), function(err, stat){ + if (err && err.code !== 'ENOENT') return done(err); + + // pass ENOENT as null stat, not error + done(null, stat || null); + }); + }); + }); + + batch.end(cb); +} + +/** + * Icon map. + */ + +var icons = { + // base icons + 'default': 'file.png', + 'folder': 'folder.png', + + // generic mime type icons + 'image': 'image.png', + 'text': 'text.png', + 'video': 'image.png', + + // generic mime suffix icons + '+json': 'text_program.png', + '+xml': 'text_program.png', + '+zip': 'file.png', + + // specific mime type icons + 'application/font-woff': 'file.png', + 'application/javascript': 'text_program.png', + 'application/json': 'file.png', + 'application/msword': 'text.png', + 'application/pdf': 'text.png', + 'application/postscript': 'page_white_vector.png', + 'application/rtf': 'text.png', + 'application/vnd.ms-excel': 'page_white_excel.png', + 'application/vnd.ms-powerpoint': 'page_white_powerpoint.png', + 'application/vnd.oasis.opendocument.presentation': 'page_white_powerpoint.png', + 'application/vnd.oasis.opendocument.spreadsheet': 'page_white_excel.png', + 'application/vnd.oasis.opendocument.text': 'page_white_word.png', + 'application/x-7z-compressed': 'file.png', + 'application/x-sh': 'application_xp_terminal.png', + 'application/x-font-ttf': 'file.png', + 'application/x-msaccess': 'list.png', + 'application/x-shockwave-flash': 'file.png', + 'application/x-sql': 'file.png', + 'application/x-tar': 'file.png', + 'application/x-xz': 'file.png', + 'application/xml': 'list.png', + 'application/zip': 'file.png', + 'image/svg+xml': 'page_white_vector.png', + 'text/css': 'text_program.png', + 'text/html': 'text_program.png', + 'text/less': 'text_program.png', + + // other, extension-specific icons + '.accdb': 'list.png', + '.apk': 'file.png', + '.app': 'application_xp.png', + '.as': 'page_white_actionscript.png', + '.asc': 'lock.png', + '.asp': 'page_white_code.png', + '.aspx': 'page_white_code.png', + '.avb': 'kiki.png', + '.AVB': 'kiki.png', + '.bat': 'application_xp_terminal.png', + '.bgb': 'bgb.png', + '.BGB': 'bgb.png', + '.bz2': 'box.png', + '.c': 'page_white_c.png', + '.cab': 'box.png', + '.cfm': 'page_white_coldfusion.png', + '.clj': 'page_white_code.png', + '.cc': 'page_white_cplusplus.png', + '.cgi': 'application_xp_terminal.png', + '.cpp': 'page_white_cplusplus.png', + '.cs': 'page_white_csharp.png', + '.csv': 'list.png', + '.db': 'page_white_database.png', + '.dbf': 'page_white_database.png', + '.deb': 'box.png', + '.dll': 'page_white_gear.png', + '.dmg': 'drive.png', + '.docx': 'page_white_word.png', + '.erb': 'page_white_ruby.png', + '.exe': 'application_xp.png', + '.fnt': 'font.png', + '.gam': 'controller.png', + '.gz': 'box.png', + '.h': 'page_white_h.png', + '.ini': 'page_white_gear.png', + '.iso': 'cd.png', + '.jar': 'box.png', + '.java': 'page_white_cup.png', + '.jsp': 'page_white_cup.png', + '.lua': 'page_white_code.png', + '.lz': 'box.png', + '.lzma': 'box.png', + '.m': 'page_white_code.png', + '.map': 'map.png', + '.msi': 'box.png', + '.mv4': 'film.png', + '.otf': 'font.png', + '.pdb': 'page_white_database.png', + '.php': 'page_white_php.png', + '.pl': 'page_white_code.png', + '.pkg': 'box.png', + '.pptx': 'page_white_powerpoint.png', + '.psd': 'page_white_picture.png', + '.py': 'page_white_code.png', + '.rar': 'box.png', + '.rb': 'page_white_ruby.png', + '.rm': 'film.png', + '.rom': 'controller.png', + '.rpm': 'box.png', + '.sass': 'page_white_code.png', + '.sav': 'controller.png', + '.scss': 'page_white_code.png', + '.srt': 'page_white_text.png', + '.tbz2': 'box.png', + '.tgz': 'box.png', + '.tlz': 'box.png', + '.vb': 'page_white_code.png', + '.vbs': 'page_white_code.png', + '.xcf': 'page_white_picture.png', + '.xlsx': 'page_white_excel.png', + '.yaws': 'page_white_code.png' +}; diff --git a/views/layouts/main.hbs b/views/layouts/main.hbs index ed38bbe..c92fccf 100644 --- a/views/layouts/main.hbs +++ b/views/layouts/main.hbs @@ -1,41 +1,7 @@ - - - - - - - - - - - - {{#if header_style}} - - {{else}} - - {{/if}} - - - - {{#if stylesheet}} - - {{/if}} - - {{#if favicon}} - - {{else}} - - {{/if}} - - {{#if title}} - {{title}} - {{else}} - Lyrical Tokarev~ 世界最後の日 - {{/if}} - + {{> head }} {{#if headerPartial}} diff --git a/views/list.hbs b/views/list.hbs new file mode 100644 index 0000000..d268c86 --- /dev/null +++ b/views/list.hbs @@ -0,0 +1,25 @@ + +
+

~ {{locals.directory}}

+ + + + + + + + {{#each locals.fileList}} + + + + + + {{/each}} + +
FilenameSizeLast Updated
+ {{name}} + + {{size}} + {{modificationDate}}
+
+ \ No newline at end of file diff --git a/views/partials/head.hbs b/views/partials/head.hbs new file mode 100644 index 0000000..6004d71 --- /dev/null +++ b/views/partials/head.hbs @@ -0,0 +1,34 @@ + + + + + + + + + + + + {{#if header_style}} + + {{else}} + + {{/if}} + + + + {{#if stylesheet}} + + {{/if}} + + {{#if favicon}} + + {{else}} + + {{/if}} + + {{#if title}} + {{title}} + {{else}} + Lyrical Tokarev~ 世界最後の日 + {{/if}} \ No newline at end of file diff --git a/views/partials/mypartial.hbs b/views/partials/mypartial.hbs deleted file mode 100644 index 3428bad..0000000 --- a/views/partials/mypartial.hbs +++ /dev/null @@ -1 +0,0 @@ -

Nigger

\ No newline at end of file