tiny-essentials
Version:
Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.
361 lines (360 loc) • 11.6 kB
JavaScript
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, lstatSync, rmSync, unlinkSync, copyFileSync, statSync, renameSync, } from 'fs';
import { join, dirname, basename, extname } from 'path';
import { toTitleCase } from '../basics/text.mjs';
/*========================*
* JSON Operations
*========================*/
/**
* Reads and parses a JSON file.
* Throws an error if the file content is not valid JSON.
* @param {string} filePath
* @returns {any}
*/
export function readJsonFile(filePath) {
if (!existsSync(filePath))
throw new Error(`File not found: ${filePath}`);
const content = readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}
/**
* Saves an object as JSON to a file.
* Automatically creates the directory if it does not exist.
* @param {string} filePath
* @param {any} data
* @param {number} [spaces=2]
*/
export function writeJsonFile(filePath, data, spaces = 2) {
const json = JSON.stringify(data, null, spaces);
writeFileSync(filePath, json, 'utf-8');
}
/*========================*
* Directory Management
*========================*/
/**
* Ensures that the directory exists, creating it recursively if needed.
* @param {string} dirPath
*/
export function ensureDirectory(dirPath) {
if (!existsSync(dirPath)) {
mkdirSync(dirPath, { recursive: true });
}
}
/**
* Clears all contents inside a directory but keeps the directory.
* @param {string} dirPath
*/
export function clearDirectory(dirPath) {
if (!existsSync(dirPath))
return;
const files = readdirSync(dirPath);
for (const file of files) {
const fullPath = join(dirPath, file);
const statData = lstatSync(fullPath);
if (statData.isDirectory()) {
rmSync(fullPath, { recursive: true, force: true });
}
else {
unlinkSync(fullPath);
}
}
}
/*========================*
* File Checks
*========================*/
/**
* Checks whether a file exists.
* @param {string} filePath
* @returns {boolean}
*/
export function fileExists(filePath) {
return existsSync(filePath) && lstatSync(filePath).isFile();
}
/**
* Checks whether a directory exists.
* @param {string} dirPath
* @returns {boolean}
*/
export function dirExists(dirPath) {
return existsSync(dirPath) && lstatSync(dirPath).isDirectory();
}
/**
* Checks whether a directory is empty.
* @param {string} dirPath
* @returns {boolean}
*/
export function isDirEmpty(dirPath) {
return readdirSync(dirPath).length === 0;
}
/*========================*
* File Operations
*========================*/
/**
* Copies a file to a destination.
* @param {string} src
* @param {string} dest
* @param {number} [mode]
*/
export function ensureCopyFile(src, dest, mode) {
ensureDirectory(dirname(dest));
copyFileSync(src, dest, mode);
}
/**
* Deletes a file (If the file exists).
* @param {string} filePath
* @returns {boolean}
*/
export function tryDeleteFile(filePath) {
if (fileExists(filePath)) {
unlinkSync(filePath);
return true;
}
return false;
}
/*========================*
* Text Operations
*========================*/
/**
* Writes text to a file (Ensures that the directory exists, creating it recursively if needed).
* @param {string} filePath
* @param {string} content
* @param {import('fs').WriteFileOptions} [ops='utf-8']
*/
export function writeTextFile(filePath, content, ops = 'utf-8') {
const dir = dirname(filePath);
ensureDirectory(dir);
writeFileSync(filePath, content, ops);
}
/*========================*
* Directory Listings
*========================*/
/**
* Lists all files and dirs in a directory (optionally recursive).
* @param {string} dirPath
* @param {boolean} [recursive=false]
* @returns {{ files: string[]; dirs: string[] }}
*/
export function listFiles(dirPath, recursive = false) {
/** @type {{ files: string[]; dirs: string[] }} */
const results = { files: [], dirs: [] };
if (!dirExists(dirPath))
return results;
const entries = readdirSync(dirPath);
for (const entry of entries) {
const fullPath = join(dirPath, entry);
const statData = lstatSync(fullPath);
if (statData.isDirectory()) {
results.dirs.push(fullPath);
if (recursive) {
const results2 = listFiles(fullPath, true);
results.files.push(...results2.files);
results.dirs.push(...results2.dirs);
}
}
else {
results.files.push(fullPath);
}
}
return results;
}
/**
* Lists all directories in a directory (optionally recursive).
* @param {string} dirPath
* @param {boolean} [recursive=false]
* @returns {string[]}
*/
export function listDirs(dirPath, recursive = false) {
/** @type {string[]} */
const results = [];
if (!dirExists(dirPath))
return results;
const entries = readdirSync(dirPath);
for (const entry of entries) {
const fullPath = join(dirPath, entry);
const statData = lstatSync(fullPath);
if (statData.isDirectory()) {
results.push(fullPath);
if (recursive) {
results.push(...listDirs(fullPath, true));
}
}
}
return results;
}
/*========================*
* File Info
*========================*/
/**
* Returns the size of a file in bytes.
* @param {string} filePath
* @returns {number}
*/
export function fileSize(filePath) {
if (!fileExists(filePath))
return 0;
const stats = statSync(filePath);
return stats.size;
}
/**
* Returns the total size of a directory in bytes.
* @param {string} dirPath
* @returns {number}
*/
export function dirSize(dirPath) {
let total = 0;
const files = listFiles(dirPath, true).files;
for (const file of files) {
total += fileSize(file);
}
return total;
}
/*========================*
* Backup Utilities
*========================*/
/**
* Creates a backup copy of a file with .bak timestamp suffix.
* @param {string} filePath
* @param {string} [ext='bak']
*/
export function backupFile(filePath, ext = 'bak') {
if (!fileExists(filePath))
return;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = `${filePath}.${ext}.${timestamp}`;
ensureCopyFile(filePath, backupPath);
}
/**
* Returns the most recent backup file path for a given file.
* @param {string} filePath
* @param {string} [ext='bak']
* @returns {string} Full path to the most recent backup
*/
export function getLatestBackupPath(filePath, ext = 'bak') {
const dir = dirname(filePath);
const baseName = basename(filePath);
const backups = readdirSync(dir)
.filter((name) => name.startsWith(`${baseName}.${ext}.`))
.sort()
.reverse();
if (backups.length === 0)
throw new Error(`No backups found for ${filePath}`);
return join(dir, backups[0]);
}
/**
* Restores the most recent backup of a file.
* @param {string} filePath
* @param {string} [ext='bak']
*/
export function restoreLatestBackup(filePath, ext = 'bak') {
const latestBackup = getLatestBackupPath(filePath, ext);
ensureCopyFile(latestBackup, filePath);
}
/*========================*
* Rename Utilities
*========================*/
/**
* Renames multiple files in a directory using a rename function.
* @param {string} dirPath - The target directory.
* @param {(original: string, index: number) => string} renameFn - Function that returns new filename.
* @param {string[]} [extensions] - Optional: Only rename files with these extensions.
*
* @throws {TypeError} If any argument has an invalid type.
* @throws {Error} If the directory does not exist or contains invalid files.
*/
export function renameFileBatch(dirPath, renameFn, extensions = []) {
// Validate types
if (typeof dirPath !== 'string')
throw new TypeError('dirPath must be a string');
if (typeof renameFn !== 'function')
throw new TypeError('renameFn must be a function');
if (!Array.isArray(extensions))
throw new TypeError('extensions must be an array of strings');
if (!existsSync(dirPath) || !statSync(dirPath).isDirectory())
throw new Error(`Directory not found or invalid: ${dirPath}`);
for (const ext of extensions) {
if (typeof ext !== 'string' || !ext.startsWith('.'))
throw new TypeError(`Invalid extension: ${ext}`);
}
const files = listFiles(dirPath).files;
let index = 0;
for (const file of files) {
const ext = extname(file);
if (extensions.length && !extensions.includes(ext))
continue;
const originalName = basename(file);
const newName = renameFn(originalName, index++);
const newPath = join(dirPath, newName);
if (originalName === newName)
continue;
renameSync(file, newPath);
}
}
/**
* Renames files using regex replacement.
* @param {string} dirPath
* @param {RegExp} pattern - Regex to match in the filename.
* @param {string} replacement - Replacement string.
* @param {string[]} [extensions]
*/
export function renameFileRegex(dirPath, pattern, replacement, extensions = []) {
renameFileBatch(dirPath, (filename) => {
const ext = extname(filename);
const name = basename(filename, ext).replace(pattern, replacement);
return `${name}${ext}`;
}, extensions);
}
/**
* Adds a prefix or suffix to filenames.
* @param {string} dirPath
* @param {{ prefix?: string, suffix?: string }} options
* @param {string[]} [extensions]
*/
export function renameFileAddPrefixSuffix(dirPath, { prefix = '', suffix = '' }, extensions = []) {
renameFileBatch(dirPath, (filename) => {
const ext = extname(filename);
const name = basename(filename, ext);
return `${prefix}${name}${suffix}${ext}`;
}, extensions);
}
/**
* Normalizes all filenames to lowercase (or uppercase).
* @param {string} dirPath
* @param {'lower' | 'upper' | 'title'} mode
* @param {string[]} [extensions]
* @param {boolean} [normalizeExt=false] - Whether to apply case change to file extensions too.
* @throws {Error} If mode is invalid.
*/
export function renameFileNormalizeCase(dirPath, mode = 'lower', extensions = [], normalizeExt = false) {
if (typeof mode !== 'string' || !['lower', 'upper', 'title'].includes(mode))
throw new Error(`Invalid mode "${mode}". Must be 'lower', 'upper' or 'title'.`);
renameFileBatch(dirPath, (filename) => {
/**
* @param {string} text
* @returns {string}
*/
const changeToMode = (text) => {
if (mode === 'lower')
return text.toLowerCase();
else if (mode === 'upper')
return text.toUpperCase();
else if (mode === 'title')
return toTitleCase(text);
else
return text;
};
const rawExt = extname(filename);
const ext = normalizeExt ? changeToMode(rawExt) : rawExt;
const name = changeToMode(basename(filename, rawExt));
return `${name}${ext}`;
}, extensions);
}
/**
* Pads numbers in filenames (e.g., "img1.jpg" -> "img001.jpg").
* @param {string} dirPath
* @param {number} totalDigits
* @param {string[]} [extensions]
*/
export function renameFilePadNumbers(dirPath, totalDigits = 3, extensions = []) {
renameFileBatch(dirPath, (filename) => {
return filename.replace(/\d+/, (match) => match.padStart(totalDigits, '0'));
}, extensions);
}