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