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