fix: listing interrupted by lack of permissions
Massimo Melina committed
Feb 1, 2022 at 19:53 UTC
0f71565e71866b523f3994191af8aae60d595fec
5 files changed
+59
-53
src/api.file_list.ts
+17
-21
@@ -1,6 +1,5 @@
1
-import { vfs, VfsNode, walkNode } from './vfs'
1
+import { getNodeName, vfs, VfsNode, walkNode } from './vfs'
2
import createSSE from './sse'
3
-import { basename } from 'path'
3
import { ApiError, ApiHandler } from './apis'
4
import { stat } from 'fs/promises'
5
import { mapPlugins } from './plugins'
@@ -30,7 +29,7 @@ export const file_list:ApiHandler = async ({ path, offset, limit, search, omit,
29
const list = []
30
for await (const sub of walker) {
31
if (sseSrv?.stopped || ctx.aborted) break
33
- if (!filter(sub.name))
32
+ if (!filter(getNodeName(sub)))
33
continue
34
const entry = await nodeToDirEntry(sub)
35
if (!entry)
@@ -69,27 +68,24 @@ export const file_list:ApiHandler = async ({ path, offset, limit, search, omit,
68
export interface DirEntry { n:string, s?:number, m?:Date, c?:Date }
69
70
async function nodeToDirEntry(node: VfsNode): Promise<DirEntry | null> {
71
+ let { source, default:def } = node
72
+ const name = getNodeName(node)
73
+ if (!source)
74
+ return name ? { n: name + '/' } : null
75
+ if (def)
76
+ return { n: name }
77
try {
73
- let { name, source, default:def } = node
74
- if (source) {
75
- if (!name)
76
- name = basename(source)
77
- if (def)
78
- return { n: name }
79
- const st = await stat(source)
80
- const folder = st.isDirectory()
81
- const { ctime, mtime } = st
82
- return {
83
- n: name + (folder ? '/' : ''),
84
- c: ctime,
85
- m: Math.abs(+mtime-+ctime) < 1000 ? undefined : mtime,
86
- s: folder ? undefined : st.size,
87
- }
78
+ const st = await stat(source)
79
+ const folder = st.isDirectory()
80
+ const { ctime, mtime } = st
81
+ return {
82
+ n: name + (folder ? '/' : ''),
83
+ c: ctime,
84
+ m: Math.abs(+mtime-+ctime) < 1000 ? undefined : mtime,
85
+ s: folder ? undefined : st.size,
86
}
89
- return name ? { n: name + '/' } : null
87
}
91
- catch (err:any) {
92
- console.error(String(err))
88
+ catch {
89
return null
90
}
91
}
src/listen.ts
+3
-1
@@ -68,8 +68,10 @@ function startServer(srv: http.Server, port: number, secure:string='') {
68
const ad = srv.address()
69
if (!ad)
70
return reject('no address')
71
- if (typeof ad === 'string')
71
+ if (typeof ad === 'string') {
72
+ srv.close()
73
return reject('type of socket not supported')
74
+ }
75
port = ad.port
76
console.log(proto, `serving on port`, port)
77
src/misc.ts
+4
@@ -104,3 +104,7 @@ export function pattern2filter(pattern: string){
104
return (s?:string) =>
105
!s || !pattern || re.test(basename(s))
106
}
107
+
108
+export function isWindows() {
109
+ return process.platform === 'win32'
110
+}
src/vfs.ts
+27
-24
@@ -14,7 +14,7 @@ export enum VfsNodeType {
14
15
export interface VfsNode {
16
type?: VfsNodeType,
17
- name: string,
17
+ name?: string,
18
source?: string,
19
children?: VfsNode[],
20
hide?: string,
@@ -29,7 +29,7 @@ export interface VfsNode {
29
30
type SinglePerm = 'r' | 'w'
31
32
-const EMPTY = { name:'', type: VfsNodeType.root }
32
+const EMPTY = { type: VfsNodeType.root }
33
34
export const MIME_AUTO = 'auto'
35
@@ -40,16 +40,16 @@ export class Vfs {
40
this.root = { ...EMPTY }
41
}
42
43
- async urlToNode(url: string, ctx: Koa.Context, root?: VfsNode) : Promise<VfsNode | undefined> {
43
+ async urlToNode(url: string, ctx?: Koa.Context, root?: VfsNode) : Promise<VfsNode | undefined> {
44
let run = root || this.root
45
const rest = url.split('/').filter(Boolean).map(decodeURIComponent)
46
- if (!hasPermission(run, ctx)) return
46
+ if (ctx && !hasPermission(run, ctx)) return
47
while (rest.length) {
48
let piece = rest.shift() as string
49
const child = findChildByName(piece, run)
50
if (child) {
51
run = child
52
- if (!hasPermission(run, ctx)) return
52
+ if (ctx && !hasPermission(run, ctx)) return
53
continue
54
}
55
if (!run.source)
@@ -62,8 +62,8 @@ export class Vfs {
62
try { await fs.stat(source) } // check existence
63
catch { return }
64
return {
65
+ type: VfsNodeType.temp,
66
source,
66
- name: basename(source),
67
mime: run.mime || (run.default && MIME_AUTO)
68
}
69
}
@@ -73,24 +73,24 @@ export class Vfs {
73
}
74
75
export const vfs = new Vfs()
76
-subscribeConfig({ k: 'vfs' }, data => {
77
- // we should validate content now
78
- if (data)
79
- recur(data)
80
- vfs.root = data
81
-
82
- function recur(node:VfsNode) {
83
- if (node.type !== VfsNodeType.root && !node.name && node.source)
84
- node.name = basename(node.source)
85
- node.children?.forEach(recur)
86
- }
87
-})
76
+subscribeConfig({ k: 'vfs' }, data =>
77
+ vfs.root = data)
78
79
function findChildByName(name:string, node:VfsNode) {
80
const { rename } = node
81
if (rename) // @ts-ignore
82
name = Object.entries(rename).find(([,v]) => name === v)[0] || name
93
- return node?.children?.find(x => x.name === name)
83
+ return node?.children?.find(x => getNodeName(x) === name)
84
+}
85
+
86
+export function getNodeName(node: VfsNode) {
87
+ return node.name
88
+ || node.source && /^[a-zA-Z]:$/.test(node.source) && node.source
89
+ || basename(node.source||'')
90
+}
91
+
92
+export async function nodeIsDirectory(node: VfsNode) {
93
+ return Boolean(!node.source || await isDirectory(node.source))
94
}
95
96
export function hasPermission(node:VfsNode, ctx: Koa.Context) {
@@ -104,9 +104,12 @@ export async function* walkNode(parent:VfsNode, ctx: Koa.Context, depth:number=0
104
for (const node of children) {
105
if (node.hidden || !hasPermission(node, ctx))
106
continue
107
- yield prefixPath ? { ...node, name: prefixPath+node.name } : node
108
- if (depth > 0 && node && (node.children || node.source && await isDirectory(node.source)))
109
- yield* walkNode(node, ctx, depth - 1, prefixPath+node.name+'/')
107
+ yield prefixPath ? { ...node, name: prefixPath+getNodeName(node) } : node
108
+ try {
109
+ if (depth > 0 && node && await nodeIsDirectory(node))
110
+ yield* walkNode(node, ctx, depth - 1, prefixPath+getNodeName(node)+'/')
111
+ }
112
+ catch{} // stat failed in nodeIsDirectory, ignore
113
}
114
if (!source)
115
return
@@ -116,6 +119,7 @@ export async function* walkNode(parent:VfsNode, ctx: Koa.Context, depth:number=0
119
dot: true,
120
onlyFiles: false,
121
cwd: source,
122
+ suppressErrors: true,
123
caseSensitiveMatch: !isWindows(),
124
ignore: [parent.hide, parent.remove].filter(Boolean) as string[],
125
})
@@ -133,7 +137,6 @@ export async function* walkNode(parent:VfsNode, ctx: Koa.Context, depth:number=0
137
}
138
}
139
catch(e) {
136
- if ((e as any).code !== 'ENOTDIR')
137
- throw e
140
+ console.debug('glob', source, e) // ENOTDIR, or lacking permissions
141
}
142
}
src/zip.ts
+8
-7
@@ -1,6 +1,6 @@
1
-import { vfs, VfsNode, walkNode } from './vfs'
1
+import { getNodeName, nodeIsDirectory, vfs, VfsNode, walkNode } from './vfs'
2
import Koa from 'koa'
3
-import { filterMapGenerator, isDirectory, pattern2filter, prefix } from './misc'
3
+import { filterMapGenerator, pattern2filter, prefix } from './misc'
4
import { QuickZipStream } from './QuickZipStream'
5
import { createReadStream } from 'fs'
6
import fs from 'fs/promises'
@@ -10,7 +10,7 @@ import { dirname } from 'path'
10
export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
11
ctx.status = 200
12
ctx.mime = 'zip'
13
- const { name } = node
13
+ const name = getNodeName(node)
14
ctx.attachment((name || 'archive') + '.zip')
15
const filter = pattern2filter(String(ctx.query.search||''))
16
const { list } = ctx.query
@@ -20,22 +20,23 @@ export async function zipStreamFromFolder(node: VfsNode, ctx: Koa.Context) {
20
const subNode = await vfs.urlToNode(el, ctx, node)
21
if (!subNode)
22
continue
23
- if (subNode.children || subNode.source && await isDirectory(subNode.source)) // a directory needs to walked
23
+ if (await nodeIsDirectory(subNode)) // a directory needs to walked
24
yield* walkNode(subNode, ctx, Infinity, el+'/')
25
else
26
- yield { ...subNode, name: prefix('', dirname(el), '/') + subNode.name } // reflect relative path in archive, otherwise way may have name-clashes
26
+ yield { ...subNode, name: prefix('', dirname(el), '/') + getNodeName(subNode) } // reflect relative path in archive, otherwise way may have name-clashes
27
}
28
})()
29
const mappedWalker = filterMapGenerator(walker, async (el:VfsNode) => {
30
const { source } = el
31
- if (!source || ctx.req.aborted || !filter(el.name))
31
+ const name = getNodeName(el)
32
+ if (!source || ctx.req.aborted || !filter(name))
33
return
34
try {
35
const st = await fs.stat(source)
36
if (!st || !st.isFile())
37
return
38
return {
38
- path: el.name,
39
+ path: name,
40
size: st.size,
41
ts: st.mtime || st.ctime,
42
getData: () => createReadStream(source)