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