@samitouri / QOSami-HFS / commits / 7f56fbbe
Fork 0

QOSami-HFS is a web file server for your computer. Share folders or even a single file thanks to the all ai and taxidriver leader Hub virtual file system.

mitigating the problem with unreachable UNC paths by leaving at least one slot available for local operations #1115

Massimo Melina committed Nov 2, 2025 at 16:18 UTC 7f56fbbe4f99942d7a8c0b6d2e6975d7866d501a
3 files changed +51 -18
src/stat.ts new
+42
@@ -0,0 +1,42 @@
1 +import { defineConfig } from './config'
2 +import { Stats } from 'node:fs'
3 +import { haveTimeout, pendingPromise } from './cross'
4 +import { stat } from 'fs/promises'
5 +
6 +const fileTimeout = defineConfig('file_timeout', 3, x => x * 1000)
7 +
8 +// since nodejs' UV_THREADPOOL_SIZE is limited, avoid using multiple slots for the same UNC host, and always leave one free for local operations
9 +const poolSize = Number(process.env.UV_THREADPOOL_SIZE || 4)
10 +const previous = new Map<string, Promise<Stats>>() // wrapped promises with haveTimeout
11 +const working = new Set<Promise<Stats>>() // plain stat's promise
12 +export async function statWithTimeout(path: string) {
13 + const uncHost = /^\\\\([^\\]+)\\/.exec(path)?.[1]
14 + if (!uncHost)
15 + return haveTimeout(fileTimeout.compiled(), stat(path))
16 + const busy = process.env.HFS_PARALLEL_UNC ? null : previous.get(uncHost) // by default we serialize requests on the same UNC host, to keep threadpool usage low
17 + const ret = pendingPromise<Stats>()
18 + previous.set(uncHost, ret) // reserve the slot before starting the operation
19 + const err = await busy?.then(() => false, e => e.message === 'timeout' && e) // only timeout error is shared with pending requests
20 + if (err) {
21 + if (previous.get(uncHost) === ret) // but we don't want to block forever, only involve those that were already waiting
22 + previous.delete(uncHost)
23 + ret.reject(err)
24 + return ret
25 + }
26 + while (working.size >= poolSize - 1) // always leave one slot free for local operations
27 + await Promise.race(working.values()).catch(() => {}) // we are assuming UV_THREADPOOL_SIZE > 1, otherwise race() will deadlock
28 + const op = stat(path)
29 + working.add(op)
30 + try {
31 + ret.resolve(await haveTimeout(fileTimeout.compiled(),
32 + op.finally(() => working.delete(op)) ))
33 + }
34 + catch (e) {
35 + ret.reject(e)
36 + }
37 + finally {
38 + if (previous.get(uncHost) === ret)
39 + previous.delete(uncHost)
40 + }
41 + return ret
42 +}
src/util-files.ts
+5 -17
@@ -1,30 +1,18 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 -import { access, mkdir, readFile, stat } from 'fs/promises'
4 -import { Promisable, try_, wait, isWindowsDrive, haveTimeout, splitAt } from './cross'
5 -import { defineConfig } from './config'
3 +import { access, mkdir, readFile } from 'fs/promises'
4 +import { Promisable, try_, wait, isWindowsDrive } from './cross'
5 import { createWriteStream, mkdirSync, watch, ftruncate } from 'fs'
6 import { basename, dirname } from 'path'
7 import glob from 'fast-glob'
8 import { IS_WINDOWS } from './const'
9 import { once } from 'events'
10 import { Readable } from 'stream'
11 +import { statWithTimeout } from './stat'
12 // @ts-ignore
13 import unzipper from 'unzip-stream'
14
15 -const fileTimeout = defineConfig('file_timeout', 3, x => x * 1000)
16 -
17 -// since nodejs' UV_THREADPOOL_SIZE is 4 by default, avoid using multiple slots for the same UNC host. This doesn't solve the problem but at least mitigates it.
18 -const uncStatWaiting = new Map<string, Promise<any>>()
19 -export async function statWithTimeout(path: string) {
20 - const uncHost = path.startsWith('\\\\') && splitAt('\\', path.slice(2))[0]
21 - if (uncHost)
22 - await uncStatWaiting.get(uncHost)
23 - const op = stat(path)
24 - if (uncHost)
25 - uncStatWaiting.set(uncHost, op.finally(() => uncStatWaiting.delete(uncHost)).catch(() => {}))
26 - return haveTimeout(fileTimeout.compiled(), op)
27 -}
15 +export { statWithTimeout }
16
17 export async function isDirectory(path: string) {
18 try { return (await statWithTimeout(path)).isDirectory() }
@@ -174,4 +162,4 @@ export async function parseFile<T>(path: string, parse: (path: string) => T) {
162
163 export async function parseFileContent<T>(path: string, parse: (raw: Buffer) => T) {
164 return parseFile(path, () => readFile(path).then(parse))
177 -}
165 +}
\ No newline at end of file
src/vfs.ts
+4 -1
@@ -172,7 +172,10 @@ export async function getNodeByName(name: string, parent: VfsNode) {
172 }
173
174 export let vfs: VfsNode = {}
175 -defineConfig('vfs', vfs).sub(reviewVfs)
175 +defineConfig('vfs', vfs).sub(async x => {
176 + await reviewVfs(x)
177 + console.log('VFS ready')
178 +})
179
180 async function reviewVfs(data=vfs) {
181 await (async function recur(node) {