@ix-xs/node-comfort 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +135 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/Arr.js +13 -5
- package/src/Checker.js +56 -8
- package/src/Cli.js +60 -18
- package/src/Color.js +32 -6
- package/src/Crypto.js +429 -78
- package/src/Emitter.js +9 -9
- package/src/Env.js +90 -8
- package/src/FS.js +366 -56
- package/src/Http.js +263 -72
- package/src/Id.js +53 -15
- package/src/Logger.js +100 -22
- package/src/Num.js +2 -1
- package/src/Obj.js +225 -36
- package/src/SQLite.js +154 -97
- package/src/Schema.js +281 -40
- package/src/Str.js +77 -29
- package/src/Sys.js +123 -22
- package/src/Time.js +96 -22
- package/src/Utils.js +18 -3
- package/src/errors.js +75 -4
- package/src/internal/shared.js +68 -14
- package/src/internal/terminal.js +49 -0
- package/types/index.d.ts +28 -5
- package/types/src/Arr.d.ts +4 -2
- package/types/src/Arr.d.ts.map +1 -1
- package/types/src/Checker.d.ts +29 -3
- package/types/src/Checker.d.ts.map +1 -1
- package/types/src/Cli.d.ts +6 -4
- package/types/src/Cli.d.ts.map +1 -1
- package/types/src/Color.d.ts +2 -2
- package/types/src/Color.d.ts.map +1 -1
- package/types/src/Crypto.d.ts +126 -28
- package/types/src/Crypto.d.ts.map +1 -1
- package/types/src/Emitter.d.ts +6 -6
- package/types/src/Emitter.d.ts.map +1 -1
- package/types/src/Env.d.ts +19 -1
- package/types/src/Env.d.ts.map +1 -1
- package/types/src/FS.d.ts +27 -20
- package/types/src/FS.d.ts.map +1 -1
- package/types/src/Http.d.ts +42 -5
- package/types/src/Http.d.ts.map +1 -1
- package/types/src/Id.d.ts.map +1 -1
- package/types/src/Logger.d.ts +11 -3
- package/types/src/Logger.d.ts.map +1 -1
- package/types/src/Num.d.ts.map +1 -1
- package/types/src/Obj.d.ts +15 -2
- package/types/src/Obj.d.ts.map +1 -1
- package/types/src/SQLite.d.ts +104 -12
- package/types/src/SQLite.d.ts.map +1 -1
- package/types/src/Schema.d.ts +61 -5
- package/types/src/Schema.d.ts.map +1 -1
- package/types/src/Str.d.ts +32 -7
- package/types/src/Str.d.ts.map +1 -1
- package/types/src/Sys.d.ts +42 -9
- package/types/src/Sys.d.ts.map +1 -1
- package/types/src/Time.d.ts +10 -5
- package/types/src/Time.d.ts.map +1 -1
- package/types/src/Utils.d.ts +16 -6
- package/types/src/Utils.d.ts.map +1 -1
- package/types/src/errors.d.ts +6 -2
- package/types/src/errors.d.ts.map +1 -1
- package/types/src/internal/shared.d.ts +7 -0
- package/types/src/internal/shared.d.ts.map +1 -1
- package/types/src/internal/terminal.d.ts +13 -0
- package/types/src/internal/terminal.d.ts.map +1 -0
package/src/FS.js
CHANGED
|
@@ -135,8 +135,12 @@ const { sleepSync, toMs } = require("./internal/shared.js");
|
|
|
135
135
|
*/
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
* Returned by `watch()`.
|
|
139
|
-
* @typedef {
|
|
138
|
+
* Returned by `watch()`. With `using`, the watch stops at the end of the block.
|
|
139
|
+
* @typedef {_WatcherMethods & Disposable} Watcher
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @typedef {object} _WatcherMethods
|
|
140
144
|
* @property {<E extends "change" | "rename" | "all" | "error">(event: E, callback: E extends "all" ? (event: "change" | "rename", file: string) => void : E extends "error" ? (error: Error) => void : (file: string) => void) => Watcher} on Listens for changes. `"all"` also gets the event name. `"error"` tells you the watch broke, for instance because the folder was deleted.
|
|
141
145
|
* @property {() => void} stop Stops watching.
|
|
142
146
|
* @property {() => Watcher} pause Ignores events until `resume()`.
|
|
@@ -301,19 +305,195 @@ const _write = (target, content, options) => {
|
|
|
301
305
|
mode = undefined;
|
|
302
306
|
}
|
|
303
307
|
}
|
|
304
|
-
|
|
308
|
+
// A name nobody can guess, created only if nothing is there ("wx"): a link planted in advance can't redirect the write.
|
|
309
|
+
const temp = `${target}.${require("node:crypto").randomBytes(8).toString("hex")}.tmp`;
|
|
310
|
+
/** @type {number | undefined} */
|
|
311
|
+
let fd;
|
|
312
|
+
let created = false;
|
|
305
313
|
try {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
fs.writeFileSync(
|
|
314
|
+
// Created with its final permissions (the umask applies, as for any new file), never wider.
|
|
315
|
+
fd = fs.openSync(temp, "wx", mode ?? 0o666);
|
|
316
|
+
created = true;
|
|
317
|
+
fs.writeFileSync(fd, content, { encoding });
|
|
318
|
+
if (options.fsync) fs.fsyncSync(fd);
|
|
319
|
+
fs.closeSync(fd);
|
|
320
|
+
fd = undefined;
|
|
310
321
|
_rename(temp, target);
|
|
311
322
|
} catch (error) {
|
|
312
|
-
|
|
323
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
324
|
+
if (created) fs.rmSync(temp, { force: true });
|
|
313
325
|
throw error;
|
|
314
326
|
}
|
|
315
327
|
};
|
|
316
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Does something exist at this path, a broken link included?
|
|
331
|
+
* @param {string} path
|
|
332
|
+
* @returns {boolean}
|
|
333
|
+
*/
|
|
334
|
+
const _lexists = (path) => {
|
|
335
|
+
try {
|
|
336
|
+
fs.lstatSync(path);
|
|
337
|
+
return true;
|
|
338
|
+
} catch {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The real path of a path that may not exist yet: the part that exists is
|
|
345
|
+
* resolved, the rest is added as it is.
|
|
346
|
+
* @param {string} path Absolute.
|
|
347
|
+
* @returns {string}
|
|
348
|
+
*/
|
|
349
|
+
const _realTarget = (path) => {
|
|
350
|
+
/** @type {string[]} */
|
|
351
|
+
const rest = [];
|
|
352
|
+
let existing = path;
|
|
353
|
+
while (!fs.existsSync(existing)) {
|
|
354
|
+
const parent = nodePath.dirname(existing);
|
|
355
|
+
if (parent === existing) return path;
|
|
356
|
+
rest.unshift(nodePath.basename(existing));
|
|
357
|
+
existing = parent;
|
|
358
|
+
}
|
|
359
|
+
return nodePath.join(_realPath(existing), ...rest);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Do both paths lead to the same file or folder? Links, short names and,
|
|
364
|
+
* on Windows, letter case don't fool it.
|
|
365
|
+
* @param {string} a
|
|
366
|
+
* @param {string} b
|
|
367
|
+
* @returns {boolean}
|
|
368
|
+
*/
|
|
369
|
+
const _sameEntry = (a, b) => {
|
|
370
|
+
let x;
|
|
371
|
+
let y;
|
|
372
|
+
try {
|
|
373
|
+
x = fs.statSync(a, { bigint: true });
|
|
374
|
+
y = fs.statSync(b, { bigint: true });
|
|
375
|
+
} catch {
|
|
376
|
+
// one of them doesn't exist
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
// some file systems have no file ids: compare the real paths then
|
|
380
|
+
if (x.ino !== 0n || y.ino !== 0n) return x.dev === y.dev && x.ino === y.ino;
|
|
381
|
+
const [ra, rb] = [_realPath(a), _realPath(b)];
|
|
382
|
+
return process.platform === "win32" ? ra.toLowerCase() === rb.toLowerCase() : ra === rb;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Is `src` a link to `dest`? Renaming it there would put the link in place
|
|
387
|
+
* of the real file. The link itself under another letter case is not.
|
|
388
|
+
* @param {string} src
|
|
389
|
+
* @param {string} dest
|
|
390
|
+
* @returns {boolean}
|
|
391
|
+
*/
|
|
392
|
+
const _linkOnto = (src, dest) => {
|
|
393
|
+
let link;
|
|
394
|
+
let other;
|
|
395
|
+
try {
|
|
396
|
+
link = fs.lstatSync(src, { bigint: true });
|
|
397
|
+
if (!link.isSymbolicLink()) return false;
|
|
398
|
+
other = fs.lstatSync(dest, { bigint: true });
|
|
399
|
+
} catch {
|
|
400
|
+
// no link, or nothing at dest to lose
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
if (link.ino !== 0n && other.dev === link.dev && other.ino === link.ino) return false;
|
|
404
|
+
const [ra, rb] = [_realPath(src), _realPath(dest)];
|
|
405
|
+
return process.platform === "win32" ? ra.toLowerCase() === rb.toLowerCase() : ra === rb;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Is `child` the folder `parent` itself, or somewhere inside it, once links
|
|
410
|
+
* are resolved? Copying or moving a folder there would destroy it.
|
|
411
|
+
* @param {string} child Absolute, may not exist yet.
|
|
412
|
+
* @param {string} parent Absolute, exists.
|
|
413
|
+
* @returns {boolean}
|
|
414
|
+
*/
|
|
415
|
+
const _within = (child, parent) => {
|
|
416
|
+
if (_sameEntry(child, parent)) return true;
|
|
417
|
+
// path.relative ignores letter case on Windows
|
|
418
|
+
const rel = nodePath.relative(_realPath(parent), _realTarget(child));
|
|
419
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${nodePath.sep}`) && !nodePath.isAbsolute(rel));
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Would copying the files of `root` into `dest` copy them onto themselves,
|
|
424
|
+
* or into the tree being read?
|
|
425
|
+
* @param {string} root
|
|
426
|
+
* @param {string} dest
|
|
427
|
+
* @param {boolean} recursive
|
|
428
|
+
* @returns {boolean}
|
|
429
|
+
*/
|
|
430
|
+
const _filesInItself = (root, dest, recursive) => (recursive ? _within(dest, root) : _sameEntry(dest, root));
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Copies a link as a link, never what it points to. On Windows a link to a
|
|
434
|
+
* folder becomes a junction, which needs no special rights. A link that
|
|
435
|
+
* can't be made is left out.
|
|
436
|
+
* @param {string} from
|
|
437
|
+
* @param {string} to
|
|
438
|
+
* @param {boolean} overwrite
|
|
439
|
+
*/
|
|
440
|
+
const _copyLink = (from, to, overwrite) => {
|
|
441
|
+
try {
|
|
442
|
+
if (_lexists(to)) {
|
|
443
|
+
// a real file or folder in the way is kept: only a link is replaced by a link
|
|
444
|
+
if (!overwrite || !fs.lstatSync(to).isSymbolicLink()) return;
|
|
445
|
+
fs.unlinkSync(to);
|
|
446
|
+
}
|
|
447
|
+
const target = fs.readlinkSync(from);
|
|
448
|
+
/** @type {"junction" | "file" | undefined} */
|
|
449
|
+
let type;
|
|
450
|
+
if (process.platform === "win32") {
|
|
451
|
+
try {
|
|
452
|
+
type = fs.statSync(from).isDirectory() ? "junction" : "file";
|
|
453
|
+
} catch {
|
|
454
|
+
type = "file";
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
fs.symlinkSync(target, to, type);
|
|
458
|
+
} catch {
|
|
459
|
+
// left out, never followed
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* `fs.cpSync()` that copies the links inside the tree as links: a junction
|
|
465
|
+
* or a link to a folder outside never brings that folder's files along.
|
|
466
|
+
* Elsewhere than on Windows, `fs.cpSync()` already copies links as links.
|
|
467
|
+
* @param {string} src Absolute.
|
|
468
|
+
* @param {string} dest Absolute.
|
|
469
|
+
* @param {{ force?: boolean, errorOnExist?: boolean, preserveTimestamps?: boolean, filter?: (source: string, destination: string) => boolean }} [options]
|
|
470
|
+
*/
|
|
471
|
+
const _copyTree = (src, dest, options = {}) => {
|
|
472
|
+
const overwrite = options.force ?? true;
|
|
473
|
+
fs.cpSync(src, dest, {
|
|
474
|
+
recursive: true,
|
|
475
|
+
dereference: false,
|
|
476
|
+
force: overwrite,
|
|
477
|
+
errorOnExist: options.errorOnExist ?? false,
|
|
478
|
+
preserveTimestamps: options.preserveTimestamps ?? false,
|
|
479
|
+
// Every platform: cpSync follows junctions on Windows, and elsewhere refuses to copy a link over the same link.
|
|
480
|
+
filter: (s, d) => {
|
|
481
|
+
if (options.filter && !options.filter(s, d)) return false;
|
|
482
|
+
// the source itself was named by the caller: only links inside it are kept as links
|
|
483
|
+
if (nodePath.resolve(s) === src) return true;
|
|
484
|
+
let link;
|
|
485
|
+
try {
|
|
486
|
+
link = fs.lstatSync(s).isSymbolicLink();
|
|
487
|
+
} catch {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
if (!link) return true;
|
|
491
|
+
_copyLink(s, d, overwrite);
|
|
492
|
+
return false;
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
|
|
317
497
|
/**
|
|
318
498
|
* Removes comments and trailing commas from JSONC.
|
|
319
499
|
* @param {string} text
|
|
@@ -680,6 +860,13 @@ function find(name, options = {}) {
|
|
|
680
860
|
return undefined;
|
|
681
861
|
}
|
|
682
862
|
|
|
863
|
+
/**
|
|
864
|
+
* Does this relative path leave its base folder?
|
|
865
|
+
* @param {string} rel From `path.relative()`, which ignores letter case on Windows.
|
|
866
|
+
* @returns {boolean}
|
|
867
|
+
*/
|
|
868
|
+
const _outside = (rel) => rel === ".." || rel.startsWith(`..${nodePath.sep}`) || nodePath.isAbsolute(rel);
|
|
869
|
+
|
|
683
870
|
/**
|
|
684
871
|
* Looks for a file or folder in a folder, then in its parent, and so on up
|
|
685
872
|
* to the root. It's how you find the `package.json` or the `.git` a file
|
|
@@ -691,13 +878,16 @@ function find(name, options = {}) {
|
|
|
691
878
|
* nc.findUp(["pnpm-lock.yaml", "yarn.lock", "package-lock.json"]);
|
|
692
879
|
*
|
|
693
880
|
* @param {string | string[]} name One name, or several to try in each folder, in this order.
|
|
694
|
-
* @param {{ cwd?: string, type?: "file" | "folder" | "any", stopAt?: string }} [options] Where to start (the working directory by default), what to look for (`"any"` by default), and a folder not to go above.
|
|
881
|
+
* @param {{ cwd?: string, type?: "file" | "folder" | "any", stopAt?: string }} [options] Where to start (the working directory by default), what to look for (`"any"` by default), and a folder not to go above: the search ends there, or as soon as it's outside of it.
|
|
695
882
|
* @returns {string | undefined} The absolute path.
|
|
696
883
|
*/
|
|
697
884
|
function findUp(name, options = {}) {
|
|
698
885
|
const names = Array.isArray(name) ? name : [name];
|
|
699
886
|
const type = options.type ?? "any";
|
|
700
|
-
|
|
887
|
+
// resolve() drops a trailing separator; relative() ignores letter case on Windows
|
|
888
|
+
const stopAt = options.stopAt === undefined ? undefined : nodePath.resolve(_abs(options.stopAt));
|
|
889
|
+
// a link, a junction or /var vs /private/var can lead to stopAt another way
|
|
890
|
+
const realStop = stopAt === undefined ? undefined : _realPath(stopAt);
|
|
701
891
|
let dir = _abs(options.cwd);
|
|
702
892
|
for (;;) {
|
|
703
893
|
for (const candidate of names) {
|
|
@@ -705,7 +895,14 @@ function findUp(name, options = {}) {
|
|
|
705
895
|
if (found) return found;
|
|
706
896
|
}
|
|
707
897
|
const parent = nodePath.dirname(dir);
|
|
708
|
-
if (parent === dir
|
|
898
|
+
if (parent === dir) return undefined;
|
|
899
|
+
if (stopAt !== undefined && realStop !== undefined) {
|
|
900
|
+
const rel = nodePath.relative(stopAt, dir);
|
|
901
|
+
const realRel = nodePath.relative(realStop, _realPath(dir));
|
|
902
|
+
// at stopAt by either path, or outside of it by both
|
|
903
|
+
if (rel === "" || realRel === "") return undefined;
|
|
904
|
+
if (_outside(rel) && _outside(realRel)) return undefined;
|
|
905
|
+
}
|
|
709
906
|
dir = parent;
|
|
710
907
|
}
|
|
711
908
|
}
|
|
@@ -1063,15 +1260,17 @@ function deleteFilesIn(path, recursive = false, filter = () => true) {
|
|
|
1063
1260
|
}
|
|
1064
1261
|
|
|
1065
1262
|
/**
|
|
1066
|
-
* Deletes everything inside a folder, keeping the folder.
|
|
1263
|
+
* Deletes everything inside a folder, keeping the folder. A link to a folder
|
|
1264
|
+
* (a junction on Windows) is refused: what it points to is left alone.
|
|
1067
1265
|
*
|
|
1068
1266
|
* @param {string} path
|
|
1069
|
-
* @returns {boolean | undefined} `undefined` if it doesn't exist.
|
|
1267
|
+
* @returns {boolean | undefined} `undefined` if it doesn't exist, `false` if it's a link or something couldn't be deleted.
|
|
1070
1268
|
*/
|
|
1071
1269
|
function emptyFolder(path) {
|
|
1072
1270
|
const target = _existing(path, "folder");
|
|
1073
1271
|
if (!target) return undefined;
|
|
1074
1272
|
try {
|
|
1273
|
+
if (fs.lstatSync(target).isSymbolicLink()) return false;
|
|
1075
1274
|
for (const entry of fs.readdirSync(target)) fs.rmSync(nodePath.join(target, entry), { recursive: true, force: true });
|
|
1076
1275
|
return true;
|
|
1077
1276
|
} catch {
|
|
@@ -1080,7 +1279,9 @@ function emptyFolder(path) {
|
|
|
1080
1279
|
}
|
|
1081
1280
|
|
|
1082
1281
|
/**
|
|
1083
|
-
* Copies a file or a whole folder, like `cp -r`.
|
|
1282
|
+
* Copies a file or a whole folder, like `cp -r`. Links inside the folder
|
|
1283
|
+
* are copied as links (junctions on Windows), never followed: files outside
|
|
1284
|
+
* the folder are never brought along.
|
|
1084
1285
|
*
|
|
1085
1286
|
* @example
|
|
1086
1287
|
* nc.copy("./templates", "./dist/templates");
|
|
@@ -1089,29 +1290,28 @@ function emptyFolder(path) {
|
|
|
1089
1290
|
* @param {string} source
|
|
1090
1291
|
* @param {string} destination
|
|
1091
1292
|
* @param {CopyOptions} [options]
|
|
1092
|
-
* @returns {boolean} `false` if the source is missing or the copy failed.
|
|
1293
|
+
* @returns {boolean} `false` if the source is missing, if the destination is the source or inside it, or if the copy failed.
|
|
1093
1294
|
*/
|
|
1094
1295
|
function copy(source, destination, options = {}) {
|
|
1095
1296
|
const src = _existing(source, "any");
|
|
1096
1297
|
if (!src) return false;
|
|
1097
1298
|
try {
|
|
1098
1299
|
const dest = _abs(destination);
|
|
1300
|
+
if (_within(dest, src)) return false;
|
|
1099
1301
|
fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
|
|
1100
|
-
|
|
1101
|
-
recursive: true,
|
|
1102
|
-
force: options.overwrite ?? true,
|
|
1103
|
-
errorOnExist: false,
|
|
1104
|
-
preserveTimestamps: options.preserveTimestamps ?? false,
|
|
1105
|
-
filter: options.filter ? (s, d) => /** @type {Function} */ (options.filter)(s, d) : undefined,
|
|
1106
|
-
});
|
|
1302
|
+
_copyTree(src, dest, { force: options.overwrite ?? true, preserveTimestamps: options.preserveTimestamps, filter: options.filter });
|
|
1107
1303
|
return true;
|
|
1108
1304
|
} catch {
|
|
1109
1305
|
return false;
|
|
1110
1306
|
}
|
|
1111
1307
|
}
|
|
1112
1308
|
|
|
1309
|
+
// what a rename says when something is in the way of the destination
|
|
1310
|
+
const _IN_THE_WAY = new Set(["EEXIST", "ENOTEMPTY", "EISDIR", "ENOTDIR"]);
|
|
1311
|
+
|
|
1113
1312
|
/**
|
|
1114
|
-
* Moves or renames a file or folder, across drives too.
|
|
1313
|
+
* Moves or renames a file or folder, across drives too. A destination in the
|
|
1314
|
+
* way is put aside first, and put back if the move fails.
|
|
1115
1315
|
*
|
|
1116
1316
|
* @example
|
|
1117
1317
|
* nc.move("./report.pdf", "./archive/2024/report.pdf");
|
|
@@ -1119,31 +1319,56 @@ function copy(source, destination, options = {}) {
|
|
|
1119
1319
|
* @param {string} source
|
|
1120
1320
|
* @param {string} destination
|
|
1121
1321
|
* @param {{ overwrite?: boolean }} [options] `overwrite` replaces an existing destination. Defaults to `true`.
|
|
1122
|
-
* @returns {boolean}
|
|
1322
|
+
* @returns {boolean} `false` if the source is missing, if the destination is inside it, if the source is a link to the destination, or if the move failed.
|
|
1123
1323
|
*/
|
|
1124
1324
|
function move(source, destination, options = {}) {
|
|
1125
1325
|
const src = _existing(source, "any");
|
|
1126
1326
|
if (!src) return false;
|
|
1127
1327
|
const dest = _abs(destination);
|
|
1128
1328
|
try {
|
|
1129
|
-
|
|
1329
|
+
// the link would take the place of the file it leads to
|
|
1330
|
+
if (_linkOnto(src, dest)) return false;
|
|
1331
|
+
if (_sameEntry(src, dest)) {
|
|
1332
|
+
// the same file: only its name can change, like its letter case on Windows
|
|
1333
|
+
fs.renameSync(src, dest);
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
if (_within(dest, src)) return false;
|
|
1337
|
+
if (options.overwrite === false && _lexists(dest)) return false;
|
|
1130
1338
|
fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
|
|
1339
|
+
let crossDevice = false;
|
|
1131
1340
|
try {
|
|
1132
1341
|
// A rename replaces a file in one step: the destination is never missing, not even for an instant.
|
|
1133
1342
|
fs.renameSync(src, dest);
|
|
1343
|
+
return true;
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
const code = /** @type {NodeJS.ErrnoException} */ (error).code ?? "";
|
|
1346
|
+
crossDevice = code === "EXDEV";
|
|
1347
|
+
// Windows says EPERM or EACCES for a folder in the way
|
|
1348
|
+
const inTheWay = _IN_THE_WAY.has(code) || (process.platform === "win32" && (code === "EPERM" || code === "EACCES"));
|
|
1349
|
+
if (!crossDevice && !(inTheWay && _lexists(dest))) throw error;
|
|
1350
|
+
}
|
|
1351
|
+
// Put what's in the way aside, and back if the move fails.
|
|
1352
|
+
const backup = _lexists(dest) ? `${dest}.${require("node:crypto").randomBytes(6).toString("hex")}.bak` : undefined;
|
|
1353
|
+
if (backup) _rename(dest, backup);
|
|
1354
|
+
try {
|
|
1355
|
+
// another drive: copy, and delete the source once the copy is complete
|
|
1356
|
+
if (crossDevice) _copyTree(src, dest);
|
|
1357
|
+
else _rename(src, dest);
|
|
1134
1358
|
} catch (error) {
|
|
1135
|
-
|
|
1136
|
-
if (
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1359
|
+
// a half-made copy is ours to remove, nothing else is
|
|
1360
|
+
if (crossDevice) fs.rmSync(dest, { recursive: true, force: true });
|
|
1361
|
+
if (backup) _rename(backup, dest);
|
|
1362
|
+
throw error;
|
|
1363
|
+
}
|
|
1364
|
+
if (backup) {
|
|
1365
|
+
try {
|
|
1366
|
+
fs.rmSync(backup, { recursive: true, force: true });
|
|
1367
|
+
} catch {
|
|
1368
|
+
// the move is done: a leftover of the old destination is better than an error
|
|
1145
1369
|
}
|
|
1146
1370
|
}
|
|
1371
|
+
if (crossDevice) fs.rmSync(src, { recursive: true, force: true });
|
|
1147
1372
|
return true;
|
|
1148
1373
|
} catch {
|
|
1149
1374
|
return false;
|
|
@@ -1155,12 +1380,13 @@ function move(source, destination, options = {}) {
|
|
|
1155
1380
|
* for most cases.
|
|
1156
1381
|
*
|
|
1157
1382
|
* @param {CopyFoldersOptions} options
|
|
1158
|
-
* @returns {number | undefined} How many were copied.
|
|
1383
|
+
* @returns {number | undefined} How many were copied: `0` when `dest` is the source folder or inside it.
|
|
1159
1384
|
*/
|
|
1160
1385
|
function copyFoldersIn(options) {
|
|
1161
1386
|
const root = _existing(options.path, "folder");
|
|
1162
1387
|
if (!root || !options.dest) return undefined;
|
|
1163
1388
|
const dest = _abs(options.dest);
|
|
1389
|
+
if (_within(dest, root)) return 0;
|
|
1164
1390
|
const recursive = options.recursive ?? true;
|
|
1165
1391
|
const filter = typeof options.filter === "function" ? options.filter : () => true;
|
|
1166
1392
|
let copied = 0;
|
|
@@ -1182,12 +1408,13 @@ function copyFoldersIn(options) {
|
|
|
1182
1408
|
* folder is created; `copy()` copies everything in one call.
|
|
1183
1409
|
*
|
|
1184
1410
|
* @param {CopyFolderOptions} options
|
|
1185
|
-
* @returns {boolean | undefined}
|
|
1411
|
+
* @returns {boolean | undefined} `false` when `dest` is the source folder or inside it.
|
|
1186
1412
|
*/
|
|
1187
1413
|
function copyFolder(options) {
|
|
1188
1414
|
const root = _existing(options.path, "folder");
|
|
1189
1415
|
if (!root || !options.dest) return undefined;
|
|
1190
1416
|
const dest = _abs(options.dest);
|
|
1417
|
+
if (_within(dest, root)) return false;
|
|
1191
1418
|
try {
|
|
1192
1419
|
if (options.force && fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true });
|
|
1193
1420
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -1212,13 +1439,14 @@ function copyFolder(options) {
|
|
|
1212
1439
|
* nc.copyFilesIn({ path: "./src", dest: "./dist", filter: (file) => file.endsWith(".css") });
|
|
1213
1440
|
*
|
|
1214
1441
|
* @param {CopyFilesOptions} options
|
|
1215
|
-
* @returns {number | undefined} How many were copied
|
|
1442
|
+
* @returns {number | undefined} How many were copied: `0` when `dest` is the source folder, or inside it with `recursive`.
|
|
1216
1443
|
*/
|
|
1217
1444
|
function copyFilesIn(options) {
|
|
1218
1445
|
const root = _existing(options.path, "folder");
|
|
1219
1446
|
if (!root || !options.dest) return undefined;
|
|
1220
1447
|
const dest = _abs(options.dest);
|
|
1221
1448
|
const recursive = options.recursive ?? true;
|
|
1449
|
+
if (_filesInItself(root, dest, recursive)) return 0;
|
|
1222
1450
|
const filter = typeof options.filter === "function" ? options.filter : () => true;
|
|
1223
1451
|
let copied = 0;
|
|
1224
1452
|
for (const file of getFilesIn(root, recursive) ?? []) {
|
|
@@ -1232,6 +1460,26 @@ function copyFilesIn(options) {
|
|
|
1232
1460
|
return copied;
|
|
1233
1461
|
}
|
|
1234
1462
|
|
|
1463
|
+
/**
|
|
1464
|
+
* Where a file goes: into `dest` under its own name if `dest` ends with `/`
|
|
1465
|
+
* or is a folder, else `dest` itself.
|
|
1466
|
+
* @param {string} src
|
|
1467
|
+
* @param {string} dest
|
|
1468
|
+
* @returns {string}
|
|
1469
|
+
*/
|
|
1470
|
+
const _fileDest = (src, dest) => {
|
|
1471
|
+
const abs = _abs(dest);
|
|
1472
|
+
let intoFolder = /[\\/]$/.test(dest);
|
|
1473
|
+
if (!intoFolder) {
|
|
1474
|
+
try {
|
|
1475
|
+
intoFolder = fs.statSync(abs).isDirectory();
|
|
1476
|
+
} catch {
|
|
1477
|
+
// nothing there yet
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return intoFolder ? nodePath.join(abs, nodePath.basename(src)) : abs;
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1235
1483
|
/**
|
|
1236
1484
|
* Copies one file. If `dest` ends with `/`, the file keeps its name inside
|
|
1237
1485
|
* that folder.
|
|
@@ -1240,13 +1488,14 @@ function copyFilesIn(options) {
|
|
|
1240
1488
|
* nc.copyFile({ path: "./src/index.js", dest: "./dist/" });
|
|
1241
1489
|
*
|
|
1242
1490
|
* @param {CopyFileOptions} options
|
|
1243
|
-
* @returns {boolean | undefined}
|
|
1491
|
+
* @returns {boolean | undefined} `false` when `dest` is the file itself.
|
|
1244
1492
|
*/
|
|
1245
1493
|
function copyFile(options) {
|
|
1246
1494
|
const src = _existing(options.path, "file");
|
|
1247
1495
|
if (!src || !options.dest) return undefined;
|
|
1248
|
-
const
|
|
1249
|
-
|
|
1496
|
+
const dest = _fileDest(src, options.dest);
|
|
1497
|
+
// with force, the copy would delete its own source
|
|
1498
|
+
if (_sameEntry(src, dest)) return false;
|
|
1250
1499
|
try {
|
|
1251
1500
|
if (options.force && fs.existsSync(dest)) fs.rmSync(dest, { force: true });
|
|
1252
1501
|
fs.mkdirSync(nodePath.dirname(dest), { recursive: true });
|
|
@@ -1261,10 +1510,12 @@ function copyFile(options) {
|
|
|
1261
1510
|
* Moves the folders inside a folder.
|
|
1262
1511
|
*
|
|
1263
1512
|
* @param {CopyFoldersOptions} options
|
|
1264
|
-
* @returns {number | undefined} How many were moved.
|
|
1513
|
+
* @returns {number | undefined} How many were moved: `0` when `dest` is the source folder or inside it.
|
|
1265
1514
|
*/
|
|
1266
1515
|
function moveFoldersIn(options) {
|
|
1267
1516
|
const root = _existing(options.path, "folder");
|
|
1517
|
+
// the folders are deleted after the copy: they must not hold it
|
|
1518
|
+
if (root && options.dest && _within(_abs(options.dest), root)) return 0;
|
|
1268
1519
|
const copied = copyFoldersIn(options);
|
|
1269
1520
|
if (copied === undefined || !root) return copied;
|
|
1270
1521
|
const filter = typeof options.filter === "function" ? options.filter : () => true;
|
|
@@ -1278,7 +1529,7 @@ function moveFoldersIn(options) {
|
|
|
1278
1529
|
* Moves a folder.
|
|
1279
1530
|
*
|
|
1280
1531
|
* @param {CopyFolderOptions & { path: string }} options
|
|
1281
|
-
* @returns {boolean | undefined}
|
|
1532
|
+
* @returns {boolean | undefined} `false` when `dest` is the source folder or inside it.
|
|
1282
1533
|
*/
|
|
1283
1534
|
function moveFolder(options) {
|
|
1284
1535
|
const root = _existing(options.path, "folder");
|
|
@@ -1288,19 +1539,40 @@ function moveFolder(options) {
|
|
|
1288
1539
|
}
|
|
1289
1540
|
|
|
1290
1541
|
/**
|
|
1291
|
-
* Moves the files inside a folder.
|
|
1542
|
+
* Moves the files inside a folder. A file that can't be copied stays where
|
|
1543
|
+
* it is, and isn't counted.
|
|
1292
1544
|
*
|
|
1293
1545
|
* @param {CopyFilesOptions} options
|
|
1294
|
-
* @returns {number | undefined} How many were moved
|
|
1546
|
+
* @returns {number | undefined} How many were moved: `0` when `dest` is the source folder, or inside it with `recursive`.
|
|
1295
1547
|
*/
|
|
1296
1548
|
function moveFilesIn(options) {
|
|
1297
1549
|
const root = _existing(options.path, "folder");
|
|
1298
1550
|
if (!root || !options.dest) return undefined;
|
|
1551
|
+
const dest = _abs(options.dest);
|
|
1552
|
+
const recursive = options.recursive ?? true;
|
|
1553
|
+
// the files are deleted after the copy: they must not be the copy
|
|
1554
|
+
if (_filesInItself(root, dest, recursive)) return 0;
|
|
1299
1555
|
const filter = typeof options.filter === "function" ? options.filter : () => true;
|
|
1300
|
-
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
|
|
1556
|
+
let moved = 0;
|
|
1557
|
+
for (const file of getFilesIn(root, recursive) ?? []) {
|
|
1558
|
+
if (!filter(file)) continue;
|
|
1559
|
+
const target = nodePath.join(dest, recursive ? nodePath.relative(root, file) : nodePath.basename(file));
|
|
1560
|
+
try {
|
|
1561
|
+
if (options.force && fs.existsSync(target)) fs.rmSync(target, { force: true });
|
|
1562
|
+
fs.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
1563
|
+
fs.copyFileSync(file, target);
|
|
1564
|
+
} catch {
|
|
1565
|
+
// not copied: the source stays where it is
|
|
1566
|
+
continue;
|
|
1567
|
+
}
|
|
1568
|
+
try {
|
|
1569
|
+
fs.rmSync(file, { force: true });
|
|
1570
|
+
moved++;
|
|
1571
|
+
} catch {
|
|
1572
|
+
// copied but still there: not moved
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return moved;
|
|
1304
1576
|
}
|
|
1305
1577
|
|
|
1306
1578
|
/**
|
|
@@ -1310,12 +1582,26 @@ function moveFilesIn(options) {
|
|
|
1310
1582
|
* nc.moveFile({ path: "./logs/app.log", dest: "./logs/archive/" });
|
|
1311
1583
|
*
|
|
1312
1584
|
* @param {CopyFileOptions} options
|
|
1313
|
-
* @returns {boolean | undefined}
|
|
1585
|
+
* @returns {boolean | undefined} `false` when `dest` is the file itself under the same name, or when the file is a link to `dest`.
|
|
1314
1586
|
*/
|
|
1315
1587
|
function moveFile(options) {
|
|
1316
1588
|
const src = _existing(options.path, "file");
|
|
1589
|
+
if (!src || !options.dest) return undefined;
|
|
1590
|
+
const dest = _fileDest(src, options.dest);
|
|
1591
|
+
// the link would take the place of the file it leads to
|
|
1592
|
+
if (_linkOnto(src, dest)) return false;
|
|
1593
|
+
if (_sameEntry(src, dest)) {
|
|
1594
|
+
// the same file: only its name can change, like its letter case on Windows
|
|
1595
|
+
if (nodePath.basename(src) === nodePath.basename(dest)) return false;
|
|
1596
|
+
try {
|
|
1597
|
+
fs.renameSync(src, dest);
|
|
1598
|
+
return true;
|
|
1599
|
+
} catch {
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1317
1603
|
const moved = copyFile(options);
|
|
1318
|
-
if (moved
|
|
1604
|
+
if (moved) fs.rmSync(src, { force: true });
|
|
1319
1605
|
return moved;
|
|
1320
1606
|
}
|
|
1321
1607
|
|
|
@@ -1448,10 +1734,24 @@ function tempFolder(prefix = "nc-") {
|
|
|
1448
1734
|
return fs.mkdtempSync(nodePath.join(os.tmpdir(), prefix));
|
|
1449
1735
|
}
|
|
1450
1736
|
|
|
1737
|
+
/**
|
|
1738
|
+
* Removes the dots and spaces at the end, in one pass: a regex like
|
|
1739
|
+
* `/[. ]+$/` would start again from every dot or space of a long name.
|
|
1740
|
+
* @param {string} text
|
|
1741
|
+
* @param {string} chars
|
|
1742
|
+
* @returns {string}
|
|
1743
|
+
*/
|
|
1744
|
+
const _trimEnd = (text, chars) => {
|
|
1745
|
+
let end = text.length;
|
|
1746
|
+
while (end > 0 && chars.includes(text[end - 1])) end--;
|
|
1747
|
+
return text.slice(0, end);
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1451
1750
|
/**
|
|
1452
1751
|
* Turns any text into a file name that works on Windows, macOS and Linux:
|
|
1453
|
-
* forbidden characters are replaced,
|
|
1454
|
-
*
|
|
1752
|
+
* forbidden characters are replaced, invisible and text direction
|
|
1753
|
+
* characters are removed, reserved names like `CON`, `NUL .txt` or
|
|
1754
|
+
* `COM¹` are prefixed, and the length is capped.
|
|
1455
1755
|
*
|
|
1456
1756
|
* @example
|
|
1457
1757
|
* nc.sanitizeFilename('Report: Q1/Q2 "final"?.pdf'); // "Report_ Q1_Q2 _final__.pdf"
|
|
@@ -1463,8 +1763,15 @@ function tempFolder(prefix = "nc-") {
|
|
|
1463
1763
|
*/
|
|
1464
1764
|
function sanitizeFilename(name, options = {}) {
|
|
1465
1765
|
const replacement = options.replacement ?? "_";
|
|
1466
|
-
let clean =
|
|
1467
|
-
|
|
1766
|
+
let clean = _trimEnd(
|
|
1767
|
+
String(name)
|
|
1768
|
+
// invisible characters, and the ones that reverse the text so that "exe.txt" reads "txt.exe"
|
|
1769
|
+
.replace(/[---]/g, "")
|
|
1770
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f\u007f]/g, replacement),
|
|
1771
|
+
". ",
|
|
1772
|
+
);
|
|
1773
|
+
// Windows ignores the extension and the spaces before it: "NUL .txt" is still the NUL device
|
|
1774
|
+
if (/^(con|prn|aux|nul|conin\$|conout\$|com[0-9¹²³]|lpt[0-9¹²³])$/i.test(_trimEnd(clean.split(".")[0], " "))) clean = `_${clean}`;
|
|
1468
1775
|
if (clean === "." || clean === "..") clean = replacement;
|
|
1469
1776
|
if (Buffer.byteLength(clean) > 255) {
|
|
1470
1777
|
// File systems count bytes, and a character cut in two would be broken.
|
|
@@ -1475,7 +1782,7 @@ function sanitizeFilename(name, options = {}) {
|
|
|
1475
1782
|
if (bytes > 255) break;
|
|
1476
1783
|
kept += char;
|
|
1477
1784
|
}
|
|
1478
|
-
clean = kept.
|
|
1785
|
+
clean = _trimEnd(kept, ". ");
|
|
1479
1786
|
}
|
|
1480
1787
|
return clean || "_";
|
|
1481
1788
|
}
|
|
@@ -1572,6 +1879,9 @@ function watch(options = {}) {
|
|
|
1572
1879
|
paused = false;
|
|
1573
1880
|
return controller;
|
|
1574
1881
|
},
|
|
1882
|
+
[Symbol.dispose]() {
|
|
1883
|
+
controller.stop();
|
|
1884
|
+
},
|
|
1575
1885
|
};
|
|
1576
1886
|
return controller;
|
|
1577
1887
|
}
|