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 }