kaven-utils
Version:
Utils for Node.js.
699 lines (698 loc) • 19.8 kB
JavaScript
/********************************************************************
* @author: Kaven
* @email: kaven@wuwenkai.com
* @website: http://blog.kaven.xyz
* @file: [Kaven-Utils] /src/KavenUtility.FileSystem.ts
* @create: 2021-08-06 23:39:38.618
* @modify: 2025-10-24 16:32:50.692
* @version: 6.1.2
* @times: 113
* @lines: 780
* @copyright: Copyright © 2021-2025 Kaven. All Rights Reserved.
* @description: [description]
* @license: [license]
********************************************************************/
import { stripComments } from "jsonc-parser";
import { CombinePath, GetBaseDir, IsEqual, NormalizePathSep, ParseLines, ToFileSize, TrimPath, TrimStart } from "kaven-basic";
import { copyFileSync, existsSync, mkdirSync, promises, readFileSync, writeFileSync } from "node:fs";
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
/**
* Simple wrap for existsSync
* @see https://nodejs.org/docs/latest-v8.x/api/html#fs_fs_existssync_path
* @since 3.0.0
* @since 2021-08-06
*/
export function IsPathExistSync(...paths) {
return paths.every(p => existsSync(p));
}
/**
* @since 3.0.0
* @version 2021-08-06
*/
export async function GetFileStats(path) {
if (!IsPathExistSync(path)) {
return undefined;
}
return await promises.stat(path);
}
/**
* @since 2.0.2
* @version 2021-08-06
*/
export async function IsFile(path) {
const stats = await GetFileStats(path);
return stats?.isFile();
}
/**
* @since 1.0.5
* @version 2021-08-06
*/
export async function IsDirectory(path) {
const stats = await GetFileStats(path);
return stats?.isDirectory();
}
/**
* @since 1.0.9
* @version 2021-08-06
*/
export async function GetFileSizeInBytes(path) {
const stats = await GetFileStats(path);
return stats?.size;
}
/**
* @since 3.0.0
* @version 2021-08-07
*/
export async function GetFileSize(path, baseOn1024) {
const size = await GetFileSizeInBytes(path);
if (size === undefined) {
return undefined;
}
return ToFileSize(size, baseOn1024);
}
/***
* @since 3.0.0
* @version 2023-12-06
*/
export async function DeleteFile(file) {
await promises.unlink(file);
}
/**
* @since 5.4.0
* @version 2023-12-06
*/
export async function DeleteFileIfExists(file) {
if (IsPathExistSync(file)) {
await DeleteFile(file);
return true;
}
return false;
}
/**
* @since 5.4.0
* @version 2025-10-14
*/
export async function TryDeleteFile(file, logger) {
try {
await DeleteFile(file);
return true;
}
catch (ex) {
logger?.Error(ex);
return false;
}
}
/**
* @since 4.0.0
* @version 2023-12-06
*/
export async function TryDeleteFiles(...files) {
await Promise.all(files.map(p => TryDeleteFile(p)));
}
/**
* @since 3.0.0
* @version 2023-12-06
*/
export async function DeleteDirectory(path) {
await promises.rm(path, {
recursive: true,
force: true,
});
}
/**
* @since 5.4.0
* @version 2025-10-14
*/
export async function TryDeleteDirectory(path, logger) {
try {
await promises.rm(path, {
recursive: true,
force: true,
});
}
catch (ex) {
logger?.Error(ex);
}
}
/**
* @since 1.0.9
* @version 2025-10-24
* @return The first directory path created.
*/
export async function MakeDirectory(path) {
return await promises.mkdir(path, {
recursive: true,
});
}
/**
* @since 5.4.0
* @version 2025-10-14
*/
export async function TryMakeDirectory(path, logger) {
try {
return await MakeDirectory(path);
}
catch (ex) {
logger?.Error(ex);
return path;
}
}
/**
* @since 1.0.9
* @version 2025-10-24
*/
export function MakeDirectorySync(path) {
return mkdirSync(path, {
recursive: true,
});
}
/**
* @since 5.4.0
* @version 2025-10-24
*/
export function TryMakeDirectorySync(path, logger) {
try {
return MakeDirectorySync(path);
}
catch (ex) {
logger?.Error(ex);
return path;
}
}
/**
* @since 5.4.0
* @version 2023-12-06
*/
export async function Delete(...paths) {
const del = async (path) => {
const stats = await GetFileStats(path);
if (stats === undefined) {
/* istanbul ignore next */
return;
}
if (stats.isDirectory()) {
await DeleteDirectory(path);
}
else {
await DeleteFile(path);
}
};
await Promise.all(paths.map(p => del(p)));
}
/**
* @since 5.4.0
* @version 2023-12-06
*/
export async function TryDelete(...paths) {
const del = async (path) => {
const stats = await GetFileStats(path);
if (stats === undefined) {
return;
}
if (stats.isDirectory()) {
await TryDeleteDirectory(path);
}
else {
await TryDeleteFile(path);
}
};
await Promise.all(paths.map(p => del(p)));
}
/**
* Copies a file from the source path to the destination path asynchronously.
*
* @example "/path/to/source/file.txt" -> "/path/to/destination/file.txt"
* @param src - The source path of the file to be copied.
* @param dest - The destination path where the file will be copied.
* @param checkSrcExists - If true, checks whether the source file exists before copying.
* @returns A Promise that resolves to true if the file is copied successfully, or false if the source file doesn't exist.
* @since 3.0.0
* @version 2025-10-24
*/
export async function CopyFile(src, dest, options) {
const logger = options?.logger;
try {
const firstDirPathCreated = await MakeDirectory(dirname(dest));
if (firstDirPathCreated) {
logger?.Info(`Created directory: ${firstDirPathCreated}`);
}
if (options?.overwrite === false) {
try {
await promises.copyFile(src, dest, promises.constants.COPYFILE_EXCL);
}
catch (err) {
if (err?.code === "EEXIST") {
// File already exists, do not overwrite
logger?.Warn(`File already exists and will not be overwritten: ${dest}`);
return false;
}
else {
throw err;
}
}
}
else {
await promises.copyFile(src, dest);
}
logger?.Info(`Copied: ${src} -> ${dest}`);
return true;
}
catch (err) {
logger?.Error(`Error copying file from ${src} to ${dest}: ${err}`);
if (options?.throwOnError) {
throw err;
}
return false;
}
}
/**
*
* @param path
* @param len
* @since 3.0.0
* @version 2021-08-07
*/
export async function TruncateFile(path, len) {
await promises.truncate(path, len);
}
/**
*
* @param dest
* @param src
* @since 1.0.5
* @version 2021-08-06
*/
export function CopyFileSync(src, dest) {
MakeDirectorySync(dirname(dest));
copyFileSync(src, dest);
}
/**
* Recursively copies the contents of a directory from the source path to the destination path asynchronously.
*
* @example "/path/to/source/directory" -> "/path/to/destination/directory"
* @param src - The source path of the directory to be copied.
* @param dest - The destination path where the directory and its contents will be copied.
* @returns A Promise that resolves when the directory and its contents are copied successfully.
* @since 3.0.0
* @version 2025-10-24
*/
export async function CopyDirectory(src, dest, options) {
// Normalize source/destination paths
src = TrimPath(src);
dest = TrimPath(dest);
// Ensure source exists and is a directory
const stat = await promises.stat(src);
if (!stat.isDirectory()) {
throw new Error(`Source "${src}" is not a directory`);
}
// Stack-based traversal
const stack = [src];
while (stack.length > 0) {
const current = stack.pop();
if (!current)
continue;
const stat = await promises.stat(current);
if (stat.isDirectory()) {
const entries = await promises.readdir(current);
for (const entry of entries) {
stack.push(join(current, entry));
}
}
else {
// Compute the relative path correctly
const relPath = relative(src, current);
const targetPath = join(dest, relPath);
await CopyFile(current, targetPath, options);
}
}
}
/**
* Copies a file or a directory from the source path to the destination path asynchronously.
* If the source is a directory, its entire contents will be recursively copied.
*
* @example
* "/path/to/source/directory" -> "/path/to/destination/directory"
* "/path/to/source/file.txt" -> "/path/to/destination/file.txt"
* @param src - The source path of the file or directory to be copied.
* @param dest - The destination path where the file or directory will be copied.
* @returns A Promise that resolves to true if the file or directory is copied successfully, or false if it doesn't exist.
* @since 3.0.0
* @version 2025-10-24
*/
export async function CopyFileOrDirectory(src, dest, options) {
// Retrieve the file stats of the source path
const stats = await GetFileStats(src);
// If stats is undefined, the source file or directory doesn't exist
if (stats === undefined) {
return false;
}
// Check if the source is a directory
if (stats.isDirectory()) {
// If it's a directory, recursively copy its entire contents
await CopyDirectory(src, dest, options);
return true;
}
else {
// If it's a file, copy the file
return await CopyFile(src, dest, options);
}
}
/**
* Copies an array of files or directories to a destination directory asynchronously.
*
* @example
* 1. "/path/to/source/file.txt" -> "/path/to/destination"
* 2. "/path/to/source/directory" -> "/path/to/destination"
* 3. ["/path/to/source/file.txt", "/path/to/source/directory"] -> "/path/to/destination"
* @param files - An array of file or directory paths to be copied.
* @param destDir - The destination directory where the files or directories will be copied.
* @since 3.0.0
* @version 2025-10-24
*/
export async function CopyToDirectory(files, destDir, options) {
const cp = async (file) => {
// Retrieve the file stats of the current file or directory
const stats = await GetFileStats(file);
// If stats is undefined, the current file or directory doesn't exist
if (stats === undefined) {
/* istanbul ignore next */
return;
}
// Calculate the destination path by joining the destination directory and the base name of the current file
const dest = join(destDir, basename(file));
// Check if the current item is a directory
if (stats.isDirectory()) {
// If it's a directory, recursively copy its entire contents to the destination
await CopyDirectory(file, dest, options);
}
else {
// If it's a file, copy the file to the destination
await CopyFile(file, dest, options);
}
};
if (typeof files === "string") {
await cp(files);
}
else {
await Promise.all(files.map(p => cp(p)));
}
}
/**
* @since 1.0.5
* @version 2021-12-07
*/
export function GetFileExtension(fileName, trimDot = false, toLowerCase = false) {
let ext = extname(fileName);
if (trimDot) {
ext = TrimStart(ext, ".", 1);
}
if (toLowerCase) {
ext = ext.toLowerCase();
}
return ext;
}
/**
*
* @param fileName
* @version 2018-11-06
* @since 1.0.5
*/
export function GetFileName(fileName) {
return basename(fileName);
}
/**
* @since 4.1.0
* @version 2023-12-05
*/
export function AppendPathToDirectory(dir, path) {
let result = path;
if (dir && !isAbsolute(path)) {
result = resolve(CombinePath(dir, path));
}
return result;
}
/**
* @since 5.4.0
* @version 2025-10-24
*/
export function AppendPathThenCreateDirectory(dir, path) {
const result = AppendPathToDirectory(dir, path);
MakeDirectorySync(result);
return result;
}
/**
* @since 5.4.0
* @version 2025-10-24
*/
export function AppendPathThenCreateParentDirectory(dir, path) {
const result = AppendPathToDirectory(dir, path);
MakeDirectorySync(dirname(result));
return result;
}
/**
*
* @param path
* @param dir
* @since 4.3.1
* @version 2022-06-29
*/
export function PathIsSubOfDirectory(path, dir) {
const r = relative(dir, path);
if (r && !r.startsWith("..") && !isAbsolute(r)) {
return true;
}
return false;
}
/**
* @since 4.3.1
* @version 2022-06-29
*/
export function PathNeedExclude(path, excludePaths) {
if (excludePaths.includes(path)) {
return true;
}
const sep = "/";
path = NormalizePathSep(path, sep);
excludePaths = excludePaths.map(p => NormalizePathSep(p, sep));
if (excludePaths.includes(path)) {
return true;
}
for (const item of excludePaths) {
if (PathIsSubOfDirectory(path, item)) {
return true;
}
}
return false;
}
/**
* Asynchronously finds a subdirectory with a specific name in the given directory.
* Returns the name of the subdirectory if found, or undefined otherwise.
*
* @param dir - The path of the directory to search.
* @param name - The name of the subdirectory to find.
* @param [ignoreCase=false] - If true, performs a case-insensitive comparison.
* @returns A Promise that resolves to the name of the found subdirectory or undefined if not found.
* @since 5.0.5
* @version 2023-11-25
*/
export async function FindSubdirectoryByName(dir, name, ignoreCase = false) {
// Get the list of items in the directory asynchronously
const items = await promises.readdir(dir);
// Iterate through the items in the directory
for (const item of items) {
// Check if the item is a directory
if (await IsDirectory(join(dir, item))) {
// Check if the directory name matches the target name (with optional case-insensitive comparison)
if (IsEqual(item, name, ignoreCase)) {
// If a match is found, return the name of the subdirectory
return item;
}
}
}
// Return undefined if no matching subdirectory is found
return undefined;
}
/**
* @param {string} data
* @param {string} path
* @since 1.0.6
* @version 2024-11-01
*/
export async function SaveStringToFile(data, path) {
// 2018-10-20, create directory if not exist
// 2024-11-01, try make directory
await TryMakeDirectory(GetBaseDir(path));
await writeFile(path, data);
return path;
}
/**
*
* @param path
* @param options
* @since 1.0.5
* @version 2025-07-12
*/
export async function GetFileContent(path, options = "utf8") {
const data = await readFile(path, options);
return data;
}
/**
* @since 5.0.1
* @version 2025-06-21
*/
export function GetFileContentSync(path, options = "utf8") {
const data = readFileSync(path, options);
return data;
}
/**
* @param {string} path
* @since 1.0.12
* @version 2023-12-11
*/
export async function LoadJsonFile(path) {
const raw = await GetFileContent(path);
const data = stripComments(raw);
return JSON.parse(data);
}
/**
* @param {string} path
* @since 5.0.1
* @version 2023-12-11
*/
export function LoadJsonFileSync(path) {
const raw = GetFileContentSync(path);
const data = stripComments(raw);
return JSON.parse(data);
}
/**
* @since 3.0.1
* @version 2025-06-18
*/
export async function GetFileLines(fileName) {
const content = await GetFileContent(fileName);
return ParseLines(content);
}
/**
* @param data
* @param path
* @since 5.0.1
* @version 2023-11-18
*/
export function SaveStringToFileSync(data, path) {
// 2018-10-20, create directory if not exist
MakeDirectorySync(GetBaseDir(path));
writeFileSync(path, data);
return path;
}
/**
* @since 5.4.3
* @version 2025-10-14
*/
export async function DeleteFilesByExtension(src, extensions, options) {
options ??= {};
options.ignoreFolderNames ??= [
".git",
"node_modules",
];
const list = typeof src === "string" ? [src] : src;
if (extensions.length === 0) {
return;
}
while (list.length > 0) {
const file = list.shift();
if (!file) {
continue;
}
let stats;
try {
stats = await promises.stat(file);
}
catch (err) {
options.logger?.Warn(`Error getting stats for file ${file}: ${err}`);
continue;
}
if (stats.isFile()) {
const str = options.caseSensitive ? file : file.toLowerCase();
if (extensions.some(p => str.endsWith(p))) {
options.logger?.Info(`Delete: ${file}`);
await promises.unlink(file);
continue;
}
}
else if (stats.isDirectory()) {
const dir = basename(file);
const str = options.caseSensitive ? dir : dir.toLowerCase();
if (options.ignoreFolderNames.includes(str)) {
options.logger?.Info(`Ignore: ${file}`);
continue;
}
const files = await promises.readdir(file);
list.push(...files.map(p => join(file, p)));
}
}
}
/**
* Asynchronously enumerates files starting from one or more input paths.
*
* This async generator yields file paths (as strings) discovered while traversing
* the provided path(s). Directory traversal is breadth-first (queue-based).
*
* @example
* // Recursively enumerate all files under "src" and "test"
* for await (const file of EnumerateFiles(["src", "test"])) {
* console.log(file);
* }
*
* @since 6.1.1
* @version 2025-10-23
*/
export async function* EnumerateFiles(input, options) {
const list = typeof input === "string" ? [input] : input;
const logger = options?.logger;
const ignoreDirectoryNames = options?.ignoreDirectoryNames ?? [];
const getStats = async (path) => {
try {
return await stat(path);
}
catch (err) {
logger?.Warn(`Error getting stats for file ${path}: ${err}`);
return undefined;
}
};
while (list.length > 0) {
const file = list.shift();
if (!file) {
continue;
}
const stats = await getStats(file);
if (!stats) {
continue;
}
if (stats.isFile()) {
yield file;
}
else if (stats.isDirectory()) {
if (ignoreDirectoryNames.length > 0) {
const dir = basename(file);
if (ignoreDirectoryNames.includes(dir)) {
logger?.Warn(`Ignore: ${file}`);
continue;
}
}
const files = await readdir(file);
const fullPaths = files.map(p => join(file, p));
if (options?.topDirectoryOnly) {
for (const f of fullPaths) {
const fStats = await getStats(f);
if (fStats?.isFile()) {
yield f;
}
}
}
else {
list.push(...fullPaths);
}
}
}
}