less hanging with unresponding UNC paths
Massimo Melina committed
Mar 5, 2026 at 10:52 UTC
f2a9c775400826af5873343bcb2abc7d0dd3ee6a
5 files changed
+19
-10
config.md
+3
-1
@@ -130,6 +130,8 @@ Configuration can be done in several ways
130
- `auto_basic` automatically detect (based on user-agent) when the basic web interface should be served, to support legacy browsers. Default is true. No UI.
131
You can disable it setting it to `false`, or recognize additional user-agents by setting a regular expression.
132
- `file_timeout` number of seconds to wait before giving up when accessing a file. Default is 3. No UI.
133
+- `smart_unc_folder_detection` Windows-only optimization for UNC paths in VFS. When enabled, if the name of the element has no dot, it is assumed to be a folder without doing a stat call.
134
+ This is faster with unreachable SMB shares, but heuristic and not fully reliable. Disable to always use regular detection. Default is true. No UI.
135
- `authorization_header` enable support for the HTTP `Authorization` header. Default is true. No UI.
136
- `cache_control_disk_files` number of seconds after which the browser should bypass the cache and check the server for an updated version of the file. Default is 5. No UI.
137
- `disable_custom_html` disable the content of `custom_html`. Default is false.
@@ -250,4 +252,4 @@ For each account entries, this is the list of properties you can have:
252
253
Do you need to load a different config file that's not `config.yaml`?
254
Use this parameter at command line `--config PATH` or similarly with an env `HFS_CONFIG`.
253
-The path you specify can be either a folder, or full-path to the file.
255
+The path you specify can be either a folder or full-path to the file.
src/first.ts
+7
-6
@@ -9,14 +9,15 @@ export function onProcessExit(cb: ProcessExitHandler) {
9
}
10
11
export let quitting = false
12
-onProcessExit(() => quitting = true)
13
-
12
// 'exit' event is handled as the last resort, but it's not compatible with async callbacks
15
-onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal =>
16
- Promise.allSettled(Array.from(cbsOnExit).map(cb => cb(signal))).then(() => {
17
- console.log('quitting', signal||'')
13
+onFirstEvent(process, ['exit', 'SIGQUIT', 'SIGTERM', 'SIGINT', 'SIGHUP'], signal => {
14
+ quitting = true
15
+ console.log('quitting', signal || '')
16
+ return Promise.allSettled(Array.from(cbsOnExit).map(cb => cb(signal))).then(() => {
17
+ console.debug('process exit')
18
process.exit(0)
19
- }))
19
+ })
20
+})
21
22
// keep calling cb in a sync fashion – returning a promise instead would break the code for argv.updating (update.ts)
23
export function onFirstEvent(emitter:EventEmitter, events: string[], cb: (...args:any[])=> void) {
src/stat.ts
+3
@@ -1,12 +1,15 @@
1
import { Worker } from 'node:worker_threads'
2
import { Stats } from 'node:fs'
3
import { PendingPromise, pendingPromise } from './cross'
4
+import { quitting } from './first'
5
6
// all stat requests for the same worker are serialized, potentially introducing extra latency
7
8
const pool = new Map<string, (path: string) => Promise<Stats>>()
9
10
export function getStatWorker(key: string) {
11
+ if (quitting)
12
+ return () => Promise.reject('quitting')
13
const existing = pool.get(key)
14
if (existing)
15
return existing
src/util-files.ts
+1
-1
@@ -25,7 +25,7 @@ export async function statWithTimeout(path: string) {
25
return haveTimeout(fileTimeout.compiled(), op)
26
}
27
28
-function getUncHost(path: string) {
28
+export function getUncHost(path: string) {
29
return /^\\\\([^\\]+)\\/.exec(path)?.[1]
30
}
31
src/vfs.ts
+5
-2
@@ -5,7 +5,7 @@ import { basename, dirname, join, resolve } from 'path'
5
import {
6
makeMatcher, setHidden, onlyTruthy, isValidFileName, throw_, VfsPerms, Who, debounceAsync,
7
isWhoObject, WHO_ANY_ACCOUNT, defaultPerms, PERM_KEYS, removeStarting, HTTP_SERVER_ERROR, try_, matches,
8
- statWithTimeout, safeDecodeURIComponent,
8
+ statWithTimeout, safeDecodeURIComponent, getUncHost,
9
} from './misc'
10
import Koa from 'koa'
11
import _ from 'lodash'
@@ -190,9 +190,12 @@ export async function getNodeByName(name: string, parent: VfsNode, assumeMissing
190
}
191
}
192
193
+const smartUncFolderDetection = defineConfig('smart_unc_folder_detection', true)
194
+
195
async function setIsFolder(node: VfsNode) {
196
if (!node.source) return
195
- const isFolder = /[\\/]$/.test(node.source) || await nodeStats(node).then(x => x?.isDirectory(), () => undefined)
197
+ const isFolder = smartUncFolderDetection.get() && getUncHost(node.source) ? !basename(node.source).includes('.') // no dot = folder – not very reliable but fast for unreachable unc hosts, and you can opt-out
198
+ : /[\\/]$/.test(node.source) || await nodeStats(node).then(x => x?.isDirectory(), () => undefined)
199
setHidden(node, { isFolder })
200
return isFolder
201
}