fix: on Windows, don't list files inside hidden folders

Massimo Melina committed Aug 31, 2024 at 17:15 UTC cb1700dfb26a1eabe6ea02c02a390fb96094a3f7
3 files changed +21 -24
src/dirStream.ts
+18 -8
@@ -1,5 +1,7 @@
1 import { makeQ } from './makeQ'
2 -import { opendir, stat } from 'fs/promises'
2 +import { stat, readdir } from 'fs/promises'
3 +import { runCmd } from './util-os'
4 +import { IS_WINDOWS } from './const'
5 import { join } from 'path'
6 import { Readable } from 'stream'
7 import { pendingPromise } from './cross'
@@ -12,10 +14,12 @@ export interface DirStreamEntry extends Dirent {
14
15 const dirQ = makeQ(3)
16
15 -export function createDirStream(startPath: string, depth=0) {
17 +export function createDirStream(startPath: string, { depth=0, hidden=true }) {
18 let stopped = false
19 let started = false
20 const closingQ: string[] = []
21 + const hiddenRoot = !hidden && IS_WINDOWS && getWindowsHiddenFiles(startPath) // produce first level faster
22 + const hiddenDeep = hiddenRoot && depth && getWindowsHiddenFiles(startPath, true)
23 const stream = new Readable({
24 objectMode: true,
25 read() {
@@ -45,20 +49,19 @@ export function createDirStream(startPath: string, depth=0) {
49 async function readDir(path: string, depth: number) {
50 if (stopped) return
51 const base = join(startPath, path)
48 - const dir = await opendir(base)
52 const subDirsDone: Promise<any>[] = []
53 let last: DirStreamEntry | undefined = undefined
54 let n = 0
52 - for await (let entry of dir) {
53 - if (stopped) {
54 - await dir.close().catch(() => {}) // only necessary for early exit
55 - break
56 - }
55 + for await (let entry of await readdir(base, { withFileTypes: true })) {
56 + if (stopped) break
57 const stats = entry.isSymbolicLink() && await stat(join(base, entry.name)).catch(() => null)
58 if (stats === null) continue
59 if (stats)
60 entry = new DirentFromStats(entry.name, stats)
61 entry.path = (path && path + '/') + entry.name
62 + const hiddenFiles = await (path && hiddenDeep || hiddenRoot)
63 + if (hiddenFiles && hiddenFiles.includes(entry.path))
64 + continue
65 if (last && closingQ.length) // pending entries
66 last.closingBranch = Promise.resolve(closingQ.shift()!)
67 last = entry
@@ -92,6 +95,13 @@ export function createDirStream(startPath: string, depth=0) {
95 }
96 }
97
98 +async function getWindowsHiddenFiles(path: string, depth=false) {
99 + const out = await runCmd('dir', ['/ah', '/b', depth ? '/s' : '/c', path.replaceAll('/', '\\')]) // cannot pass '', so we pass /c as a noop parameter
100 + .catch(()=>'') // error in case of no matching file
101 + const slice = !depth ? 0 : path.length + (path.at(-1) === '\\' ? 0 : 1)
102 + return out.trimEnd().split('\n').map(x => x.slice(slice).replaceAll('\\', '/'))
103 +}
104 +
105 type DirentStatsKeysIntersection = keyof Dirent & keyof Stats;
106 const kStats = Symbol('stats')
107 // Adapting an internal class in Node.js to mimic the behavior of `Dirent` when creating it manually from `Stats`.
src/util-files.ts
+2 -15
@@ -6,7 +6,6 @@ import { createWriteStream, mkdirSync, watch } from 'fs'
6 import { basename, dirname } from 'path'
7 import glob from 'fast-glob'
8 import { IS_WINDOWS } from './const'
9 -import { runCmd } from './util-os'
9 import { once, Readable } from 'stream'
10 import { createDirStream, DirStreamEntry } from './dirStream'
11 // @ts-ignore
@@ -72,26 +71,14 @@ export function adjustStaticPathForGlob(path: string) {
71 return glob.escapePath(path.replace(/\\/g, '/'))
72 }
73
75 -// wrapper adding a few features: hidden files, onlyFiles and onlyFolders
76 -export async function* dirStream(path: string, { depth=0, onlyFiles=false, onlyFolders = false }={}) {
74 +export async function* dirStream(path: string, { depth=0, onlyFiles=false, onlyFolders = false, hidden=true }={}) {
75 if (!await isDirectory(path))
76 throw Error('ENOTDIR')
79 - const skip = await getItemsToSkip(path)
80 - for await (const entry of createDirStream(path, depth)) {
77 + for await (const entry of createDirStream(path, { depth, hidden })) {
78 const dirent = entry as DirStreamEntry
79 if (dirent.isDirectory() ? onlyFiles : (onlyFolders || !dirent.isFile())) continue
83 - if (skip?.includes(entry.path)) continue
80 yield dirent
81 }
86 -
87 - async function getItemsToSkip(path: string) {
88 - if (!IS_WINDOWS) return
89 - const winPath = path.replace(/\//g, '\\')
90 - const out = await runCmd('dir', ['/ah', '/b', depth ? '/s' : '/c', winPath]) // cannot pass '', so we pass /c as a noop parameter
91 - .catch(()=>'') // error in case of no matching file
92 - return out.split('\n').map(x =>
93 - x.slice(!depth ? 0 : winPath.length + 1).trim().replace(/\\/g, '/'));
94 - }
82 }
83
84 export async function unzip(stream: Readable, cb: (path: string) => Promisable<false | string>) {
src/vfs.ts
+1 -1
@@ -298,7 +298,7 @@ export async function* walkNode(parent: VfsNode, {
298 try {
299 let lastDir = prefixPath.slice(0, -1) || '.'
300 parentsCache.set(lastDir, parent)
301 - for await (const entry of dirStream(source, { depth, onlyFolders })) {
301 + for await (const entry of dirStream(source, { depth, onlyFolders, hidden: false })) {
302 if (ctx?.req.aborted)
303 return
304 const {path} = entry