aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJohannes Zellner <johannes@cloudron.io>2020-03-03 17:57:28 +0100
committerJohannes Zellner <johannes@cloudron.io>2020-03-03 17:57:28 +0100
commit313dfe99cf8f763b36f333c5072e2430a6b7941f (patch)
tree520922a7f374b816901345fb087d66868eeff954
parent33ee47f3ca0322ccb19153cafa1c1f7b886dc61a (diff)
downloadSurfer-313dfe99cf8f763b36f333c5072e2430a6b7941f.tar.gz
Surfer-313dfe99cf8f763b36f333c5072e2430a6b7941f.tar.zst
Surfer-313dfe99cf8f763b36f333c5072e2430a6b7941f.zip
Use custom public folder listing
-rw-r--r--frontend/404.html2
-rw-r--r--frontend/js/public.js144
-rw-r--r--frontend/public.html89
-rw-r--r--package-lock.json50
-rw-r--r--package.json1
-rwxr-xr-xserver.js24
-rw-r--r--src/auth.js17
7 files changed, 262 insertions, 65 deletions
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 @@
9<body> 9<body>
10 10
11<div class="container-center"> 11<div class="container-center">
12 <p>File not found</p> 12 <p>File or directory not found</p>
13</div> 13</div>
14 14
15</body> 15</body>
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 @@
1(function () {
2 'use strict';
3
4 /* global superagent */
5 /* global Vue */
6 /* global $ */
7 /* global filesize */
8
9 function sanitize(filePath) {
10 filePath = '/' + filePath;
11 return filePath.replace(/\/+/g, '/');
12 }
13
14 function encode(filePath) {
15 return filePath.split('/').map(encodeURIComponent).join('/');
16 }
17
18 function decode(filePath) {
19 return filePath.split('/').map(decodeURIComponent).join('/');
20 }
21
22 var mimeTypes = {
23 images: [ '.png', '.jpg', '.jpeg', '.tiff', '.gif' ],
24 text: [ '.txt', '.md' ],
25 pdf: [ '.pdf' ],
26 html: [ '.html', '.htm', '.php' ],
27 video: [ '.mp4', '.mpg', '.mpeg', '.ogg', '.mkv', '.avi', '.mov' ]
28 };
29
30 function getPreviewUrl(entry, basePath) {
31 var path = '/_admin/img/';
32
33 if (entry.isDirectory) return path + 'directory.png';
34 if (mimeTypes.images.some(function (e) { return entry.filePath.endsWith(e); })) return sanitize(basePath + '/' + entry.filePath);
35 if (mimeTypes.text.some(function (e) { return entry.filePath.endsWith(e); })) return path +'text.png';
36 if (mimeTypes.pdf.some(function (e) { return entry.filePath.endsWith(e); })) return path + 'pdf.png';
37 if (mimeTypes.html.some(function (e) { return entry.filePath.endsWith(e); })) return path + 'html.png';
38 if (mimeTypes.video.some(function (e) { return entry.filePath.endsWith(e); })) return path + 'video.png';
39
40 return path + 'unknown.png';
41 }
42
43 // simple extension detection, does not work with double extension like .tar.gz
44 function getExtension(entry) {
45 if (entry.isFile) return entry.filePath.slice(entry.filePath.lastIndexOf('.') + 1);
46 return '';
47 }
48
49 function loadDirectory() {
50 app.busy = true;
51
52 var filePath = sanitize(window.location.pathname);
53
54 app.path = filePath;
55
56 superagent.get('/api/files/' + encode(filePath)).query({ access_token: localStorage.accessToken }).end(function (error, result) {
57 app.busy = false;
58
59 if (result && result.statusCode === 401) return logout();
60 if (error) return console.error(error);
61
62 result.body.entries.sort(function (a, b) { return a.isDirectory && b.isFile ? -1 : 1; });
63 app.entries = result.body.entries.map(function (entry) {
64 entry.previewUrl = getPreviewUrl(entry, filePath);
65 entry.extension = getExtension(entry);
66 entry.rename = false;
67 entry.filePathNew = entry.filePath;
68 return entry;
69 });
70 app.path = filePath;
71 app.pathParts = decode(filePath).split('/').filter(function (e) { return !!e; }).map(function (e, i, a) {
72 return {
73 name: e,
74 link: '#' + sanitize('/' + a.slice(0, i).join('/') + '/' + e)
75 };
76 });
77 });
78 }
79
80 function open(row, column, event) {
81 var fullPath = encode(sanitize(app.path + '/' + row.filePath));
82
83 if (row.isDirectory) return window.location.href = fullPath;
84
85 app.activeEntry = row;
86 app.activeEntry.fullPath = fullPath;
87 app.previewDrawerVisible = true
88
89 // need to wait for DOM element to exist
90 setTimeout(function () {
91 $('iframe').on('load', function (e) {
92 if (!e.target.contentWindow.document.body) return;
93
94 e.target.contentWindow.document.body.style.display = 'flex'
95 e.target.contentWindow.document.body.style.justifyContent = 'center'
96 });
97 }, 0);
98 }
99
100 var app = new Vue({
101 el: '#app',
102 data: {
103 ready: false,
104 busy: false,
105 path: '',
106 previewDrawerVisible: false,
107 activeEntry: {},
108 entries: []
109 },
110 methods: {
111 onDownload: function (entry) {
112 if (entry.isDirectory) return;
113 window.location.href = encode('/api/files/' + sanitize(this.path + '/' + entry.filePath)) + '?access_token=' + localStorage.accessToken;
114 },
115 prettyDate: function (row, column, cellValue, index) {
116 var date = new Date(cellValue),
117 diff = (((new Date()).getTime() - date.getTime()) / 1000),
118 day_diff = Math.floor(diff / 86400);
119
120 if (isNaN(day_diff) || day_diff < 0)
121 return;
122
123 return day_diff === 0 && (
124 diff < 60 && 'just now' ||
125 diff < 120 && '1 minute ago' ||
126 diff < 3600 && Math.floor( diff / 60 ) + ' minutes ago' ||
127 diff < 7200 && '1 hour ago' ||
128 diff < 86400 && Math.floor( diff / 3600 ) + ' hours ago') ||
129 day_diff === 1 && 'Yesterday' ||
130 day_diff < 7 && day_diff + ' days ago' ||
131 day_diff < 31 && Math.ceil( day_diff / 7 ) + ' weeks ago' ||
132 day_diff < 365 && Math.round( day_diff / 30 ) + ' months ago' ||
133 Math.round( day_diff / 365 ) + ' years ago';
134 },
135 prettyFileSize: function (row, column, cellValue, index) {
136 return filesize(cellValue);
137 },
138 loadDirectory: loadDirectory,
139 open: open,
140 }
141 });
142
143 loadDirectory();
144})(); \ 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 @@
1<html>
2<head>
3 <title> Surfer </title>
4
5 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
6
7 <link rel="icon" type="image/png" href="/_admin/img/logo.png">
8
9 <link rel="stylesheet" href="/_admin/css/theme-chalk_2.11.1.css">
10 <link rel="stylesheet" href="/_admin/css/style.css">
11
12 <script src="/_admin/js/jquery-1.12.1.min.js"></script>
13 <script src="/_admin/js/vue.min.js"></script>
14 <script src="/_admin/js/element-ui_2.11.1.min.js"></script>
15 <script src="/_admin/js/element-ui_en_2.11.1.min.js"></script>
16 <script src="/_admin/js/filesize.min.js"></script>
17 <script src="/_admin/js/superagent.js"></script>
18
19</head>
20<body>
21
22<div id="app">
23
24<el-container>
25 <el-header>
26 <el-row type="flex" justify="space-between">
27 <div style="flex-grow: 2; padding: 0 7px;">
28 <p style="font-size: 24px; margin: 4px 0;">{{ path }}</p>
29 </div>
30 <div>
31 <a href="/_admin">
32 <el-button type="primary" icon="el-icon-user" size="small">Login</el-button>
33 </a>
34 </div>
35 </el-row>
36 </el-header>
37 <el-main>
38
39 <div v-show="busy">
40 <center><h1><i class="el-icon-loading"></i></h1></center>
41 </div>
42
43 <div v-show="!busy && entries.length" v-cloak>
44 <center>
45 <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">
46 <el-table-column prop="previewUrl" label="Type" width="80px" sortable>
47 <template slot-scope="scope">
48 <el-image v-bind:src="scope.row.previewUrl" class="list-icon" style="width: 32px; height: 32px" fit="cover"></el-image>
49 </template>
50 </el-table-column>
51 <el-table-column prop="filePath" label="Name" sortable></el-table-column>
52 <el-table-column prop="size" label="Size" width="150px" sortable :formatter="prettyFileSize"></el-table-column>
53 <el-table-column prop="mtime" label="Modified" width="150px" sortable :formatter="prettyDate"></el-table-column>
54 <el-table-column label="Actions" align="right" width="200px" class-name="list-actions">
55 <template slot-scope="scope">
56 <el-button size="small" icon="el-icon-download" type="text" plain circle v-show="scope.row.isFile" @click.stop="onDownload(scope.row)"></el-button>
57 </template>
58 </el-table-column>
59 </el-table>
60 </center>
61 </div>
62
63 <div v-show="!busy && !entries.length">
64 <center>
65 Folder is empty
66 </center>
67 </div>
68
69 <el-drawer :title="activeEntry.filePath":with-header="false" :visible.sync="previewDrawerVisible" direction="rtl" size="50%">
70 <div style="display: flex; flex-direction: column; height: 100%;">
71 <iframe :src="activeEntry.fullPath" style="width: 100%; height: 100%; border: none; margin: 10px;"></iframe>
72 <center>
73 <el-button size="small" icon="el-icon-download" style="margin: 10px;" @click.stop="onDownload(activeEntry)">Download</el-button>
74 <a :href="activeEntry.fullPath" target="_blank">
75 <el-button size="small" icon="el-icon-link" style="margin: 10px;">Open</el-button>
76 </a>
77 </center>
78 </div>
79 </el-drawer>
80
81 </el-main>
82</el-container>
83
84</div>
85
86<script src="/_admin/js/public.js"></script>
87
88</body>
89</html>
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 @@
160 } 160 }
161 } 161 }
162 }, 162 },
163 "batch": {
164 "version": "0.6.1",
165 "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
166 "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY="
167 },
168 "bcrypt-pbkdf": { 163 "bcrypt-pbkdf": {
169 "version": "1.0.1", 164 "version": "1.0.1",
170 "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", 165 "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
@@ -1997,51 +1992,6 @@
1997 } 1992 }
1998 } 1993 }
1999 }, 1994 },
2000 "serve-index": {
2001 "version": "1.9.1",
2002 "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",
2003 "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=",
2004 "requires": {
2005 "accepts": "~1.3.4",
2006 "batch": "0.6.1",
2007 "debug": "2.6.9",
2008 "escape-html": "~1.0.3",
2009 "http-errors": "~1.6.2",
2010 "mime-types": "~2.1.17",
2011 "parseurl": "~1.3.2"
2012 },
2013 "dependencies": {
2014 "depd": {
2015 "version": "1.1.1",
2016 "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
2017 "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
2018 },
2019 "http-errors": {
2020 "version": "1.6.2",
2021 "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
2022 "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
2023 "requires": {
2024 "depd": "1.1.1",
2025 "inherits": "2.0.3",
2026 "setprototypeof": "1.0.3",
2027 "statuses": ">= 1.3.1 < 2"
2028 }
2029 },
2030 "mime-db": {
2031 "version": "1.33.0",
2032 "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
2033 "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
2034 },
2035 "mime-types": {
2036 "version": "2.1.18",
2037 "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
2038 "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
2039 "requires": {
2040 "mime-db": "~1.33.0"
2041 }
2042 }
2043 }
2044 },
2045 "serve-static": { 1995 "serve-static": {
2046 "version": "1.13.1", 1996 "version": "1.13.1",
2047 "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.1.tgz", 1997 "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 @@
40 "readline-sync": "^1.4.9", 40 "readline-sync": "^1.4.9",
41 "request": "^2.83.0", 41 "request": "^2.83.0",
42 "safetydance": "^0.1.1", 42 "safetydance": "^0.1.1",
43 "serve-index": "^1.9.1",
44 "superagent": "^5.1.3", 43 "superagent": "^5.1.3",
45 "underscore": "^1.8.3", 44 "underscore": "^1.8.3",
46 "uuid": "^3.2.1", 45 "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'),
16 multipart = require('./src/multipart'), 16 multipart = require('./src/multipart'),
17 mkdirp = require('mkdirp'), 17 mkdirp = require('mkdirp'),
18 auth = require('./src/auth.js'), 18 auth = require('./src/auth.js'),
19 serveIndex = require('serve-index'),
20 webdav = require('webdav-server').v2, 19 webdav = require('webdav-server').v2,
21 files = require('./src/files.js')(path.resolve(__dirname, process.argv[2] || 'files')); 20 files = require('./src/files.js')(path.resolve(__dirname, process.argv[2] || 'files'));
22 21
@@ -49,7 +48,7 @@ function setSettings(req, res, next) {
49 48
50// Load the config file 49// Load the config file
51try { 50try {
52 console.log(`Using config file: ${CONFIG_FILE}`); 51 console.log(`Using config file at: ${CONFIG_FILE}`);
53 config = require(CONFIG_FILE); 52 config = require(CONFIG_FILE);
54} catch (e) { 53} catch (e) {
55 if (e.code === 'MODULE_NOT_FOUND') console.log(`Config file ${CONFIG_FILE} not found`); 54 if (e.code === 'MODULE_NOT_FOUND') console.log(`Config file ${CONFIG_FILE} not found`);
@@ -68,7 +67,7 @@ var webdavServer = new webdav.WebDAVServer({
68}); 67});
69 68
70webdavServer.setFileSystem('/', new webdav.PhysicalFileSystem(ROOT_FOLDER), function (success) { 69webdavServer.setFileSystem('/', new webdav.PhysicalFileSystem(ROOT_FOLDER), function (success) {
71 console.log(`Mounting ${ROOT_FOLDER} as webdav resource`, success); 70 if (success) console.log(`Mounting webdav resource from: ${ROOT_FOLDER}`);
72}); 71});
73 72
74var multipart = multipart({ maxFieldsSize: 2 * 1024, limit: '512mb', timeout: 3 * 60 * 1000 }); 73var multipart = multipart({ maxFieldsSize: 2 * 1024, limit: '512mb', timeout: 3 * 60 * 1000 });
@@ -81,7 +80,7 @@ router.get ('/api/tokens', auth.verify, auth.getTokens);
81router.post ('/api/tokens', auth.verify, auth.createToken); 80router.post ('/api/tokens', auth.verify, auth.createToken);
82router.delete('/api/tokens/:token', auth.verify, auth.delToken); 81router.delete('/api/tokens/:token', auth.verify, auth.delToken);
83router.get ('/api/profile', auth.verify, auth.getProfile); 82router.get ('/api/profile', auth.verify, auth.getProfile);
84router.get ('/api/files/*', auth.verify, files.get); 83router.get ('/api/files/*', auth.verifyIfNeeded, files.get);
85router.post ('/api/files/*', auth.verify, multipart, files.post); 84router.post ('/api/files/*', auth.verify, multipart, files.post);
86router.put ('/api/files/*', auth.verify, files.put); 85router.put ('/api/files/*', auth.verify, files.put);
87router.delete('/api/files/*', auth.verify, files.del); 86router.delete('/api/files/*', auth.verify, files.del);
@@ -101,17 +100,22 @@ app.use('/', function welcomePage(req, res, next) {
101 if (config.folderListingEnabled || req.path !== '/') return next(); 100 if (config.folderListingEnabled || req.path !== '/') return next();
102 res.status(200).sendFile(path.join(__dirname, '/frontend/welcome.html')); 101 res.status(200).sendFile(path.join(__dirname, '/frontend/welcome.html'));
103}); 102});
104app.use('/', function (req, res, next) { 103app.use('/', function (req, res) {
105 if (config.folderListingEnabled) return next(); 104 if (!config.folderListingEnabled) return res.status(404).sendFile(__dirname + '/frontend/404.html');
106 res.status(404).sendFile(__dirname + '/frontend/404.html'); 105
106 if (!fs.existsSync(path.join(ROOT_FOLDER, req.path))) return res.status(404).sendFile(__dirname + '/frontend/404.html');
107
108 res.status(200).sendFile(__dirname + '/frontend/public.html');
107}); 109});
108app.use('/', serveIndex(ROOT_FOLDER, { icons: true }));
109app.use(lastMile()); 110app.use(lastMile());
110 111
111var server = app.listen(3000, function () { 112var server = app.listen(3000, function () {
112 var host = server.address().address; 113 var host = server.address().address;
113 var port = server.address().port; 114 var port = server.address().port;
114 115
115 console.log('Surfer listening on http://%s:%s', host, port); 116 console.log(`Base path: ${ROOT_FOLDER}`);
116 console.log('Using base path', ROOT_FOLDER); 117 console.log();
118 console.log(`Listening on http://${host}:${port}`);
119
120 auth.init(config);
117}); 121});
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-';
19const API_TOKEN_PREFIX = 'api-'; 19const API_TOKEN_PREFIX = 'api-';
20 20
21if (AUTH_METHOD === 'ldap') { 21if (AUTH_METHOD === 'ldap') {
22 console.log('Use ldap auth'); 22 console.log('Using ldap auth');
23} else { 23} else {
24 console.log(`Use local auth file ${LOCAL_AUTH_FILE}`); 24 console.log(`Using local auth file at: ${LOCAL_AUTH_FILE}`);
25} 25}
26 26
27var gConfig = {};
28
27var tokenStore = { 29var tokenStore = {
28 data: {}, 30 data: {},
29 save: function () { 31 save: function () {
@@ -53,7 +55,7 @@ var tokenStore = {
53 55
54// load token store data if any 56// load token store data if any
55try { 57try {
56 console.log(`Using tokenstore file: ${TOKENSTORE_FILE}`); 58 console.log(`Using tokenstore file at: ${TOKENSTORE_FILE}`);
57 tokenStore.data = JSON.parse(fs.readFileSync(TOKENSTORE_FILE, 'utf-8')); 59 tokenStore.data = JSON.parse(fs.readFileSync(TOKENSTORE_FILE, 'utf-8'));
58} catch (e) { 60} catch (e) {
59 // start with empty token store 61 // start with empty token store
@@ -103,6 +105,10 @@ function verifyUser(username, password, callback) {
103 } 105 }
104} 106}
105 107
108exports.init = function (config) {
109 gConfig = config;
110};
111
106exports.login = function (req, res, next) { 112exports.login = function (req, res, next) {
107 verifyUser(req.body.username, req.body.password, function (error, user) { 113 verifyUser(req.body.username, req.body.password, function (error, user) {
108 if (error) return next(new HttpError(401, 'Invalid credentials')); 114 if (error) return next(new HttpError(401, 'Invalid credentials'));
@@ -130,6 +136,11 @@ exports.verify = function (req, res, next) {
130 136
131}; 137};
132 138
139exports.verifyIfNeeded = function (req, res, next) {
140 if (!gConfig.folderListingEnabled) return exports.verify(req, res, next);
141 next();
142};
143
133exports.logout = function (req, res, next) { 144exports.logout = function (req, res, next) {
134 var accessToken = req.query.access_token || req.body.accessToken; 145 var accessToken = req.query.access_token || req.body.accessToken;
135 146