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 }