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)