vfs.remove
Massimo Melina committed
Dec 16, 2021 at 12:46 UTC
136729df9447cd39539103a941c546b0b9996228
7 files changed
+72
-25
dev-notes.md
-5
@@ -9,11 +9,6 @@
9
10
# To do
11
- permissions
12
- R / to see the list
13
- R /folder to see it in the list and access it
14
- you see what you can also get
15
- if permission is denied a 401 warning is displayed with login link
16
- how to trap such clicks? 1) redirect with hash 2) trap click in client and call test api
12
- search
13
- vfs: serve an html for a folder?
14
- "default" property for vfsNode?
package-lock.json
+31
@@ -29,6 +29,7 @@
29
"@types/koa-mount": "^4.0.1",
30
"@types/koa-static": "^4.0.2",
31
"@types/lodash": "^4.14.178",
32
+ "@types/micromatch": "^4.0.2",
33
"@types/mime-types": "^2.1.1",
34
"@types/minimist": "^1.2.2",
35
"@types/mocha": "^9.0.0",
@@ -182,6 +183,12 @@
183
"@types/node": "*"
184
}
185
},
186
+ "node_modules/@types/braces": {
187
+ "version": "3.0.1",
188
+ "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.1.tgz",
189
+ "integrity": "sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==",
190
+ "dev": true
191
+ },
192
"node_modules/@types/connect": {
193
"version": "3.4.35",
194
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -327,6 +334,15 @@
334
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
335
"dev": true
336
},
337
+ "node_modules/@types/micromatch": {
338
+ "version": "4.0.2",
339
+ "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.2.tgz",
340
+ "integrity": "sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA==",
341
+ "dev": true,
342
+ "dependencies": {
343
+ "@types/braces": "*"
344
+ }
345
+ },
346
"node_modules/@types/mime": {
347
"version": "1.3.2",
348
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@@ -3230,6 +3246,12 @@
3246
"@types/node": "*"
3247
}
3248
},
3249
+ "@types/braces": {
3250
+ "version": "3.0.1",
3251
+ "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.1.tgz",
3252
+ "integrity": "sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==",
3253
+ "dev": true
3254
+ },
3255
"@types/connect": {
3256
"version": "3.4.35",
3257
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
@@ -3375,6 +3397,15 @@
3397
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==",
3398
"dev": true
3399
},
3400
+ "@types/micromatch": {
3401
+ "version": "4.0.2",
3402
+ "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.2.tgz",
3403
+ "integrity": "sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA==",
3404
+ "dev": true,
3405
+ "requires": {
3406
+ "@types/braces": "*"
3407
+ }
3408
+ },
3409
"@types/mime": {
3410
"version": "1.3.2",
3411
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
package.json
+1
@@ -36,6 +36,7 @@
36
"@types/koa-mount": "^4.0.1",
37
"@types/koa-static": "^4.0.2",
38
"@types/lodash": "^4.14.178",
39
+ "@types/micromatch": "^4.0.2",
40
"@types/mime-types": "^2.1.1",
41
"@types/minimist": "^1.2.2",
42
"@types/mocha": "^9.0.0",
src/apis.ts
+4
-14
@@ -1,7 +1,6 @@
1
-import glob from 'fast-glob'
1
import Koa from 'koa'
2
import { vfs, VfsNode } from './vfs'
4
-import { enforceFinal, wantArray } from './misc'
3
+import { globDir } from './misc'
4
import { Stats } from 'fs'
5
import { stat } from 'fs/promises'
6
import _ from 'lodash'
@@ -25,7 +24,7 @@ export function apiMw(apis: ApiHandlers) : Koa.Middleware {
24
25
export const frontEndApis: ApiHandlers = {
26
async file_list(params:any) {
28
- let node = vfs.urlToNode(params.path || '/')
27
+ let node = await vfs.urlToNode(params.path || '/')
28
if (!node)
29
return
30
const list = await Promise.all((node.children ||[]).map(node =>
@@ -33,17 +32,8 @@ export const frontEndApis: ApiHandlers = {
32
_.remove(list, x => !x)
33
let path = node.source
34
if (path) {
36
- // using / because trying path.join broke glob() on my Windows machine
37
- path = enforceFinal('/', glob.escapePath(path.replace(/\\/g,'/')))
38
- const res = await glob(path + '*', {
39
- stats: true,
40
- dot: true,
41
- markDirectories: true,
42
- onlyFiles: false,
43
- ignore: wantArray(node.hide).map(x => path+x),
44
- })
45
- const { rename } = node
46
- list.push( ...res.map(x => statToFile(rename?.[x.name] || x.name, x.stats!)) )
35
+ const res = await globDir(path, [node.hide, node.remove])
36
+ list.push( ...res.map(x => statToFile(node!.rename?.[x.name] || x.name, x.stats!)) )
37
}
38
return { list }
39
}
src/index.ts
+1
-1
@@ -29,7 +29,7 @@ srv.use(async (ctx, next) => {
29
return await next()
30
if (path.startsWith(FRONTEND_URI))
31
return await serveFrontendPrefixed(ctx,next)
32
- const node = vfs.urlToNode(decodeURI(path))
32
+ const node = await vfs.urlToNode(decodeURI(path))
33
if (!node)
34
return await next()
35
const { source } = node
src/misc.ts
+21
-1
@@ -1,4 +1,5 @@
1
import { stat } from 'fs/promises'
2
+import glob from 'fast-glob'
3
4
export function enforceFinal(sub:string, s:string) {
5
return s.endsWith(sub) ? s : s+sub
@@ -11,4 +12,23 @@ export function wantArray(x:any) {
12
export async function isDirectory(path: string) {
13
try { return (await stat(path)).isDirectory() }
14
catch(e) { return false }
14
-}
\ No newline at end of file
15
+}
16
+
17
+export function complySlashes(path: string) {
18
+ return glob.escapePath(path.replace(/\\/g,'/'))
19
+}
20
+
21
+export function prefix(pre:string, v:string|number, post:string='') {
22
+ return v ? pre+v+post : ''
23
+}
24
+
25
+export async function globDir(path: string, ignore?: any[]) {
26
+ path = enforceFinal('/', complySlashes(path)) // fast-glob lib wants forward-slashes
27
+ return await glob(path + '*', {
28
+ stats: true,
29
+ dot: true,
30
+ markDirectories: true,
31
+ onlyFiles: false,
32
+ ignore: ignore?.flat().filter(Boolean).map(x => path!+x),
33
+ })
34
+}
src/vfs.ts
+14
-4
@@ -3,6 +3,8 @@ import yaml from 'yaml'
3
import fs from 'fs/promises'
4
import { FSWatcher, watch } from 'fs'
5
import { basename } from 'path'
6
+import { isMatch } from 'micromatch'
7
+import { complySlashes, prefix } from './misc'
8
9
enum VfsNodeType {
10
root,
@@ -14,6 +16,7 @@ export interface VfsNode {
16
source?: string,
17
children?: VfsNode[],
18
hide?: string | string[],
19
+ remove?: string | string[],
20
hidden?: boolean,
21
rename?: Record<string,string>,
22
}
@@ -28,7 +31,7 @@ export class Vfs {
31
}
32
33
async load(path: string, watchFile:boolean=true) {
31
- console.debug('loading vfs')
34
+ console.debug('loading',path)
35
try {
36
const data = await fs.readFile(path, 'utf8')
37
this.root = yaml.parse(data)
@@ -58,7 +61,7 @@ export class Vfs {
61
}
62
}
63
61
- urlToNode(url: string) {
64
+ async urlToNode(url: string) {
65
let run = this.root
66
const rest = url.split('/').filter(Boolean)
67
while (rest.length) {
@@ -72,8 +75,15 @@ export class Vfs {
75
}
76
// @ts-ignore
77
const find = run?.children?.find(x => x.name === piece)
75
- if (!find)
76
- return run.source ? { source:run.source + '/' + piece + '/' + rest.join('/') } : null
78
+ if (!find) {
79
+ if (!run.source)
80
+ return null
81
+ const relativeSource = piece + prefix('/', rest.join('/'))
82
+ const baseSource = complySlashes(run.source+ '/')
83
+ const source = baseSource + relativeSource
84
+ const removed = isMatch(source, [run.remove].flat().map(x => baseSource + x))
85
+ return removed || !await fs.stat(source) ? null : { source }
86
+ }
87
run = find
88
}
89
return run