kaven-utils
Version:
Utils for Node.js.
706 lines (705 loc) • 19.7 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: 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)));
}
}
}