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