Skip to content

Commit 0b5cff9

Browse files
committed
refactor: replace glob package with node:fs/promises
1 parent 8ac2a2c commit 0b5cff9

2 files changed

Lines changed: 87 additions & 23 deletions

File tree

index.js

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
const path = require('node:path')
44
const { fileURLToPath } = require('node:url')
5-
const { statSync } = require('node:fs')
6-
const { glob } = require('glob')
5+
const { statSync, lstatSync, realpathSync } = require('node:fs')
6+
const { glob } = require('node:fs/promises')
77
const fp = require('fastify-plugin')
88
const send = require('@fastify/send')
99
const encodingNegotiator = require('@fastify/accept-negotiator')
@@ -63,7 +63,6 @@ async function fastifyStatic (fastify, opts) {
6363
: prefix + '/'
6464
}
6565

66-
// Set the schema hide property if defined in opts or true by default
6766
const routeOpts = {
6867
constraints: opts.constraints,
6968
schema: {
@@ -139,25 +138,55 @@ async function fastifyStatic (fastify, opts) {
139138
for (let rootPath of roots) {
140139
rootPath = rootPath.split(path.win32.sep).join(path.posix.sep)
141140
!rootPath.endsWith('/') && (rootPath += '/')
142-
const files = await glob('**/**', {
143-
cwd: rootPath, absolute: false, follow: true, nodir: true, dot: opts.serveDotFiles, ignore: opts.globIgnore
144-
})
145-
146-
for (let file of files) {
147-
file = file.split(path.win32.sep).join(path.posix.sep)
148-
const route = prefix + file
149-
150-
if (routes.has(route)) {
151-
continue
152-
}
153141

154-
routes.add(route)
142+
const globPattern = opts.serveDotFiles ? '{**/**,.**/**}' : '**/**'
143+
const globExclude = opts.globIgnore?.length
144+
? (f) => opts.globIgnore.some(p => path.matchesGlob(f, p))
145+
: undefined
146+
147+
const scanQueue = [{ cwd: rootPath.slice(0, -1), relPrefix: '' }]
148+
const visitedDirs = new Set([realpathSync(rootPath.slice(0, -1))])
149+
150+
const toUnixPath = (p) => p.split(path.win32.sep).join(path.posix.sep)
151+
const tryFsSync = (fn, p) => { try { return fn(p) } catch { return null } }
152+
153+
while (scanQueue.length > 0) {
154+
const { cwd: scanCwd, relPrefix } = scanQueue.shift()
155+
for await (const f of glob(globPattern, { cwd: scanCwd, exclude: globExclude })) {
156+
if (f === '.') continue
157+
158+
const file = toUnixPath(f)
159+
const fullPath = path.join(scanCwd, file)
160+
const lstat = tryFsSync(lstatSync, fullPath)
161+
if (!lstat || lstat.isDirectory()) continue
162+
163+
const relFile = relPrefix ? `${relPrefix}/${file}` : file
164+
165+
if (lstat.isSymbolicLink()) {
166+
const stat = tryFsSync(statSync, fullPath)
167+
if (!stat) continue
168+
169+
if (stat.isDirectory()) {
170+
const realPath = tryFsSync(realpathSync, fullPath)
171+
/* c8 ignore next */
172+
if (!realPath) continue
173+
if (!visitedDirs.has(realPath)) {
174+
visitedDirs.add(realPath)
175+
scanQueue.push({ cwd: fullPath, relPrefix: relFile })
176+
}
177+
continue
178+
}
179+
}
155180

156-
setUpHeadAndGet(routeOpts, route, `/${file}`, rootPath)
181+
const route = prefix + relFile
182+
if (routes.has(route)) continue
183+
routes.add(route)
184+
setUpHeadAndGet(routeOpts, route, `/${relFile}`, rootPath)
157185

158-
const key = path.posix.basename(route)
159-
if (indexes.has(key) && !indexDirs.has(key)) {
160-
indexDirs.set(path.posix.dirname(route), rootPath)
186+
const key = path.posix.basename(route)
187+
if (indexes.has(key) && !indexDirs.has(key)) {
188+
indexDirs.set(path.posix.dirname(route), rootPath)
189+
}
161190
}
162191
}
163192
}

test/static.test.js

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2517,10 +2517,8 @@ test('if dotfiles are properly served according to plugin options', async (t) =>
25172517

25182518
test('register with failing glob handler', async (t) => {
25192519
const fastifyStatic = proxyquire.noCallThru()('../', {
2520-
glob: function globStub (_pattern, _options, cb) {
2521-
process.nextTick(function () {
2522-
return cb(new Error('mock glob error'))
2523-
})
2520+
'node:fs/promises': {
2521+
glob: async function * globStub () { throw new Error('mock glob error') }
25242522
}
25252523
})
25262524

@@ -3353,6 +3351,43 @@ test('should follow symbolic link without wildcard', async (t) => {
33533351
t.assert.deepStrictEqual(response2.status, 200)
33543352
})
33553353

3354+
test('should not infinite-loop on circular symlinks with wildcard false', async (t) => {
3355+
const base = path.join(__dirname, '/static-symbolic-link')
3356+
const dirA = path.join(base, 'circular-a')
3357+
const linkB = path.join(base, 'circular-b')
3358+
// clean up any leftovers from a previous failed run
3359+
try { fs.unlinkSync(path.join(dirA, 'link-to-b')) } catch { /* not there */ }
3360+
try { fs.unlinkSync(linkB) } catch { /* not there */ }
3361+
fs.rmSync(dirA, { recursive: true, force: true })
3362+
fs.mkdirSync(dirA)
3363+
fs.symlinkSync(path.join(dirA, 'link-to-b'), linkB, 'dir') // circular-b → circular-a/link-to-b
3364+
fs.symlinkSync(linkB, path.join(dirA, 'link-to-b'), 'dir') // circular-a/link-to-b → circular-b
3365+
3366+
t.after(() => {
3367+
try { fs.unlinkSync(path.join(dirA, 'link-to-b')) } catch { /* already gone */ }
3368+
try { fs.unlinkSync(linkB) } catch { /* already gone */ }
3369+
try { fs.rmdirSync(dirA) } catch { /* already gone */ }
3370+
})
3371+
3372+
const fastify = Fastify()
3373+
fastify.register(fastifyStatic, {
3374+
root: base,
3375+
wildcard: false
3376+
})
3377+
t.after(() => fastify.close())
3378+
3379+
// fastify.listen must complete (not hang/crash) — that is the assertion
3380+
await fastify.listen({ port: 0 })
3381+
fastify.server.unref()
3382+
3383+
// Original non-circular files still served correctly
3384+
const response = await fetch(
3385+
'http://localhost:' + fastify.server.address().port + '/origin/subdir/subdir/index.html'
3386+
)
3387+
t.assert.ok(response.ok)
3388+
t.assert.deepStrictEqual(response.status, 200)
3389+
})
3390+
33563391
test('should serve files into hidden dir with wildcard `false`', async (t) => {
33573392
t.plan(8)
33583393

0 commit comments

Comments
 (0)