From 313dfe99cf8f763b36f333c5072e2430a6b7941f Mon Sep 17 00:00:00 2001 From: Johannes Zellner Date: Tue, 3 Mar 2020 17:57:28 +0100 Subject: Use custom public folder listing --- frontend/404.html | 2 +- frontend/js/public.js | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++ frontend/public.html | 89 +++++++++++++++++++++++++++++++ package-lock.json | 50 ------------------ package.json | 1 - server.js | 24 +++++---- src/auth.js | 17 ++++-- 7 files changed, 262 insertions(+), 65 deletions(-) create mode 100644 frontend/js/public.js create mode 100644 frontend/public.html diff --git a/frontend/404.html b/frontend/404.html index c660424..15da5c2 100644 --- a/frontend/404.html +++ b/frontend/404.html @@ -9,7 +9,7 @@
-

File not found

+

File or directory not found

diff --git a/frontend/js/public.js b/frontend/js/public.js new file mode 100644 index 0000000..c295a05 --- /dev/null +++ b/frontend/js/public.js @@ -0,0 +1,144 @@ +(function () { + 'use strict'; + + /* global superagent */ + /* global Vue */ + /* global $ */ + /* global filesize */ + + function sanitize(filePath) { + filePath = '/' + filePath; + return filePath.replace(/\/+/g, '/'); + } + + function encode(filePath) { + return filePath.split('/').map(encodeURIComponent).join('/'); + } + + function decode(filePath) { + return filePath.split('/').map(decodeURIComponent).join('/'); + } + + var mimeTypes = { + images: [ '.png', '.jpg', '.jpeg', '.tiff', '.gif' ], + text: [ '.txt', '.md' ], + pdf: [ '.pdf' ], + html: [ '.html', '.htm', '.php' ], + video: [ '.mp4', '.mpg', '.mpeg', '.ogg', '.mkv', '.avi', '.mov' ] + }; + + function getPreviewUrl(entry, basePath) { + var path = '/_admin/img/'; + + if (entry.isDirectory) return path + 'directory.png'; + if (mimeTypes.images.some(function (e) { return entry.filePath.endsWith(e); })) return sanitize(basePath + '/' + entry.filePath); + if (mimeTypes.text.some(function (e) { return entry.filePath.endsWith(e); })) return path +'text.png'; + if (mimeTypes.pdf.some(function (e) { return entry.filePath.endsWith(e); })) return path + 'pdf.png'; + if (mimeTypes.html.some(function (e) { return entry.filePath.endsWith(e); })) return path + 'html.png'; + if (mimeTypes.video.some(function (e) { return entry.filePath.endsWith(e); })) return path + 'video.png'; + + return path + 'unknown.png'; + } + + // simple extension detection, does not work with double extension like .tar.gz + function getExtension(entry) { + if (entry.isFile) return entry.filePath.slice(entry.filePath.lastIndexOf('.') + 1); + return ''; + } + + function loadDirectory() { + app.busy = true; + + var filePath = sanitize(window.location.pathname); + + app.path = filePath; + + superagent.get('/api/files/' + encode(filePath)).query({ access_token: localStorage.accessToken }).end(function (error, result) { + app.busy = false; + + if (result && result.statusCode === 401) return logout(); + if (error) return console.error(error); + + result.body.entries.sort(function (a, b) { return a.isDirectory && b.isFile ? -1 : 1; }); + app.entries = result.body.entries.map(function (entry) { + entry.previewUrl = getPreviewUrl(entry, filePath); + entry.extension = getExtension(entry); + entry.rename = false; + entry.filePathNew = entry.filePath; + return entry; + }); + app.path = filePath; + app.pathParts = decode(filePath).split('/').filter(function (e) { return !!e; }).map(function (e, i, a) { + return { + name: e, + link: '#' + sanitize('/' + a.slice(0, i).join('/') + '/' + e) + }; + }); + }); + } + + function open(row, column, event) { + var fullPath = encode(sanitize(app.path + '/' + row.filePath)); + + if (row.isDirectory) return window.location.href = fullPath; + + app.activeEntry = row; + app.activeEntry.fullPath = fullPath; + app.previewDrawerVisible = true + + // need to wait for DOM element to exist + setTimeout(function () { + $('iframe').on('load', function (e) { + if (!e.target.contentWindow.document.body) return; + + e.target.contentWindow.document.body.style.display = 'flex' + e.target.contentWindow.document.body.style.justifyContent = 'center' + }); + }, 0); + } + + var app = new Vue({ + el: '#app', + data: { + ready: false, + busy: false, + path: '', + previewDrawerVisible: false, + activeEntry: {}, + entries: [] + }, + methods: { + onDownload: function (entry) { + if (entry.isDirectory) return; + window.location.href = encode('/api/files/' + sanitize(this.path + '/' + entry.filePath)) + '?access_token=' + localStorage.accessToken; + }, + prettyDate: function (row, column, cellValue, index) { + var date = new Date(cellValue), + diff = (((new Date()).getTime() - date.getTime()) / 1000), + day_diff = Math.floor(diff / 86400); + + if (isNaN(day_diff) || day_diff < 0) + return; + + return day_diff === 0 && ( + diff < 60 && 'just now' || + diff < 120 && '1 minute ago' || + diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' || + diff < 7200 && '1 hour ago' || + diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') || + day_diff === 1 && 'Yesterday' || + day_diff < 7 && day_diff + ' days ago' || + day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' || + day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' || + Math.round( day_diff / 365 ) + ' years ago'; + }, + prettyFileSize: function (row, column, cellValue, index) { + return filesize(cellValue); + }, + loadDirectory: loadDirectory, + open: open, + } + }); + + loadDirectory(); +})(); \ No newline at end of file diff --git a/frontend/public.html b/frontend/public.html new file mode 100644 index 0000000..2ddf543 --- /dev/null +++ b/frontend/public.html @@ -0,0 +1,89 @@ + + + Surfer + + + + + + + + + + + + + + + + + + +
+ + + + +
+

{{ path }}

+
+ +
+
+ + +
+

+
+ +
+
+ + + + + + + + + + + +
+
+ +
+
+ Folder is empty +
+
+ + +
+ +
+ Download + + Open + +
+
+
+ +
+
+ +
+ + + + + diff --git a/package-lock.json b/package-lock.json index ca6666b..f1d69fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -160,11 +160,6 @@ } } }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=" - }, "bcrypt-pbkdf": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", @@ -1997,51 +1992,6 @@ } } }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "depd": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", - "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" - }, - "http-errors": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", - "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", - "requires": { - "depd": "1.1.1", - "inherits": "2.0.3", - "setprototypeof": "1.0.3", - "statuses": ">= 1.3.1 < 2" - } - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "requires": { - "mime-db": "~1.33.0" - } - } - } - }, "serve-static": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", diff --git a/package.json b/package.json index 9c8c6c5..49f20c1 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "readline-sync": "^1.4.9", "request": "^2.83.0", "safetydance": "^0.1.1", - "serve-index": "^1.9.1", "superagent": "^5.1.3", "underscore": "^1.8.3", "uuid": "^3.2.1", diff --git a/server.js b/server.js index 052e28e..dd7d7cb 100755 --- a/server.js +++ b/server.js @@ -16,7 +16,6 @@ var express = require('express'), multipart = require('./src/multipart'), mkdirp = require('mkdirp'), auth = require('./src/auth.js'), - serveIndex = require('serve-index'), webdav = require('webdav-server').v2, files = require('./src/files.js')(path.resolve(__dirname, process.argv[2] || 'files')); @@ -49,7 +48,7 @@ function setSettings(req, res, next) { // Load the config file try { - console.log(`Using config file: ${CONFIG_FILE}`); + console.log(`Using config file at: ${CONFIG_FILE}`); config = require(CONFIG_FILE); } catch (e) { if (e.code === 'MODULE_NOT_FOUND') console.log(`Config file ${CONFIG_FILE} not found`); @@ -68,7 +67,7 @@ var webdavServer = new webdav.WebDAVServer({ }); webdavServer.setFileSystem('/', new webdav.PhysicalFileSystem(ROOT_FOLDER), function (success) { - console.log(`Mounting ${ROOT_FOLDER} as webdav resource`, success); + if (success) console.log(`Mounting webdav resource from: ${ROOT_FOLDER}`); }); var multipart = multipart({ maxFieldsSize: 2 * 1024, limit: '512mb', timeout: 3 * 60 * 1000 }); @@ -81,7 +80,7 @@ router.get ('/api/tokens', auth.verify, auth.getTokens); router.post ('/api/tokens', auth.verify, auth.createToken); router.delete('/api/tokens/:token', auth.verify, auth.delToken); router.get ('/api/profile', auth.verify, auth.getProfile); -router.get ('/api/files/*', auth.verify, files.get); +router.get ('/api/files/*', auth.verifyIfNeeded, files.get); router.post ('/api/files/*', auth.verify, multipart, files.post); router.put ('/api/files/*', auth.verify, files.put); router.delete('/api/files/*', auth.verify, files.del); @@ -101,17 +100,22 @@ app.use('/', function welcomePage(req, res, next) { if (config.folderListingEnabled || req.path !== '/') return next(); res.status(200).sendFile(path.join(__dirname, '/frontend/welcome.html')); }); -app.use('/', function (req, res, next) { - if (config.folderListingEnabled) return next(); - res.status(404).sendFile(__dirname + '/frontend/404.html'); +app.use('/', function (req, res) { + if (!config.folderListingEnabled) return res.status(404).sendFile(__dirname + '/frontend/404.html'); + + if (!fs.existsSync(path.join(ROOT_FOLDER, req.path))) return res.status(404).sendFile(__dirname + '/frontend/404.html'); + + res.status(200).sendFile(__dirname + '/frontend/public.html'); }); -app.use('/', serveIndex(ROOT_FOLDER, { icons: true })); app.use(lastMile()); var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; - console.log('Surfer listening on http://%s:%s', host, port); - console.log('Using base path', ROOT_FOLDER); + console.log(`Base path: ${ROOT_FOLDER}`); + console.log(); + console.log(`Listening on http://${host}:${port}`); + + auth.init(config); }); diff --git a/src/auth.js b/src/auth.js index a885d49..5f4c777 100644 --- a/src/auth.js +++ b/src/auth.js @@ -19,11 +19,13 @@ const LOGIN_TOKEN_PREFIX = 'login-'; const API_TOKEN_PREFIX = 'api-'; if (AUTH_METHOD === 'ldap') { - console.log('Use ldap auth'); + console.log('Using ldap auth'); } else { - console.log(`Use local auth file ${LOCAL_AUTH_FILE}`); + console.log(`Using local auth file at: ${LOCAL_AUTH_FILE}`); } +var gConfig = {}; + var tokenStore = { data: {}, save: function () { @@ -53,7 +55,7 @@ var tokenStore = { // load token store data if any try { - console.log(`Using tokenstore file: ${TOKENSTORE_FILE}`); + console.log(`Using tokenstore file at: ${TOKENSTORE_FILE}`); tokenStore.data = JSON.parse(fs.readFileSync(TOKENSTORE_FILE, 'utf-8')); } catch (e) { // start with empty token store @@ -103,6 +105,10 @@ function verifyUser(username, password, callback) { } } +exports.init = function (config) { + gConfig = config; +}; + exports.login = function (req, res, next) { verifyUser(req.body.username, req.body.password, function (error, user) { if (error) return next(new HttpError(401, 'Invalid credentials')); @@ -130,6 +136,11 @@ exports.verify = function (req, res, next) { }; +exports.verifyIfNeeded = function (req, res, next) { + if (!gConfig.folderListingEnabled) return exports.verify(req, res, next); + next(); +}; + exports.logout = function (req, res, next) { var accessToken = req.query.access_token || req.body.accessToken; -- cgit v1.2.3