codekingpro/portable-devtools
115k
1'use strict'
2module.exports = writeFile
3module.exports.sync = writeFileSync
4module.exports._getTmpname = getTmpname // for testing
5module.exports._cleanupOnExit = cleanupOnExit
6
7const fs = require('fs')
8const crypto = require('node:crypto')
9const { onExit } = require('signal-exit')
10const path = require('path')
11const { promisify } = require('util')
12const activeFiles = {}
13
14// if we run inside of a worker_thread, `process.pid` is not unique
15/* istanbul ignore next */
16const threadId = (function getId () {
17 try {
18 const workerThreads = require('worker_threads')
19
20 /// if we are in main thread, this is set to `0`
21 return workerThreads.threadId
22 } catch (e) {
23 // worker_threads are not available, fallback to 0
24 return 0
25 }
26})()
27
28let invocations = 0
29function getTmpname (filename) {
30 return filename + '.' +
31 crypto.createHash('sha1')
32 .update(__filename)
33 .update(String(process.pid))
34 .update(String(threadId))
35 .update(String(++invocations))
36 .digest()
37 .readUInt32BE(0)
38}
39
40function cleanupOnExit (tmpfile) {
41 return () => {
42 try {
43 fs.unlinkSync(typeof tmpfile === 'function' ? tmpfile() : tmpfile)
44 } catch {
45 // ignore errors
46 }
47 }
48}
49
50function serializeActiveFile (absoluteName) {
51 return new Promise(resolve => {
52 // make a queue if it doesn't already exist
53 if (!activeFiles[absoluteName]) {
54 activeFiles[absoluteName] = []
55 }
56
57 activeFiles[absoluteName].push(resolve) // add this job to the queue
58 if (activeFiles[absoluteName].length === 1) {
59 resolve()
60 } // kick off the first one
61 })
62}
63
64// https://github.com/isaacs/node-graceful-fs/blob/master/polyfills.js#L315-L342
65function isChownErrOk (err) {
66 if (err.code === 'ENOSYS') {
67 return true
68 }
69
70 const nonroot = !process.getuid || process.getuid() !== 0
71 if (nonroot) {
72 if (err.code === 'EINVAL' || err.code === 'EPERM') {
73 return true
74 }
75 }
76
77 return false
78}
79
80async function writeFileAsync (filename, data, options = {}) {
81 if (typeof options === 'string') {
82 options = { encoding: options }
83 }
84
85 let fd
86 let tmpfile
87 /* istanbul ignore next -- The closure only gets called when onExit triggers */
88 const removeOnExitHandler = onExit(cleanupOnExit(() => tmpfile))
89 const absoluteName = path.resolve(filename)
90
91 try {
92 await serializeActiveFile(absoluteName)
93 const truename = await promisify(fs.realpath)(filename).catch(() => filename)
94 tmpfile = getTmpname(truename)
95
96 if (!options.mode || !options.chown) {
97 // Either mode or chown is not explicitly set
98 // Default behavior is to copy it from original file
99 const stats = await promisify(fs.stat)(truename).catch(() => {})
100 if (stats) {
101 if (options.mode == null) {
102 options.mode = stats.mode
103 }
104
105 if (options.chown == null && process.getuid) {
106 options.chown = { uid: stats.uid, gid: stats.gid }
107 }
108 }
109 }
110
111 fd = await promisify(fs.open)(tmpfile, 'w', options.mode)
112 if (options.tmpfileCreated) {
113 await options.tmpfileCreated(tmpfile)
114 }
115 if (ArrayBuffer.isView(data)) {
116 await promisify(fs.write)(fd, data, 0, data.length, 0)
117 } else if (data != null) {
118 await promisify(fs.write)(fd, String(data), 0, String(options.encoding || 'utf8'))
119 }
120
121 if (options.fsync !== false) {
122 await promisify(fs.fsync)(fd)
123 }
124
125 await promisify(fs.close)(fd)
126 fd = null
127
128 if (options.chown) {
129 await promisify(fs.chown)(tmpfile, options.chown.uid, options.chown.gid).catch(err => {
130 if (!isChownErrOk(err)) {
131 throw err
132 }
133 })
134 }
135
136 if (options.mode) {
137 await promisify(fs.chmod)(tmpfile, options.mode).catch(err => {
138 if (!isChownErrOk(err)) {
139 throw err
140 }
141 })
142 }
143
144 await promisify(fs.rename)(tmpfile, truename)
145 } finally {
146 if (fd) {
147 await promisify(fs.close)(fd).catch(
148 /* istanbul ignore next */
149 () => {}
150 )
151 }
152 removeOnExitHandler()
153 await promisify(fs.unlink)(tmpfile).catch(() => {})
154 activeFiles[absoluteName].shift() // remove the element added by serializeSameFile
155 if (activeFiles[absoluteName].length > 0) {
156 activeFiles[absoluteName][0]() // start next job if one is pending
157 } else {
158 delete activeFiles[absoluteName]
159 }
160 }
161}
162
163async function writeFile (filename, data, options, callback) {
164 if (options instanceof Function) {
165 callback = options
166 options = {}
167 }
168
169 const promise = writeFileAsync(filename, data, options)
170 if (callback) {
171 try {
172 const result = await promise
173 return callback(result)
174 } catch (err) {
175 return callback(err)
176 }
177 }
178
179 return promise
180}
181
182function writeFileSync (filename, data, options) {
183 if (typeof options === 'string') {
184 options = { encoding: options }
185 } else if (!options) {
186 options = {}
187 }
188 try {
189 filename = fs.realpathSync(filename)
190 } catch (ex) {
191 // it's ok, it'll happen on a not yet existing file
192 }
193 const tmpfile = getTmpname(filename)
194
195 if (!options.mode || !options.chown) {
196 // Either mode or chown is not explicitly set
197 // Default behavior is to copy it from original file
198 try {
199 const stats = fs.statSync(filename)
200 options = Object.assign({}, options)
201 if (!options.mode) {
202 options.mode = stats.mode
203 }
204 if (!options.chown && process.getuid) {
205 options.chown = { uid: stats.uid, gid: stats.gid }
206 }
207 } catch (ex) {
208 // ignore stat errors
209 }
210 }
211
212 let fd
213 const cleanup = cleanupOnExit(tmpfile)
214 const removeOnExitHandler = onExit(cleanup)
215
216 let threw = true
217 try {
218 fd = fs.openSync(tmpfile, 'w', options.mode || 0o666)
219 if (options.tmpfileCreated) {
220 options.tmpfileCreated(tmpfile)
221 }
222 if (ArrayBuffer.isView(data)) {
223 fs.writeSync(fd, data, 0, data.length, 0)
224 } else if (data != null) {
225 fs.writeSync(fd, String(data), 0, String(options.encoding || 'utf8'))
226 }
227 if (options.fsync !== false) {
228 fs.fsyncSync(fd)
229 }
230
231 fs.closeSync(fd)
232 fd = null
233
234 if (options.chown) {
235 try {
236 fs.chownSync(tmpfile, options.chown.uid, options.chown.gid)
237 } catch (err) {
238 if (!isChownErrOk(err)) {
239 throw err
240 }
241 }
242 }
243
244 if (options.mode) {
245 try {
246 fs.chmodSync(tmpfile, options.mode)
247 } catch (err) {
248 if (!isChownErrOk(err)) {
249 throw err
250 }
251 }
252 }
253
254 fs.renameSync(tmpfile, filename)
255 threw = false
256 } finally {
257 if (fd) {
258 try {
259 fs.closeSync(fd)
260 } catch (ex) {
261 // ignore close errors at this stage, error may have closed fd already.
262 }
263 }
264 removeOnExitHandler()
265 if (threw) {
266 cleanup()
267 }
268 }
269}
270 