UNPKG

kaven-utils

Version:

Utils for Node.js.

699 lines (698 loc) 19.8 kB
/******************************************************************** * @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); } } } }