rimraf-move-remove.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. // https://youtu.be/uhRWMGBjlO8?t=537
  2. //
  3. // 1. readdir
  4. // 2. for each entry
  5. // a. if a non-empty directory, recurse
  6. // b. if an empty directory, move to random hidden file name in $TEMP
  7. // c. unlink/rmdir $TEMP
  8. //
  9. // This works around the fact that unlink/rmdir is non-atomic and takes
  10. // a non-deterministic amount of time to complete.
  11. //
  12. // However, it is HELLA SLOW, like 2-10x slower than a naive recursive rm.
  13. import { basename, parse, resolve } from 'path';
  14. import { defaultTmp, defaultTmpSync } from './default-tmp.js';
  15. import { ignoreENOENT, ignoreENOENTSync } from './ignore-enoent.js';
  16. import { chmodSync, lstatSync, promises as fsPromises, renameSync, rmdirSync, unlinkSync, } from './fs.js';
  17. const { lstat, rename, unlink, rmdir, chmod } = fsPromises;
  18. import { readdirOrError, readdirOrErrorSync } from './readdir-or-error.js';
  19. // crypto.randomBytes is much slower, and Math.random() is enough here
  20. const uniqueFilename = (path) => `.${basename(path)}.${Math.random()}`;
  21. const unlinkFixEPERM = async (path) => unlink(path).catch((er) => {
  22. if (er.code === 'EPERM') {
  23. return chmod(path, 0o666).then(() => unlink(path), er2 => {
  24. if (er2.code === 'ENOENT') {
  25. return;
  26. }
  27. throw er;
  28. });
  29. }
  30. else if (er.code === 'ENOENT') {
  31. return;
  32. }
  33. throw er;
  34. });
  35. const unlinkFixEPERMSync = (path) => {
  36. try {
  37. unlinkSync(path);
  38. }
  39. catch (er) {
  40. if (er?.code === 'EPERM') {
  41. try {
  42. return chmodSync(path, 0o666);
  43. }
  44. catch (er2) {
  45. if (er2?.code === 'ENOENT') {
  46. return;
  47. }
  48. throw er;
  49. }
  50. }
  51. else if (er?.code === 'ENOENT') {
  52. return;
  53. }
  54. throw er;
  55. }
  56. };
  57. export const rimrafMoveRemove = async (path, opt) => {
  58. if (opt?.signal?.aborted) {
  59. throw opt.signal.reason;
  60. }
  61. try {
  62. return await rimrafMoveRemoveDir(path, opt, await lstat(path));
  63. }
  64. catch (er) {
  65. if (er?.code === 'ENOENT')
  66. return true;
  67. throw er;
  68. }
  69. };
  70. const rimrafMoveRemoveDir = async (path, opt, ent) => {
  71. if (opt?.signal?.aborted) {
  72. throw opt.signal.reason;
  73. }
  74. if (!opt.tmp) {
  75. return rimrafMoveRemoveDir(path, { ...opt, tmp: await defaultTmp(path) }, ent);
  76. }
  77. if (path === opt.tmp && parse(path).root !== path) {
  78. throw new Error('cannot delete temp directory used for deletion');
  79. }
  80. const entries = ent.isDirectory() ? await readdirOrError(path) : null;
  81. if (!Array.isArray(entries)) {
  82. // this can only happen if lstat/readdir lied, or if the dir was
  83. // swapped out with a file at just the right moment.
  84. /* c8 ignore start */
  85. if (entries) {
  86. if (entries.code === 'ENOENT') {
  87. return true;
  88. }
  89. if (entries.code !== 'ENOTDIR') {
  90. throw entries;
  91. }
  92. }
  93. /* c8 ignore stop */
  94. if (opt.filter && !(await opt.filter(path, ent))) {
  95. return false;
  96. }
  97. await ignoreENOENT(tmpUnlink(path, opt.tmp, unlinkFixEPERM));
  98. return true;
  99. }
  100. const removedAll = (await Promise.all(entries.map(ent => rimrafMoveRemoveDir(resolve(path, ent.name), opt, ent)))).reduce((a, b) => a && b, true);
  101. if (!removedAll) {
  102. return false;
  103. }
  104. // we don't ever ACTUALLY try to unlink /, because that can never work
  105. // but when preserveRoot is false, we could be operating on it.
  106. // No need to check if preserveRoot is not false.
  107. if (opt.preserveRoot === false && path === parse(path).root) {
  108. return false;
  109. }
  110. if (opt.filter && !(await opt.filter(path, ent))) {
  111. return false;
  112. }
  113. await ignoreENOENT(tmpUnlink(path, opt.tmp, rmdir));
  114. return true;
  115. };
  116. const tmpUnlink = async (path, tmp, rm) => {
  117. const tmpFile = resolve(tmp, uniqueFilename(path));
  118. await rename(path, tmpFile);
  119. return await rm(tmpFile);
  120. };
  121. export const rimrafMoveRemoveSync = (path, opt) => {
  122. if (opt?.signal?.aborted) {
  123. throw opt.signal.reason;
  124. }
  125. try {
  126. return rimrafMoveRemoveDirSync(path, opt, lstatSync(path));
  127. }
  128. catch (er) {
  129. if (er?.code === 'ENOENT')
  130. return true;
  131. throw er;
  132. }
  133. };
  134. const rimrafMoveRemoveDirSync = (path, opt, ent) => {
  135. if (opt?.signal?.aborted) {
  136. throw opt.signal.reason;
  137. }
  138. if (!opt.tmp) {
  139. return rimrafMoveRemoveDirSync(path, { ...opt, tmp: defaultTmpSync(path) }, ent);
  140. }
  141. const tmp = opt.tmp;
  142. if (path === opt.tmp && parse(path).root !== path) {
  143. throw new Error('cannot delete temp directory used for deletion');
  144. }
  145. const entries = ent.isDirectory() ? readdirOrErrorSync(path) : null;
  146. if (!Array.isArray(entries)) {
  147. // this can only happen if lstat/readdir lied, or if the dir was
  148. // swapped out with a file at just the right moment.
  149. /* c8 ignore start */
  150. if (entries) {
  151. if (entries.code === 'ENOENT') {
  152. return true;
  153. }
  154. if (entries.code !== 'ENOTDIR') {
  155. throw entries;
  156. }
  157. }
  158. /* c8 ignore stop */
  159. if (opt.filter && !opt.filter(path, ent)) {
  160. return false;
  161. }
  162. ignoreENOENTSync(() => tmpUnlinkSync(path, tmp, unlinkFixEPERMSync));
  163. return true;
  164. }
  165. let removedAll = true;
  166. for (const ent of entries) {
  167. const p = resolve(path, ent.name);
  168. removedAll = rimrafMoveRemoveDirSync(p, opt, ent) && removedAll;
  169. }
  170. if (!removedAll) {
  171. return false;
  172. }
  173. if (opt.preserveRoot === false && path === parse(path).root) {
  174. return false;
  175. }
  176. if (opt.filter && !opt.filter(path, ent)) {
  177. return false;
  178. }
  179. ignoreENOENTSync(() => tmpUnlinkSync(path, tmp, rmdirSync));
  180. return true;
  181. };
  182. const tmpUnlinkSync = (path, tmp, rmSync) => {
  183. const tmpFile = resolve(tmp, uniqueFilename(path));
  184. renameSync(path, tmpFile);
  185. return rmSync(tmpFile);
  186. };
  187. //# sourceMappingURL=rimraf-move-remove.js.map