vfs
Massimo Melina committed
Dec 14, 2021 at 12:21 UTC
d6e4bac3522068b25d2ffcac159f73efcffa1dd9
9 files changed
+164
-24
frontend/src/BrowseFiles.ts
+4
-2
@@ -1,14 +1,16 @@
1
import { Link, useLocation } from 'react-router-dom'
2
import { useApi } from './api'
3
import { createElement as h, Fragment } from 'react'
4
-import { formatBytes, hIcon, Loading } from './misc'
4
+import { formatBytes, hError, hIcon, Loading } from './misc'
5
import { Head } from './Head'
6
7
export function BrowseFiles() {
8
- const path = useLocation().pathname
8
+ const path = decodeURI(useLocation().pathname)
9
let res = useApi('file_list', { path })
10
if (!res)
11
return h(Loading)
12
+ if (res instanceof Error)
13
+ return hError(res)
14
const { list } = res
15
return h(Fragment, {},
16
h(Head, { list }),
frontend/src/index.scss
+5
@@ -32,6 +32,11 @@ button {
32
cursor: pointer;
33
}
34
35
+.error-msg {
36
+ background-color: #faa;
37
+ color: #833;
38
+}
39
+
40
#folder-path {
41
margin: 0.2em;
42
& a {
frontend/src/misc.ts
+8
@@ -4,6 +4,10 @@ export function hIcon(name: string) {
4
return h(Icon, { name })
5
}
6
7
+export function hError(err: Error) {
8
+ return h('div', { className:'error-msg' }, err.message)
9
+}
10
+
11
const SYS_ICONS: Record<string,string> = {
12
file: 'description',
13
}
@@ -41,4 +45,8 @@ export function round(v: number, decimals: number = 0) {
45
46
export function prefix(pre:string, v:string|number, post:string='') {
47
return v ? pre+v+post : ''
48
+}
49
+
50
+export function enforceFinal(sub:string, s:string) {
51
+ return s.endsWith(sub) ? s : s+sub
52
}
\ No newline at end of file
package-lock.json
+37
-1
@@ -10,13 +10,16 @@
10
"license": "GPL-3.0",
11
"dependencies": {
12
"@koa/router": "^10.1.1",
13
+ "@types/lodash": "^4.14.178",
14
"fast-glob": "^3.2.7",
15
"koa": "^2.13.4",
16
"koa-better-http-proxy": "^0.2.9",
17
"koa-bodyparser": "^4.3.0",
18
"koa-mount": "^4.0.0",
19
"koa-static": "^5.0.0",
19
- "mime-types": "^2.1.34"
20
+ "lodash": "^4.17.21",
21
+ "mime-types": "^2.1.34",
22
+ "yaml": "^1.10.2"
23
},
24
"devDependencies": {
25
"@types/axios": "^0.14.0",
@@ -316,6 +319,11 @@
319
"@types/koa-send": "*"
320
}
321
},
322
+ "node_modules/@types/lodash": {
323
+ "version": "4.14.178",
324
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
325
+ "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw=="
326
+ },
327
"node_modules/@types/mime": {
328
"version": "1.3.2",
329
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -1711,6 +1719,11 @@
1719
"node": ">=8"
1720
}
1721
},
1722
+ "node_modules/lodash": {
1723
+ "version": "4.17.21",
1724
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
1725
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
1726
+ },
1727
"node_modules/log-symbols": {
1728
"version": "4.1.0",
1729
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -3011,6 +3024,14 @@
3024
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
3025
"dev": true
3026
},
3027
+ "node_modules/yaml": {
3028
+ "version": "1.10.2",
3029
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
3030
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
3031
+ "engines": {
3032
+ "node": ">= 6"
3033
+ }
3034
+ },
3035
"node_modules/yargs": {
3036
"version": "16.2.0",
3037
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
@@ -3340,6 +3361,11 @@
3361
"@types/koa-send": "*"
3362
}
3363
},
3364
+ "@types/lodash": {
3365
+ "version": "4.14.178",
3366
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
3367
+ "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw=="
3368
+ },
3369
"@types/mime": {
3370
"version": "1.3.2",
3371
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -4402,6 +4428,11 @@
4428
"package-json": "^6.3.0"
4429
}
4430
},
4431
+ "lodash": {
4432
+ "version": "4.17.21",
4433
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
4434
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
4435
+ },
4436
"log-symbols": {
4437
"version": "4.1.0",
4438
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@@ -5333,6 +5364,11 @@
5364
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
5365
"dev": true
5366
},
5367
+ "yaml": {
5368
+ "version": "1.10.2",
5369
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
5370
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="
5371
+ },
5372
"yargs": {
5373
"version": "16.2.0",
5374
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
package.json
+6
-3
@@ -23,20 +23,23 @@
23
"koa-bodyparser": "^4.3.0",
24
"koa-mount": "^4.0.0",
25
"koa-static": "^5.0.0",
26
- "mime-types": "^2.1.34"
26
+ "lodash": "^4.17.21",
27
+ "mime-types": "^2.1.34",
28
+ "yaml": "^1.10.2"
29
},
30
"devDependencies": {
31
+ "@types/axios": "^0.14.0",
32
"@types/koa": "^2.13.4",
33
"@types/koa__router": "^8.0.11",
34
"@types/koa-bodyparser": "^4.3.4",
35
"@types/koa-mount": "^4.0.1",
36
"@types/koa-static": "^4.0.2",
37
+ "@types/lodash": "^4.14.178",
38
"@types/mime-types": "^2.1.1",
39
"@types/mocha": "^9.0.0",
40
"@types/node": "^16.11.12",
37
- "@types/axios": "^0.14.0",
38
- "mocha": "^9.1.3",
41
"axios": "^0.24.0",
42
+ "mocha": "^9.1.3",
43
"nodemon": "^2.0.15",
44
"ts-node": "^10.4.0",
45
"typescript": "^4.5.2"
src/apis.ts
+35
-16
@@ -1,5 +1,10 @@
1
import glob from 'fast-glob'
2
import Koa from 'koa'
3
+import { vfs } from './vfs'
4
+import { enforceFinal } from '../frontend/src/misc'
5
+import { Stats } from 'fs'
6
+import { stat } from 'fs/promises'
7
+import _ from 'lodash'
8
9
type ApiHandler = (params?:any, ctx?:any) => any
10
type ApiHandlers = Record<string, ApiHandler>
@@ -20,22 +25,36 @@ export function apiMw(apis: ApiHandlers) : Koa.Middleware {
25
26
export const frontEndApis: ApiHandlers = {
27
async file_list(params:any) {
23
- const res = await glob('.' + (params.path || '/') + '*', {
24
- stats: true,
25
- dot: true,
26
- markDirectories: true,
27
- onlyFiles: false,
28
- })
29
- const list = res.map(x => {
30
- const o = x.stats
31
- const folder = x.path.endsWith('/')
32
- return {
33
- n: x.name+(folder ? '/' : ''),
34
- c: o?.ctime,
35
- m: o?.mtime,
36
- s: folder ? undefined : o?.size,
37
- }
38
- })
28
+ let node = vfs.urlToNode(params.path || '/')
29
+ if (!node)
30
+ return
31
+ const list = await Promise.all((node.children ||[]).map(async node =>
32
+ node.source && await stat(node.source).then(
33
+ statRes => statToFile(node.name, statRes),
34
+ () => null) ))
35
+ _.remove(list, x => !x)
36
+ let path = node.source
37
+ if (path) {
38
+ // using / because trying path.join broke glob() on my Windows machine
39
+ path = enforceFinal('/', glob.escapePath(path)) + '*'
40
+ const res = await glob(path, {
41
+ stats: true,
42
+ dot: true,
43
+ markDirectories: true,
44
+ onlyFiles: false,
45
+ })
46
+ list.push( ...res.map(x => statToFile(x.name, x.stats!)) )
47
+ }
48
return { list }
49
}
50
}
51
+
52
+function statToFile(name: string | undefined, stat:Stats) {
53
+ const folder = stat.isDirectory()
54
+ return {
55
+ n: name + (folder ? '/' : ''),
56
+ c: stat?.ctime,
57
+ m: stat?.mtime,
58
+ s: folder ? undefined : stat?.size,
59
+ }
60
+}
src/index.ts
+2
-2
@@ -1,10 +1,10 @@
1
import Koa from 'koa'
2
-import serve from 'koa-static'
2
import mount from 'koa-mount'
3
import bodyParser from 'koa-bodyparser'
4
import { apiMw, frontEndApis } from './apis'
5
import { serveFrontend } from './frontend'
6
import { API_URI, DEV, FRONTEND_URI } from './const'
7
+import { serveFileFromVfs } from './vfs'
8
9
const PORT = 80
10
@@ -22,7 +22,7 @@ srv.use(async (ctx, next) => {
22
srv.use(mount(API_URI, new Koa().use(bodyParser()).use(apiMw(frontEndApis))))
23
24
// serve shared files and front-end files
25
-const serveFiles = serve('.', { hidden:true })
25
+const serveFiles = serveFileFromVfs()
26
const serveFrontendPrefixed = mount(FRONTEND_URI.slice(0,-1), serveFrontend)
27
srv.use(async (ctx, next) => {
28
if (ctx.method !== 'GET')
src/vfs.ts
new
+64
@@ -0,0 +1,64 @@
1
+import Koa from 'koa'
2
+import yaml from 'yaml'
3
+import fs, { createReadStream } from 'fs'
4
+import { basename } from 'path'
5
+
6
+enum VfsNodeType {
7
+ root,
8
+}
9
+
10
+interface VfsNode {
11
+ type?: VfsNodeType,
12
+ name?: string,
13
+ source?: string,
14
+ children?: VfsNode[]
15
+}
16
+
17
+class Vfs {
18
+ root: VfsNode = {}
19
+
20
+ load(path: string, watch:boolean=true) {
21
+ const data = fs.readFileSync(path, 'utf8')
22
+ this.root = yaml.parse(data)
23
+ if (!this.root)
24
+ throw `Couldn't load ${path}`
25
+ this.root.type = VfsNodeType.root
26
+ recur(this.root)
27
+ if (watch)
28
+ fs.watch(path, () => vfs.load(path))
29
+
30
+ function recur(node:VfsNode) {
31
+ if (node.type !== VfsNodeType.root && !node.name && node.source)
32
+ node.name = basename(node.source)
33
+ node.children?.forEach(recur)
34
+ }
35
+ }
36
+
37
+ urlToNode(url: string) {
38
+ let run = this.root
39
+ const rest = url.split('/').filter(Boolean)
40
+ while (rest.length) {
41
+ const piece = rest[0]
42
+ // @ts-ignore
43
+ const find = run?.children?.find(x => x.name === piece)
44
+ if (!find)
45
+ return run.source ? { source:run.source + '/' + rest.join('/') } : null
46
+ run = find
47
+ rest.shift()
48
+ }
49
+ return run
50
+ }
51
+}
52
+
53
+export function serveFileFromVfs() : Koa.Middleware {
54
+ return async (ctx, next) => {
55
+ let { url } = ctx
56
+ const path = vfs.urlToNode(decodeURI(url))?.source
57
+ if (path)
58
+ ctx.body = createReadStream(path as string)
59
+ await next()
60
+ }
61
+}
62
+
63
+export const vfs = new Vfs()
64
+vfs.load('vfs.yaml')
vfs.yaml
new
+3
@@ -0,0 +1,3 @@
1
+source: .
2
+children:
3
+ - source: /music