]> git.immae.eu Git - perso/Immae/Projets/Nodejs/Surfer.git/commitdiff
Use custom public folder listing
authorJohannes Zellner <johannes@cloudron.io>
Tue, 3 Mar 2020 16:57:28 +0000 (17:57 +0100)
committerJohannes Zellner <johannes@cloudron.io>
Tue, 3 Mar 2020 16:57:28 +0000 (17:57 +0100)
frontend/404.html
frontend/js/public.js [new file with mode: 0644]
frontend/public.html [new file with mode: 0644]
package-lock.json
package.json
server.js
src/auth.js

index c660424d0b5b40a4dc421a85ca71f6647b533089..15da5c2da8628f0aaff1546241de5a1668ccfd99 100644 (file)
@@ -9,7 +9,7 @@
 <body>
 
 <div class="container-center">
-  <p>File not found</p>
+  <p>File or directory not found</p>
 </div>
 
 </body>
diff --git a/frontend/js/public.js b/frontend/js/public.js
new file mode 100644 (file)
index 0000000..c295a05
--- /dev/null
@@ -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 (file)
index 0000000..2ddf543
--- /dev/null
@@ -0,0 +1,89 @@
+<html>
+<head>
+    <title> Surfer </title>
+
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+
+    <link rel="icon" type="image/png" href="/_admin/img/logo.png">
+
+    <link rel="stylesheet" href="/_admin/css/theme-chalk_2.11.1.css">
+    <link rel="stylesheet" href="/_admin/css/style.css">
+
+    <script src="/_admin/js/jquery-1.12.1.min.js"></script>
+    <script src="/_admin/js/vue.min.js"></script>
+    <script src="/_admin/js/element-ui_2.11.1.min.js"></script>
+    <script src="/_admin/js/element-ui_en_2.11.1.min.js"></script>
+    <script src="/_admin/js/filesize.min.js"></script>
+    <script src="/_admin/js/superagent.js"></script>
+
+</head>
+<body>
+
+<div id="app">
+
+<el-container>
+  <el-header>
+    <el-row type="flex" justify="space-between">
+      <div style="flex-grow: 2; padding: 0 7px;">
+        <p style="font-size: 24px; margin: 4px 0;">{{ path }}</p>
+      </div>
+      <div>
+        <a href="/_admin">
+          <el-button type="primary" icon="el-icon-user" size="small">Login</el-button>
+        </a>
+      </div>
+    </el-row>
+  </el-header>
+  <el-main>
+
+    <div v-show="busy">
+      <center><h1><i class="el-icon-loading"></i></h1></center>
+    </div>
+
+    <div v-show="!busy && entries.length" v-cloak>
+      <center>
+        <el-table :data="entries" style="max-width: 1280px; width: 100%" height="100%" empty-text="Folder is emtpy" :default-sort="{ prop: 'filePath', order: 'descending' }" @row-click="open">
+          <el-table-column prop="previewUrl" label="Type" width="80px" sortable>
+            <template slot-scope="scope">
+              <el-image v-bind:src="scope.row.previewUrl" class="list-icon" style="width: 32px; height: 32px" fit="cover"></el-image>
+            </template>
+          </el-table-column>
+          <el-table-column prop="filePath" label="Name" sortable></el-table-column>
+          <el-table-column prop="size" label="Size" width="150px" sortable :formatter="prettyFileSize"></el-table-column>
+          <el-table-column prop="mtime" label="Modified" width="150px" sortable :formatter="prettyDate"></el-table-column>
+          <el-table-column label="Actions" align="right" width="200px" class-name="list-actions">
+            <template slot-scope="scope">
+              <el-button size="small" icon="el-icon-download" type="text" plain circle v-show="scope.row.isFile" @click.stop="onDownload(scope.row)"></el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </center>
+    </div>
+
+    <div v-show="!busy && !entries.length">
+      <center>
+        Folder is empty
+      </center>
+    </div>
+
+    <el-drawer :title="activeEntry.filePath":with-header="false" :visible.sync="previewDrawerVisible" direction="rtl" size="50%">
+      <div style="display: flex; flex-direction: column; height: 100%;">
+        <iframe :src="activeEntry.fullPath" style="width: 100%; height: 100%; border: none; margin: 10px;"></iframe>
+        <center>
+          <el-button size="small" icon="el-icon-download" style="margin: 10px;" @click.stop="onDownload(activeEntry)">Download</el-button>
+          <a :href="activeEntry.fullPath" target="_blank">
+            <el-button size="small" icon="el-icon-link" style="margin: 10px;">Open</el-button>
+          </a>
+        </center>
+      </div>
+    </el-drawer>
+
+  </el-main>
+</el-container>
+
+</div>
+
+<script src="/_admin/js/public.js"></script>
+
+</body>
+</html>
index ca6666b40c966b2cdb00d382f06e221d3b625790..f1d69fb20bdca11dab716422ce2bdb01366cd322 100644 (file)
         }
       }
     },
-    "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",
         }
       }
     },
-    "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",
index 9c8c6c57f423ffb8cc2948a860f0729703d87a74..49f20c13994666918975eddfd5df7ae0728bdde5 100644 (file)
@@ -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",
index 052e28ea39c5f938f6f287b4498b40f78000764d..dd7d7cba8c2c898b8e87dc68259ccb2a2acc820e 100755 (executable)
--- 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);
 });
index a885d492eb2a68cf5f9e572b02fc01188d01ca40..5f4c7776601ceb0cbcd4117212c22c3ee8160382 100644 (file)
@@ -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;