UNPKG

6.57 kBJavaScriptView Raw
1// This is a fork of https://github.com/npm/write-file-atomic v2.3.0
2// with graceful-fs replaced with fs to avoid memory leak during testing
3// See: https://github.com/stylelint/stylelint/pull/2992
4
5"use strict";
6module.exports = writeFile;
7module.exports.sync = writeFileSync;
8module.exports._getTmpname = getTmpname; // for testing
9module.exports._cleanupOnExit = cleanupOnExit;
10
11var fs = require("fs");
12var MurmurHash3 = require("imurmurhash");
13var onExit = require("signal-exit");
14var path = require("path");
15var activeFiles = {};
16
17var invocations = 0;
18function getTmpname(filename) {
19 return (
20 filename +
21 "." +
22 MurmurHash3(__filename)
23 .hash(String(process.pid))
24 .hash(String(++invocations))
25 .result()
26 );
27}
28
29function cleanupOnExit(tmpfile) {
30 return function() {
31 try {
32 fs.unlinkSync(typeof tmpfile === "function" ? tmpfile() : tmpfile);
33 } catch (_) {}
34 };
35}
36
37function writeFile(filename, data, options, callback) {
38 if (options instanceof Function) {
39 callback = options;
40 options = null;
41 }
42 if (!options) options = {};
43
44 var Promise = options.Promise || global.Promise;
45 var truename;
46 var fd;
47 var tmpfile;
48 var removeOnExit = cleanupOnExit(() => tmpfile);
49 var absoluteName = path.resolve(filename);
50
51 new Promise(function serializeSameFile(resolve) {
52 // make a queue if it doesn't already exist
53 if (!activeFiles[absoluteName]) activeFiles[absoluteName] = [];
54
55 activeFiles[absoluteName].push(resolve); // add this job to the queue
56 if (activeFiles[absoluteName].length === 1) resolve(); // kick off the first one
57 })
58 .then(function getRealPath() {
59 return new Promise(function(resolve) {
60 fs.realpath(filename, function(_, realname) {
61 truename = realname || filename;
62 tmpfile = getTmpname(truename);
63 resolve();
64 });
65 });
66 })
67 .then(function stat() {
68 return new Promise(function stat(resolve) {
69 if (options.mode && options.chown) resolve();
70 else {
71 // Either mode or chown is not explicitly set
72 // Default behavior is to copy it from original file
73 fs.stat(truename, function(err, stats) {
74 if (err || !stats) resolve();
75 else {
76 options = Object.assign({}, options);
77
78 if (!options.mode) {
79 options.mode = stats.mode;
80 }
81 if (!options.chown && process.getuid) {
82 options.chown = { uid: stats.uid, gid: stats.gid };
83 }
84 resolve();
85 }
86 });
87 }
88 });
89 })
90 .then(function thenWriteFile() {
91 return new Promise(function(resolve, reject) {
92 fs.open(tmpfile, "w", options.mode, function(err, _fd) {
93 fd = _fd;
94 if (err) reject(err);
95 else resolve();
96 });
97 });
98 })
99 .then(function write() {
100 return new Promise(function(resolve, reject) {
101 if (Buffer.isBuffer(data)) {
102 fs.write(fd, data, 0, data.length, 0, function(err) {
103 if (err) reject(err);
104 else resolve();
105 });
106 } else if (data != null) {
107 fs.write(
108 fd,
109 String(data),
110 0,
111 String(options.encoding || "utf8"),
112 function(err) {
113 if (err) reject(err);
114 else resolve();
115 }
116 );
117 } else resolve();
118 });
119 })
120 .then(function syncAndClose() {
121 if (options.fsync !== false) {
122 return new Promise(function(resolve, reject) {
123 fs.fsync(fd, function(err) {
124 if (err) reject(err);
125 else fs.close(fd, resolve);
126 });
127 });
128 }
129 })
130 .then(function chown() {
131 if (options.chown) {
132 return new Promise(function(resolve, reject) {
133 fs.chown(tmpfile, options.chown.uid, options.chown.gid, function(
134 err
135 ) {
136 if (err) reject(err);
137 else resolve();
138 });
139 });
140 }
141 })
142 .then(function chmod() {
143 if (options.mode) {
144 return new Promise(function(resolve, reject) {
145 fs.chmod(tmpfile, options.mode, function(err) {
146 if (err) reject(err);
147 else resolve();
148 });
149 });
150 }
151 })
152 .then(function rename() {
153 return new Promise(function(resolve, reject) {
154 fs.rename(tmpfile, truename, function(err) {
155 if (err) reject(err);
156 else resolve();
157 });
158 });
159 })
160 .then(function success() {
161 removeOnExit();
162 callback();
163 })
164 .catch(function fail(err) {
165 removeOnExit();
166 fs.unlink(tmpfile, function() {
167 callback(err);
168 });
169 })
170 .then(function checkQueue() {
171 activeFiles[absoluteName].shift(); // remove the element added by serializeSameFile
172 if (activeFiles[absoluteName].length > 0) {
173 activeFiles[absoluteName][0](); // start next job if one is pending
174 } else delete activeFiles[absoluteName];
175 });
176}
177
178function writeFileSync(filename, data, options) {
179 if (!options) options = {};
180 try {
181 filename = fs.realpathSync(filename);
182 } catch (ex) {
183 // it's ok, it'll happen on a not yet existing file
184 }
185 var tmpfile = getTmpname(filename);
186
187 try {
188 if (!options.mode || !options.chown) {
189 // Either mode or chown is not explicitly set
190 // Default behavior is to copy it from original file
191 try {
192 var stats = fs.statSync(filename);
193 options = Object.assign({}, options);
194 if (!options.mode) {
195 options.mode = stats.mode;
196 }
197 if (!options.chown && process.getuid) {
198 options.chown = { uid: stats.uid, gid: stats.gid };
199 }
200 } catch (ex) {
201 // ignore stat errors
202 }
203 }
204
205 var removeOnExit = onExit(cleanupOnExit(tmpfile));
206 var fd = fs.openSync(tmpfile, "w", options.mode);
207 if (Buffer.isBuffer(data)) {
208 fs.writeSync(fd, data, 0, data.length, 0);
209 } else if (data != null) {
210 fs.writeSync(fd, String(data), 0, String(options.encoding || "utf8"));
211 }
212 if (options.fsync !== false) {
213 fs.fsyncSync(fd);
214 }
215 fs.closeSync(fd);
216 if (options.chown)
217 fs.chownSync(tmpfile, options.chown.uid, options.chown.gid);
218 if (options.mode) fs.chmodSync(tmpfile, options.mode);
219 fs.renameSync(tmpfile, filename);
220 removeOnExit();
221 } catch (err) {
222 removeOnExit();
223 try {
224 fs.unlinkSync(tmpfile);
225 } catch (e) {}
226 throw err;
227 }
228}