FakeFS.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.normalizeLineEndings = exports.BasePortableFakeFS = exports.FakeFS = void 0;
  4. const os_1 = require("os");
  5. const copyPromise_1 = require("./algorithms/copyPromise");
  6. const path_1 = require("./path");
  7. class FakeFS {
  8. constructor(pathUtils) {
  9. this.pathUtils = pathUtils;
  10. }
  11. async *genTraversePromise(init, { stableSort = false } = {}) {
  12. const stack = [init];
  13. while (stack.length > 0) {
  14. const p = stack.shift();
  15. const entry = await this.lstatPromise(p);
  16. if (entry.isDirectory()) {
  17. const entries = await this.readdirPromise(p);
  18. if (stableSort) {
  19. for (const entry of entries.sort()) {
  20. stack.push(this.pathUtils.join(p, entry));
  21. }
  22. }
  23. else {
  24. throw new Error(`Not supported`);
  25. }
  26. }
  27. else {
  28. yield p;
  29. }
  30. }
  31. }
  32. async removePromise(p, { recursive = true, maxRetries = 5 } = {}) {
  33. let stat;
  34. try {
  35. stat = await this.lstatPromise(p);
  36. }
  37. catch (error) {
  38. if (error.code === `ENOENT`) {
  39. return;
  40. }
  41. else {
  42. throw error;
  43. }
  44. }
  45. if (stat.isDirectory()) {
  46. if (recursive) {
  47. const entries = await this.readdirPromise(p);
  48. await Promise.all(entries.map(entry => {
  49. return this.removePromise(this.pathUtils.resolve(p, entry));
  50. }));
  51. }
  52. // 5 gives 1s worth of retries at worst
  53. for (let t = 0; t <= maxRetries; t++) {
  54. try {
  55. await this.rmdirPromise(p);
  56. break;
  57. }
  58. catch (error) {
  59. if (error.code !== `EBUSY` && error.code !== `ENOTEMPTY`) {
  60. throw error;
  61. }
  62. else if (t < maxRetries) {
  63. await new Promise(resolve => setTimeout(resolve, t * 100));
  64. }
  65. }
  66. }
  67. }
  68. else {
  69. await this.unlinkPromise(p);
  70. }
  71. }
  72. removeSync(p, { recursive = true } = {}) {
  73. let stat;
  74. try {
  75. stat = this.lstatSync(p);
  76. }
  77. catch (error) {
  78. if (error.code === `ENOENT`) {
  79. return;
  80. }
  81. else {
  82. throw error;
  83. }
  84. }
  85. if (stat.isDirectory()) {
  86. if (recursive)
  87. for (const entry of this.readdirSync(p))
  88. this.removeSync(this.pathUtils.resolve(p, entry));
  89. this.rmdirSync(p);
  90. }
  91. else {
  92. this.unlinkSync(p);
  93. }
  94. }
  95. async mkdirpPromise(p, { chmod, utimes } = {}) {
  96. p = this.resolve(p);
  97. if (p === this.pathUtils.dirname(p))
  98. return undefined;
  99. const parts = p.split(this.pathUtils.sep);
  100. let createdDirectory;
  101. for (let u = 2; u <= parts.length; ++u) {
  102. const subPath = parts.slice(0, u).join(this.pathUtils.sep);
  103. if (!this.existsSync(subPath)) {
  104. try {
  105. await this.mkdirPromise(subPath);
  106. }
  107. catch (error) {
  108. if (error.code === `EEXIST`) {
  109. continue;
  110. }
  111. else {
  112. throw error;
  113. }
  114. }
  115. createdDirectory !== null && createdDirectory !== void 0 ? createdDirectory : (createdDirectory = subPath);
  116. if (chmod != null)
  117. await this.chmodPromise(subPath, chmod);
  118. if (utimes != null) {
  119. await this.utimesPromise(subPath, utimes[0], utimes[1]);
  120. }
  121. else {
  122. const parentStat = await this.statPromise(this.pathUtils.dirname(subPath));
  123. await this.utimesPromise(subPath, parentStat.atime, parentStat.mtime);
  124. }
  125. }
  126. }
  127. return createdDirectory;
  128. }
  129. mkdirpSync(p, { chmod, utimes } = {}) {
  130. p = this.resolve(p);
  131. if (p === this.pathUtils.dirname(p))
  132. return undefined;
  133. const parts = p.split(this.pathUtils.sep);
  134. let createdDirectory;
  135. for (let u = 2; u <= parts.length; ++u) {
  136. const subPath = parts.slice(0, u).join(this.pathUtils.sep);
  137. if (!this.existsSync(subPath)) {
  138. try {
  139. this.mkdirSync(subPath);
  140. }
  141. catch (error) {
  142. if (error.code === `EEXIST`) {
  143. continue;
  144. }
  145. else {
  146. throw error;
  147. }
  148. }
  149. createdDirectory !== null && createdDirectory !== void 0 ? createdDirectory : (createdDirectory = subPath);
  150. if (chmod != null)
  151. this.chmodSync(subPath, chmod);
  152. if (utimes != null) {
  153. this.utimesSync(subPath, utimes[0], utimes[1]);
  154. }
  155. else {
  156. const parentStat = this.statSync(this.pathUtils.dirname(subPath));
  157. this.utimesSync(subPath, parentStat.atime, parentStat.mtime);
  158. }
  159. }
  160. }
  161. return createdDirectory;
  162. }
  163. async copyPromise(destination, source, { baseFs = this, overwrite = true, stableSort = false, stableTime = false, linkStrategy = null } = {}) {
  164. return await (0, copyPromise_1.copyPromise)(this, destination, baseFs, source, { overwrite, stableSort, stableTime, linkStrategy });
  165. }
  166. copySync(destination, source, { baseFs = this, overwrite = true } = {}) {
  167. const stat = baseFs.lstatSync(source);
  168. const exists = this.existsSync(destination);
  169. if (stat.isDirectory()) {
  170. this.mkdirpSync(destination);
  171. const directoryListing = baseFs.readdirSync(source);
  172. for (const entry of directoryListing) {
  173. this.copySync(this.pathUtils.join(destination, entry), baseFs.pathUtils.join(source, entry), { baseFs, overwrite });
  174. }
  175. }
  176. else if (stat.isFile()) {
  177. if (!exists || overwrite) {
  178. if (exists)
  179. this.removeSync(destination);
  180. const content = baseFs.readFileSync(source);
  181. this.writeFileSync(destination, content);
  182. }
  183. }
  184. else if (stat.isSymbolicLink()) {
  185. if (!exists || overwrite) {
  186. if (exists)
  187. this.removeSync(destination);
  188. const target = baseFs.readlinkSync(source);
  189. this.symlinkSync((0, path_1.convertPath)(this.pathUtils, target), destination);
  190. }
  191. }
  192. else {
  193. throw new Error(`Unsupported file type (file: ${source}, mode: 0o${stat.mode.toString(8).padStart(6, `0`)})`);
  194. }
  195. const mode = stat.mode & 0o777;
  196. this.chmodSync(destination, mode);
  197. }
  198. async changeFilePromise(p, content, opts = {}) {
  199. if (Buffer.isBuffer(content)) {
  200. return this.changeFileBufferPromise(p, content, opts);
  201. }
  202. else {
  203. return this.changeFileTextPromise(p, content, opts);
  204. }
  205. }
  206. async changeFileBufferPromise(p, content, { mode } = {}) {
  207. let current = Buffer.alloc(0);
  208. try {
  209. current = await this.readFilePromise(p);
  210. }
  211. catch (error) {
  212. // ignore errors, no big deal
  213. }
  214. if (Buffer.compare(current, content) === 0)
  215. return;
  216. await this.writeFilePromise(p, content, { mode });
  217. }
  218. async changeFileTextPromise(p, content, { automaticNewlines, mode } = {}) {
  219. let current = ``;
  220. try {
  221. current = await this.readFilePromise(p, `utf8`);
  222. }
  223. catch (error) {
  224. // ignore errors, no big deal
  225. }
  226. const normalizedContent = automaticNewlines
  227. ? normalizeLineEndings(current, content)
  228. : content;
  229. if (current === normalizedContent)
  230. return;
  231. await this.writeFilePromise(p, normalizedContent, { mode });
  232. }
  233. changeFileSync(p, content, opts = {}) {
  234. if (Buffer.isBuffer(content)) {
  235. return this.changeFileBufferSync(p, content, opts);
  236. }
  237. else {
  238. return this.changeFileTextSync(p, content, opts);
  239. }
  240. }
  241. changeFileBufferSync(p, content, { mode } = {}) {
  242. let current = Buffer.alloc(0);
  243. try {
  244. current = this.readFileSync(p);
  245. }
  246. catch (error) {
  247. // ignore errors, no big deal
  248. }
  249. if (Buffer.compare(current, content) === 0)
  250. return;
  251. this.writeFileSync(p, content, { mode });
  252. }
  253. changeFileTextSync(p, content, { automaticNewlines = false, mode } = {}) {
  254. let current = ``;
  255. try {
  256. current = this.readFileSync(p, `utf8`);
  257. }
  258. catch (error) {
  259. // ignore errors, no big deal
  260. }
  261. const normalizedContent = automaticNewlines
  262. ? normalizeLineEndings(current, content)
  263. : content;
  264. if (current === normalizedContent)
  265. return;
  266. this.writeFileSync(p, normalizedContent, { mode });
  267. }
  268. async movePromise(fromP, toP) {
  269. try {
  270. await this.renamePromise(fromP, toP);
  271. }
  272. catch (error) {
  273. if (error.code === `EXDEV`) {
  274. await this.copyPromise(toP, fromP);
  275. await this.removePromise(fromP);
  276. }
  277. else {
  278. throw error;
  279. }
  280. }
  281. }
  282. moveSync(fromP, toP) {
  283. try {
  284. this.renameSync(fromP, toP);
  285. }
  286. catch (error) {
  287. if (error.code === `EXDEV`) {
  288. this.copySync(toP, fromP);
  289. this.removeSync(fromP);
  290. }
  291. else {
  292. throw error;
  293. }
  294. }
  295. }
  296. async lockPromise(affectedPath, callback) {
  297. const lockPath = `${affectedPath}.flock`;
  298. const interval = 1000 / 60;
  299. const startTime = Date.now();
  300. let fd = null;
  301. // Even when we detect that a lock file exists, we still look inside to see
  302. // whether the pid that created it is still alive. It's not foolproof
  303. // (there are false positive), but there are no false negative and that's
  304. // all that matters in 99% of the cases.
  305. const isAlive = async () => {
  306. let pid;
  307. try {
  308. ([pid] = await this.readJsonPromise(lockPath));
  309. }
  310. catch (error) {
  311. // If we can't read the file repeatedly, we assume the process was
  312. // aborted before even writing finishing writing the payload.
  313. return Date.now() - startTime < 500;
  314. }
  315. try {
  316. // "As a special case, a signal of 0 can be used to test for the
  317. // existence of a process" - so we check whether it's alive.
  318. process.kill(pid, 0);
  319. return true;
  320. }
  321. catch (error) {
  322. return false;
  323. }
  324. };
  325. while (fd === null) {
  326. try {
  327. fd = await this.openPromise(lockPath, `wx`);
  328. }
  329. catch (error) {
  330. if (error.code === `EEXIST`) {
  331. if (!await isAlive()) {
  332. try {
  333. await this.unlinkPromise(lockPath);
  334. continue;
  335. }
  336. catch (error) {
  337. // No big deal if we can't remove it. Just fallback to wait for
  338. // it to be eventually released by its owner.
  339. }
  340. }
  341. if (Date.now() - startTime < 60 * 1000) {
  342. await new Promise(resolve => setTimeout(resolve, interval));
  343. }
  344. else {
  345. throw new Error(`Couldn't acquire a lock in a reasonable time (via ${lockPath})`);
  346. }
  347. }
  348. else {
  349. throw error;
  350. }
  351. }
  352. }
  353. await this.writePromise(fd, JSON.stringify([process.pid]));
  354. try {
  355. return await callback();
  356. }
  357. finally {
  358. try {
  359. // closePromise needs to come before unlinkPromise otherwise another process can attempt
  360. // to get the file handle after the unlink but before close resuling in
  361. // EPERM: operation not permitted, open
  362. await this.closePromise(fd);
  363. await this.unlinkPromise(lockPath);
  364. }
  365. catch (error) {
  366. // noop
  367. }
  368. }
  369. }
  370. async readJsonPromise(p) {
  371. const content = await this.readFilePromise(p, `utf8`);
  372. try {
  373. return JSON.parse(content);
  374. }
  375. catch (error) {
  376. error.message += ` (in ${p})`;
  377. throw error;
  378. }
  379. }
  380. readJsonSync(p) {
  381. const content = this.readFileSync(p, `utf8`);
  382. try {
  383. return JSON.parse(content);
  384. }
  385. catch (error) {
  386. error.message += ` (in ${p})`;
  387. throw error;
  388. }
  389. }
  390. async writeJsonPromise(p, data) {
  391. return await this.writeFilePromise(p, `${JSON.stringify(data, null, 2)}\n`);
  392. }
  393. writeJsonSync(p, data) {
  394. return this.writeFileSync(p, `${JSON.stringify(data, null, 2)}\n`);
  395. }
  396. async preserveTimePromise(p, cb) {
  397. const stat = await this.lstatPromise(p);
  398. const result = await cb();
  399. if (typeof result !== `undefined`)
  400. p = result;
  401. if (this.lutimesPromise) {
  402. await this.lutimesPromise(p, stat.atime, stat.mtime);
  403. }
  404. else if (!stat.isSymbolicLink()) {
  405. await this.utimesPromise(p, stat.atime, stat.mtime);
  406. }
  407. }
  408. async preserveTimeSync(p, cb) {
  409. const stat = this.lstatSync(p);
  410. const result = cb();
  411. if (typeof result !== `undefined`)
  412. p = result;
  413. if (this.lutimesSync) {
  414. this.lutimesSync(p, stat.atime, stat.mtime);
  415. }
  416. else if (!stat.isSymbolicLink()) {
  417. this.utimesSync(p, stat.atime, stat.mtime);
  418. }
  419. }
  420. }
  421. exports.FakeFS = FakeFS;
  422. class BasePortableFakeFS extends FakeFS {
  423. constructor() {
  424. super(path_1.ppath);
  425. }
  426. }
  427. exports.BasePortableFakeFS = BasePortableFakeFS;
  428. function getEndOfLine(content) {
  429. const matches = content.match(/\r?\n/g);
  430. if (matches === null)
  431. return os_1.EOL;
  432. const crlf = matches.filter(nl => nl === `\r\n`).length;
  433. const lf = matches.length - crlf;
  434. return crlf > lf ? `\r\n` : `\n`;
  435. }
  436. function normalizeLineEndings(originalContent, newContent) {
  437. return newContent.replace(/\r?\n/g, getEndOfLine(originalContent));
  438. }
  439. exports.normalizeLineEndings = normalizeLineEndings;