fix: set-comment fails if descript.ion file is hidden on Windows
Massimo Melina committed
May 21, 2025 at 22:20 UTC
5acc5e0aee8a18c05fd2740d1627d820d773c1cc
3 files changed
+29
-8
src/comments.ts
+3
-3
@@ -1,8 +1,7 @@
1
import { defineConfig } from './config'
2
import { dirname, basename, join } from 'path'
3
import { CFG } from './cross'
4
-import { parseFileContent, parseFileCache } from './util-files'
5
-import { createWriteStream } from 'fs'
4
+import { parseFileContent, parseFileCache, safeWriteStream } from './util-files'
5
import { loadFileAttr, singleWorkerFromBatchWorker, storeFileAttr } from './misc'
6
import _ from 'lodash'
7
import iconv from 'iconv-lite'
@@ -56,7 +55,7 @@ const setCommentDescriptIon = singleWorkerFromBatchWorker(async (jobs: [path: st
55
if (!comments.size)
56
return unlink(path)
57
// encode comments in descript.ion format
59
- const ws = createWriteStream(path)
58
+ const ws = await safeWriteStream(path)
59
comments.forEach((comment, filename) => {
60
const multiline = comment.includes('\n')
61
const line = (filename.includes(' ') ? `"${filename}"` : filename)
@@ -66,6 +65,7 @@ const setCommentDescriptIon = singleWorkerFromBatchWorker(async (jobs: [path: st
65
ws.write(MULTILINE_SUFFIX, 'binary')
66
ws.write('\n')
67
})
68
+ await new Promise(res => ws.end(res))
69
}))
70
})
71
src/log.ts
+1
-1
@@ -196,6 +196,6 @@ debugLogFile.once('open', () => {
196
repeat(DAY, () => { // do it sync, to avoid overlapping
197
if (statSync(path).size < LIMIT) return // no need
198
renameSync(path, 'old-' + path)
199
- debugLogFile = createWriteStream(path, { flags: 'w' }) // new file
199
+ debugLogFile = createWriteStream(path) // new file
200
})
201
}).on('error', () => console.log("cannot create debug.log"))
src/util-files.ts
+25
-4
@@ -2,7 +2,7 @@
2
3
import { access, mkdir, readFile, stat } from 'fs/promises'
4
import { Promisable, try_, wait, isWindowsDrive } from './misc'
5
-import { createWriteStream, mkdirSync, watch } from 'fs'
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'
@@ -85,8 +85,7 @@ export async function unzip(stream: Readable, cb: (path: string) => Promisable<f
85
if (!dest || type !== 'File')
86
return entry.autodrain()
87
console.debug('unzip', dest)
88
- await prepareFolder(dest)
89
- const thisFile = entry.pipe(createWriteStream(dest).on('error', reject))
88
+ const thisFile = entry.pipe(await safeWriteStream(dest))
89
await once(thisFile, 'finish')
90
}) )
91
)
@@ -107,7 +106,7 @@ export async function prepareFolder(path: string, dirnameIt=true) {
106
107
export function createFileWithPath(path: string, options?: Parameters<typeof createWriteStream>[1]) {
108
const folder = dirname(path)
110
- if (!isWindowsDrive(folder))
109
+ if (!isWindowsDrive(folder)) // can't use prepareFolder because it's async
110
try { mkdirSync(folder, { recursive: true }) }
111
catch {
112
return
@@ -115,6 +114,28 @@ export function createFileWithPath(path: string, options?: Parameters<typeof cre
114
return createWriteStream(path, options)
115
}
116
117
+export async function safeWriteStream(path: string, options?: Parameters<typeof createWriteStream>[1]) {
118
+ await prepareFolder(path)
119
+ return new Promise<ReturnType<typeof createWriteStream>>((resolve, reject) => {
120
+ const first = createWriteStream(path, options)
121
+ .on('open', () => resolve(first))
122
+ .on('error', (e: any) => {
123
+ if (!IS_WINDOWS || e.code !== 'EPERM') // Windows throws EPERM for hidden files with flags 'w' and 'a'
124
+ return reject(e)
125
+ if (typeof options === 'string')
126
+ options = { encoding: options }
127
+ else
128
+ options ||= {}
129
+ if (options.flags && options.flags !== 'w') // we only handle the 'w' case
130
+ return reject(e)
131
+ options.flags = 'r+'
132
+ const second = createWriteStream(path, options)
133
+ .on('open', fd => ftruncate(fd, 0, () => resolve(second)))
134
+ .on('error', reject)
135
+ })
136
+ })
137
+}
138
+
139
export function isValidFileName(name: string) {
140
return !(IS_WINDOWS ? /[/:"*?<>|\\]/ : /\//).test(name) && !dirTraversal(name)
141
}