UNPKG

kaven-utils

Version:

Utils for Node.js.

706 lines (705 loc) 19.7 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: 2024-11-01 16:34:42.274 * @version: 5.4.5 * @times: 88 * @lines: 800 * @copyright: Copyright © 2021-2024 Kaven. All Rights Reserved. * @description: [description] * @license: [license] ********************************************************************/ import { AnsiUp } from "ansi_up"; import { stripComments } from "jsonc-parser"; import { CombinePath, FileSize, GetBaseDir, IsEqual, NormalizePathSep, SplitStringByNewline, Strings_CR_LF, Strings_Empty, Strings_HTML_BR, Strings_HTML_NBSP, Strings_LF, TrimStart } from "kaven-basic"; import { copyFileSync, existsSync, mkdirSync, promises, readFileSync, writeFileSync } from "node:fs"; import { readFile, writeFile } from "node:fs/promises"; import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path"; import { KavenLogger } from "./KavenLogger.js"; import { InternalLogger } from "./KavenUtility.Internal.js"; /** * 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 FileSize(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 2023-12-06 */ export async function TryDeleteFile(file) { try { await DeleteFile(file); return true; } catch (ex) { InternalLogger()?.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 2023-12-06 */ export async function TryDeleteDirectory(path) { try { await promises.rm(path, { recursive: true, force: true, }); } catch (ex) { /* istanbul ignore next */ InternalLogger()?.Error(ex); } } /** * @since 1.0.9 * @version 2023-12-06 */ export async function MakeDirectory(path) { await promises.mkdir(path, { recursive: true, }); return path; } /** * @since 5.4.0 * @version 2023-12-06 */ export async function MakeDirectoryIfNotExists(path) { if (IsPathExistSync(path)) { return path; } return await MakeDirectory(path); } /** * @since 5.4.0 * @version 2023-12-06 */ export async function TryMakeDirectory(path) { try { return await MakeDirectory(path); } catch (ex) { InternalLogger()?.Error(ex); return path; } } /** * @since 1.0.9 * @version 2023-12-06 */ export function MakeDirectorySync(path) { mkdirSync(path, { recursive: true, }); return path; } /** * @since 5.4.0 * @version 2023-12-06 */ export function MakeDirectorySyncIfNotExists(path) { if (IsPathExistSync(path)) { return path; } return MakeDirectorySync(path); } /** * @since 5.4.0 * @version 2023-12-06 */ export function TryMakeDirectorySync(path) { try { return MakeDirectorySync(path); } catch (ex) { InternalLogger()?.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 2021-09-13 */ export async function CopyFile(src, dest, checkSrcExists = false) { // Check if source file existence should be verified if (checkSrcExists) { // If source file doesn't exist, return false if (!IsPathExistSync(src)) { return false; } } // Ensure the destination directory exists await MakeDirectoryIfNotExists(dirname(dest)); // Copy the file from source to destination asynchronously await promises.copyFile(src, dest); // Return true to indicate successful file copy return true; } /** * * @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 2021-08-06 */ export async function CopyDirectory(src, dest) { // Get the length of the source path to calculate relative paths const srcLength = src.length; // Initialize a stack to keep track of directories to process const list = [src]; // Process directories and files in a loop until the stack is empty while (list.length > 0) { // Pop the last item from the stack const item = list.pop(); /* istanbul ignore next */ // Ignore coverage for the "undefined" check (should never happen) if (item === undefined) { continue; } // Check if the item is a directory if (await IsDirectory(item)) { // If it's a directory, list its contents and add them to the stack const files = await promises.readdir(item); for (const file of files) { list.push(join(item, file)); } } else { // If it's a file, calculate relative paths and copy the file const file = item.substring(srcLength); const to = join(dest, file); await CopyFile(item, to); } } } /** * 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 2021-08-06 */ export async function CopyFileOrDirectory(src, dest) { // 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); return true; } else { // If it's a file, copy the file return await CopyFile(src, dest); } } /** * 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 2023-12-06 */ export async function CopyToDirectory(files, destDir) { 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); } else { // If it's a file, copy the file to the destination await CopyFile(file, dest); } }; 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 2023-12-05 */ export function AppendPathThenCreateDirectory(dir, path) { return MakeDirectorySyncIfNotExists(AppendPathToDirectory(dir, path)); } /** * @since 5.4.0 * @version 2023-12-05 */ export function AppendPathThenCreateParentDirectory(dir, path) { const result = AppendPathToDirectory(dir, path); MakeDirectorySyncIfNotExists(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 ansiToHtml * @param options * @since 1.0.5 * @version 2023-11-18 */ export async function GetFileContent(path, ansiToHtml = false, options = "utf8") { const data = await readFile(path, options); // 2021-09-13 if (data === "") { return data; } if (ansiToHtml) { const ansi_up = new AnsiUp(); const content = ansi_up.ansi_to_html(data); const lines = SplitStringByNewline(content); if (lines) { if (lines.length === 1) { return lines[0]; } else { const arr = lines.map((line) => { const temp = Strings_HTML_BR + line.replace(/^\s+/, (m) => { return m.replace(/\s/g, Strings_HTML_NBSP); }); return temp; }); return arr.join(""); } } else { return Strings_Empty; } } else { return data; } } /** * * @param path * @param ansiToHtml * @param options * @since 5.0.1 * @version 2023-11-18 */ export function GetFileContentSync(path, ansiToHtml = false, options = "utf8") { const data = readFileSync(path, options); // 2021-09-13 if (data === "") { return data; } if (ansiToHtml) { const ansi_up = new AnsiUp(); const content = ansi_up.ansi_to_html(data); const lines = SplitStringByNewline(content); if (lines) { if (lines.length === 1) { return lines[0]; } else { const arr = lines.map((line) => { const temp = Strings_HTML_BR + line.replace(/^\s+/, (m) => { return m.replace(/\s/g, Strings_HTML_NBSP); }); return temp; }); return arr.join(""); } } else { return Strings_Empty; } } else { 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); } /** * * @param fileName * @returns * @since 3.0.1 * @version 2021-09-01 */ export async function GetFileLines(fileName) { const content = await GetFileContent(fileName); const lines = SplitStringByNewline(content); return { endOfLineSequence: content.includes(Strings_CR_LF) ? Strings_CR_LF : Strings_LF, lines: lines, }; } /** * @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 2024-05-24 */ export async function DeleteFilesByExtension(src, extensions, caseSensitive = false, 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) { KavenLogger.Default.Warn(`Error getting stats for file ${file}:`, err); continue; } if (stats.isFile()) { const str = caseSensitive ? file : file.toLowerCase(); if (extensions.some(p => str.endsWith(p))) { KavenLogger.Default.Info(`Delete: ${file}`); await promises.unlink(file); continue; } } else if (stats.isDirectory()) { const dir = basename(file); const str = caseSensitive ? dir : dir.toLowerCase(); if (ignoreFolderNames.includes(str)) { KavenLogger.Default.Info(`Ignore: ${file}`); continue; } const files = await promises.readdir(file); list.push(...files.map(p => join(file, p))); } } }