fsmate
Version:
A lightweight Node.js library for working with the file system (fs) in a simplified way.
1,814 lines (1,587 loc) • 53.2 kB
JavaScript
/**
* fsMate v2.5.0
* A modular collection of file system utilities for Node.js
* https://github.com/jsvibe/fsMate
*
* @license MIT
* @copyright 2025 Indian Modassir
* @author Indian Modassir
*
* Date: 11-09-2025 12:59:59 GMT+0530
*/
const process = require('process');
const path = require('path');
const fs = require('fs');
const fs$prom = fs.promises;
const { Readable } = require('stream');
const { sprintf } = require('printfy');
const crypto = require('crypto');
/* fsMate */
const fsMate = {};
const CRLF = /\r?\n/g;
const stream = {};
// 1MB chunks for speed & safety
const highWaterMark = 1024 ** 2;
const arr = [];
const slice = arr.slice;
let lastError;
// Executes a file system method safely and returns false on error
async function fs$async(method) {
const fn = fs$prom[method] || fsMate[method];
let output;
lastError = '';
try {
output = await fn.apply(null, slice.call(arguments, 1));
return output != null ? output : true;
} catch(e) {
return lastError = e, false;
}
}
// Executes a synchronous FS method safely and returns false on error
function fs$sync(method) {
method += 'Sync';
const fn = fs[method] || fsMate[method];
let output;
lastError = '';
try {
output = fn.apply(null, slice.call(arguments, 1));
return output != null ? output : true;
} catch(e) {
return lastError = e, false;
}
}
// Reverses the given string
function strrev(str) {
return str.split('').reverse().join('');
}
// Ensures the input is iterable; wraps string into an array
function iterable(obj) {
return typeof obj === 'string' ? [obj] : obj;
}
// Iterates over items and calls the async callback for each, providing resolve/reject
function safeAsyncEach(obj, callback) {
return new Promise(function(resolve, reject) {
obj = iterable(obj);
let name;
for(name in obj) {
callback.call(fsMate, resolve, reject, obj[name]);
}
});
}
// Replaces characters in 'str' based on mapping from 'from' to 'to'
function strtr(str, from, to) {
let index, i = 0,
result = '';
for (; i < str.length; i++) {
index = from.indexOf(str[i]);
result += index > -1 && to[index] || str[i];
}
return result;
}
// Dynamically invokes an fs method (sync or async); throws if unsupported
function $fs(method, args, async) {
let fn = async
? fs$prom[method]
: fs[method];
if (typeof fn !== 'function') {
throw new Error(
sprintf('Unsupported: [fs.%s] update node version.', method)
);
}
return fn.apply(null, args);
};
/**
* Generates a temporary,
* obfuscated file or directory name using a given prefix.
*
* @param {string} prefix Base directory for the temporary name.
* @returns {string} A unique, safe temporary path.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/tmpName.md
*/
fsMate.tmpName = function(prefix) {
return sprintf('%s/.!%s', prefix, strrev(strtr(crypto.randomBytes(2).toString('base64'), '/=', '-!')));
};
/**
* Generates a temporary,
* obfuscated file or directory name using a given prefix.
* and encode filename in sha1 then convert hex format.
*
* @param {string} prefix Base directory for the temporary name.
* @param {string} suffix The file basename
* @returns {string} A unique, safe temporary path.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/tempNam.md
*/
fsMate.tempNam = function(prefix, suffix) {
return sprintf('%s/%s.tmp', prefix, crypto.createHash('sha1').update(suffix).digest('hex'));
};
/**
* Tells whether the filename is a regular file
*
* @param {string} path Path to the file.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isFile.md
*/
fsMate.isFile = function(path) {
return stat(path, 'isFile');
};
/**
* Tells whether the filename is a regular file
*
* @param {string} path Path to the file.
* @returns {boolean} Returns true if the file exists,
* false otherwise.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isFileSync.md
*/
fsMate.isFileSync = function(path) {
return stat(path, 'isFile', true);
};
/**
* Tells whether the filename is a directory
*
* @param {string} path Path to the dir.
* @returns {boolean} Returns true if the dir exists,
* false otherwise.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isDirSync.md
*/
fsMate.isDirSync = function(path) {
return stat(path, 'isDirectory', true);
};
/**
* Tells whether the filename is a directory
*
* @param {string} path Path to the dir.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isDir.md
*/
fsMate.isDir = function(path) {
return stat(path, 'isDirectory');
};
/**
* Tells whether the filename is a directory
*
* @param {string} path Path to the file.
* @returns {Promise<any>} Resolves or rejects via callback.
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isLink.md
*/
fsMate.isLink = function(path) {
return stat(path, 'isSymbolicLink');
};
/**
* Tells whether the filename is a symbolic link.
*
* @param {string} path Path to the file.
* @returns {boolean} Returns true if the dir exists,
* false otherwise.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isLinkSync.md
*/
fsMate.isLinkSync = function(path) {
return stat(path, 'isSymbolicLink', true);
};
/* STAT */
function stat(path, executer, sync) {
let execute = function(stats) {
return executer ? stats[executer]() : stats;
};
try {
lastError = '';
return sync
? execute(fs.statSync(path))
: new Promise(function (resolve, reject) {
fs.stat(path, function (err, stats) {
err ? reject(err) : resolve(execute(stats));
});
});
} catch (e) {
return lastError = e, false;
}
}
/**
* Tells whether a file or a dir exists and is readable
*
* @param {string} path Path to the file or directory.
* @returns {boolean} Returns true if the readable,
* false otherwise.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isReadableSync.md
*/
fsMate.isReadableSync = function(path) {
return access(path, fs.constants.R_OK, true);
};
/**
* Tells whether a file or a dir exists and is readable
*
* @param {string} path Path to the file or directory.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isReadable.md
*/
fsMate.isReadable = function(path) {
return access(path, fs.constants.R_OK);
};
/**
* Tells whether the filename is writable
*
* @param {string} path Path to the file or directory.
* @returns {boolean} Returns true if the writable,
* false otherwise.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isWritableSync.md
*/
fsMate.isWritableSync = function(path) {
return access(path, fs.constants.W_OK, true);
};
/**
* Tells whether the filename is writable
*
* @param {string} path Path to the file or directory.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isWritable.md
*/
fsMate.isWritable = function(path) {
return access(path, fs.constants.W_OK);
};
/**
* Tells whether the filename is executable
*
* @param {string} path Path to the file or directory.
* @returns {boolean} Returns true if the executable,
* false otherwise.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isExecutableSync.md
*/
fsMate.isExecutableSync = function(path) {
return access(path, fs.constants.X_OK, true);
};
/**
* Tells whether the filename is executable
*
* @param {string} path Path to the file or directory.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/isExecutable.md
*/
fsMate.isExecutable = function(path) {
return access(path, fs.constants.X_OK);
};
/* ACCESS */
function access(path, mode, sync) {
try {
lastError = '';
return sync
? ($fs('accessSync', [path, mode]), true)
: new Promise(function (resolve, reject) {
$fs('access', [path, mode, function(err) {
err ? reject(err) : resolve(!err);
}]);
});
} catch (e) {
return lastError = e, false;
}
}
/**
* Gets file size
*
* @param {string} filename Path to the file.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/filesize.md
*/
fsMate.filesize = function(filename) {
return new Promise(function(resolve, reject) {
fs.stat(filename, function(err, stats) {
err ? reject(err) : resolve(stats.size);
});
});
};
/**
* Gets file size
*
* @param {string} filename Path to the file.
* @returns {number} Returns a filesize in bytes format
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/filesizeSync.md
*/
fsMate.filesizeSync = function(filename) {
return fs.statSync(filename).size;
};
/**
* Creates a directories recursively
* Attempts to create the directory specified by pathname.
*
* @param {string|string[]} paths Path to file or files iterator
* @param {number} mode A file mode.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/mkdir.md
*/
fsMate.mkdir = function(paths, mode = 0o777) {
return safeAsyncEach(paths, function(resolve, reject, path) {
fs.mkdir(path, {recursive: true, mode}, function(err, path) {
err ? reject(err) : resolve(!!path);
});
});
};
/**
* Creates a directories recursively
* Attempts to create the directory specified by pathname.
*
* @param {string|string[]} paths Path to file or files iterator
* @param {number} mode A file mode.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/mkdirSync.md
*/
fsMate.mkdirSync = function(paths, mode = 0o777) {
paths = iterable(paths);
let path;
for(path of paths) {
if (!this.isDirSync(path)) {
fs.mkdirSync(path, {recursive: true, mode});
}
}
};
/**
* Creates files.
*
* @param {string|string[]} paths Path to file or files iterator
* @param {boolean} overwrite true for overwrite, default false
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/mkfile.md
*/
fsMate.mkfile = async function(paths, overwrite = false) {
paths = iterable(paths);
let path, fd;
for(path of paths) {
if (!overwrite && await fs$async('isReadable', path)) {
throw new Error(
sprintf('Failed to create [%s] file already exists.', path)
);
}
fd = await fs$prom.open(path, 'w');
await fd.close();
}
};
/**
* Checks if a file or directory exists (async).
* Wraps fs.exists in a Promise.
*
* @param {string} path Path to check.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/exists.md
*/
fsMate.exists = function(path) {
return new Promise(function(resolve) {
fs.exists(path, function(exists) {
resolve(exists);
});
});
};
/**
* Creates files.
*
* @param {string|string[]} paths Path to file or files iterator
* @param {boolean} overwrite true for overwrite, default false
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/mkfileSync.md
*/
fsMate.mkfileSync = function(paths, overwrite = false) {
paths = iterable(paths);
let fd, path;
for(path of paths) {
if (!overwrite && this.isReadableSync(path)) {
throw new Error(sprintf(
'Cannot rename because the newPath [%s] already exists.',
path
));
}
fd = fs.openSync(path, 'w');
fs.closeSync(fd);
}
};
/**
* Sets access and modification time of file.
* Synchronously change file timestamps of the file referenced by the supplied path.
*
* @param {string|string[]} files Path to file or files iterator
* @param {number} mtime The last modified time. If a string is provided, it will be coerced to number.
* @param {number} atime The last access time. If a string is provided, it will be coerced to number.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/touchSync.md
*/
fsMate.touchSync = function(files, mtime, atime) {
files = iterable(files);
let file;
for(file of files) {
this.mkdirSync(path.dirname(file));
this.mkfileSync(file, true);
if (mtime) {
fs.utimesSync(file, atime, mtime);
}
}
};
/**
* Sets access and modification time of file.
* Asynchronously change file timestamps of the file referenced by the supplied path.
*
* @param {string|string[]} files Path to file or files iterator
* @param {number} mtime The last modified time. If a string is provided, it will be coerced to number.
* @param {number} atime The last access time. If a string is provided, it will be coerced to number.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/touch.md
*/
fsMate.touch = async function(files, mtime, atime) {
files = iterable(files);
let file;
for(file of files) {
await this.mkdir(path.dirname(file));
await this.mkfile(file, true);
if (mtime) {
await fs$prom.utimes(file, atime, mtime);
}
}
};
/**
* Renames a file or directory.
*
* @param {string} oldPath The old file path
* @param {string} newPath The new file path
* @param {boolean} overwrite true for overwrite, default false
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/rename.md
*/
fsMate.rename = async function(oldPath, newPath, overwrite = false) {
if (!overwrite &&
(await fs$async('isReadable', newPath))) {
throw new Error(sprintf(
'Cannot rename because the newPath [%s] already exists.',
newPath
));
}
await fs$prom.rename(oldPath, newPath);
};
/**
* Renames a file or directory.
*
* @param {string} oldPath The old file path
* @param {string} newPath The new file path
* @param {boolean} overwrite true for overwrite, default false
* @returns {boolean} Returns true if the renamed,
* false otherwise
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/renameSync.md
*/
fsMate.renameSync = function(oldPath, newPath, overwrite = false) {
// We check that target does not exist
if (!overwrite && this.isReadableSync(newPath)) {
throw new Error(sprintf(
'Cannot rename because the newPath [%s] already exists.',
newPath
));
}
return fs$sync('rename', oldPath, newPath);
};
/**
* Moves a file or directory.
*
* @param {string} oldPath The old file path
* @param {string} newPath The new file path
* @param {boolean} overwrite true for overwrite, default false
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/move.md
*/
fsMate.move = async function(oldPath, newPath, overwrite = false) {
const oldDir = path.resolve(path.dirname(oldPath));
const newDir = path.resolve(path.dirname(newPath));
if (path.resolve(oldDir) === path.resolve(newDir)) {
throw new Error(sprintf(
'Failed to move file or directory: [%s] and [%s] are the same.',
oldDir,
newDir
));
}
return this.rename(oldPath, newPath, overwrite);
};
/**
* Moves a file or directory.
*
* @param {string} oldPath The old file path
* @param {string} newPath The new file path
* @param {boolean} overwrite true for overwrite, default false
* @returns {boolean} Returns true if the renamed,
* false otherwise
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/moveSync.md
*/
fsMate.moveSync = function(oldPath, newPath, overwrite = false) {
const oldDir = path.resolve(path.dirname(oldPath));
const newDir = path.resolve(path.dirname(newPath));
if (path.resolve(oldDir) === path.resolve(newDir)) {
throw new Error(sprintf(
'Failed to move file or directory: [%s] and [%s] are the same.',
oldDir,
newDir
));
}
return this.renameSync(oldPath, newPath, overwrite);
};
function filterWith(results, target) {
let value,
ret = [],
length = results.length,
indexOf = [].indexOf,
i = 0;
for(; i < length; i++) {
value = results[i];
if (Array.isArray(target) && indexOf.call(target, value) === -1) {
ret.push(value);
} else if (typeof target === 'function') {
if (target(value, i, results)) {
ret.push(value);
}
}
}
return ret;
}
/**
* Recursively reads directory contents.
* Supports options for deep scan, file types, and full paths.
*
* @param {string} dir Path to the dir
* @param {Object} options Scan options.
* @param {Array<string>|Function} filter Custom filteration.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/scandir.md
*/
fsMate.scandir = async function(dir, options = {}, filter) {
let file, buffer, filePath, allow,
files = await fs$prom.readdir(dir, {withFileTypes: true}),
{withDeepScan, withFileTypes, withFullPath} = options,
results = arguments[3] || [];
// Directories filteration
if (options.dirOnly) {
allow = 'Directory';
}
// Handle only file filteration
if (options.fileOnly) {
allow = 'File';
}
for(file of files) {
buffer = file.name;
filePath = path.join(dir, buffer);
// Attach with fileTypes
if (withFileTypes) {
buffer = file;
}
// Attach with full initial file path
if (withFullPath) {
buffer.name ? buffer.name = filePath : buffer = filePath;
}
// Directory or File filteration
if (allow) {
if (file['is' + allow]()) {
results.push(buffer);
}
// Otherwise, store all files and directories
} else {
results.push(buffer);
}
// For deep scaning
if (withDeepScan && file.isDirectory()) {
await fsMate.scandir(filePath, options, filter, results);
}
}
return Promise.resolve(filter ? filterWith(results, filter) : results);
};
/**
* Recursively reads directory contents.
* Supports options for deep scan, file types, and full paths.
*
* @param {string} dir Path to the dir
* @param {Object} options Scan options.
* @param {Array<string>|Function} filter Custom filteration.
* @returns {Array<string>} Array of file names, Dirent objects, or full paths.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/scandirSync.md
*/
fsMate.scandirSync = function(dir, options = {}, filter) {
let file, buffer, filePath, allow,
files = fs.readdirSync(dir, {withFileTypes: true}),
{withDeepScan, withFileTypes, withFullPath} = options,
results = arguments[3] || [];
// Directories filteration
if (options.dirOnly) {
allow = 'Directory';
}
// Handle only file filteration
if (options.fileOnly) {
allow = 'File';
}
for(file of files) {
buffer = file.name;
filePath = path.join(dir, buffer);
// Attach with fileTypes
if (withFileTypes) {
buffer = file;
}
// Attach with full initial file path
if (withFullPath) {
buffer.name ? buffer.name = filePath : buffer = filePath;
}
// Directory or File filteration
if (allow) {
if (file['is' + allow]()) {
results.push(buffer);
}
// Otherwise, store all files and directories
} else {
results.push(buffer);
}
// For deep scaning
if (withDeepScan && file.isDirectory()) {
fsMate.scandirSync(filePath, options, filter, results);
}
}
return filter ? filterWith(results, filter) : results;
};
/**
* Removes files or directories.
* Renames directories to temp names before deletion to avoid issues.
*
* @param {string|string[]} files Path to file or files iterator
* @param {boolean} recursive Whether to remove directories recursively.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/remove.md
*/
fsMate.remove = async function(files, recursive) {
let file, tmpName, fsIterator, origFile;
// Reverse to delete children before parents
files = typeof files !== 'string'
? files.reverse()
: [files];
// Take advantage of `fs.rm` if supported!
if (fs.rm) {
// return this.rm(files);
}
for(file of files) {
if (await this.isLink(file)) {
if (!(await fs$async('unlink', file)) || path.sep !== '\\' || (await fs$async('rmdir', file))) {
throw lastError;
}
} else if (await this.isDir(file)) {
if (!recursive) {
tmpName = this.tmpName(path.dirname(await fs$prom.realpath(file)));
if (await this.exists(tmpName)) {
await this.remove([tmpName], true);
}
// Renaming temp dir [.!!oTZ], if not exists
if (!(await this.exists(tmpName)) && (await fs$async('rename', file, tmpName))) {
origFile = file;
file = tmpName;
} else {
origFile = null;
}
}
fsIterator = await this.scandir(file, {withFullPath: true});
await this.remove(fsIterator, true);
// Removes empty directory
if (!(await fs$async('rmdir', file)) && (await this.exists(file)) && !recursive) {
if (origFile && await fs$async('rename', file, origFile)) {
file = origFile;
}
throw lastError;
}
// Removes files
} else if (!(await fs$async('unlink', file)) && (lastError || (await this.exists(file)))) {
throw lastError;
}
}
};
/**
* Removes files or directories.
* Renames directories to temp names before deletion to avoid issues.
*
* @param {string|string[]} files Path to file or files iterator
* @param {boolean} recursive Whether to remove directories recursively.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/removeSync.md
*/
fsMate.removeSync = function(files, recursive) {
let file, tmpName, fsIterator, origFile;
// Reverse to delete children before parents
files = typeof files !== 'string'
? files.reverse()
: [files];
// Take advantage of `fs.rm` if supported!
if (fs.rmSync) {
return this.rmSync(files);
}
for(file of files) {
if (this.isLinkSync(file)) {
if (!fs$sync('unlink', file) || path.sep !== '\\' || fs$sync('rmdir', file)) {
throw lastError;
}
} else if (this.isDirSync(file)) {
if (!recursive) {
tmpName = this.tmpName(path.dirname(file));
if (fs.existsSync(tmpName)) {
this.removeSync([tmpName], true);
}
// Renaming temp dir [.!!oTZ], if not exists
if (!fs.existsSync(tmpName) && fs$sync('rename', file, tmpName)) {
origFile = file;
file = tmpName;
} else {
origFile = null;
}
}
fsIterator = this.scandirSync(file, {withFullPath: true});
this.removeSync(fsIterator, true);
// Removes empty directory
if (!fs$sync('rmdir', file) && fs.existsSync(file) && !recursive) {
if (origFile && fs$sync('rename', file, origFile)) {
file = origFile;
}
throw lastError;
}
// Removes files
} else if (!fs$sync('unlink', file) && (lastError || fs.existsSync(file))) {
throw lastError;
}
}
};
/**
* Removes files or directories asynchronously with options.
* Renames directories to temp names before deletion to avoid issues.
*
* @param {string|string[]} files Path to file or files iterator
* @param {Object} options Removal options
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/rm.md
*/
fsMate.rm = async function(files, options) {
let file, tmpName, origFile;
options = options || {recursive: true, force: true};
// Reverse to delete children before parents
files = typeof files !== 'string'
? files.reverse()
: [files];
for(file of files) {
if (await this.isDir(file)) {
tmpName = this.tmpName(path.dirname(await fs$prom.realpath(file)));
// Renaming temp dir [.!!oTZ], if not exists
if (!(await this.exists(tmpName)) && (await fs$async('rename', file, tmpName))) {
origFile = file;
file = tmpName;
} else {
origFile = null;
}
}
try {
// Final remove it!
await $fs('rm', [file, options], true);
} catch(e) {
// Restore original folder name
if (origFile && await fsMate.rename(file, origFile)) {
file = origFile;
}
throw e;
}
}
};
/**
* Removes files or directories asynchronously with options.
* Renames directories to temp names before deletion to avoid issues.
*
* @param {string|string[]} files Path to file or files iterator
* @param {Object} options Removal options
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/rmSync.md
*/
fsMate.rmSync = function(files, options) {
let file, tmpName, origFile;
options = options || {recursive: true, force: true};
// Reverse to delete children before parents
files = typeof files !== 'string'
? files.reverse()
: [files];
for(file of files) {
if (this.isDirSync(file)) {
tmpName = this.tmpName(path.dirname(fs.realpathSync(file)));
// Renaming temp dir [.!!oTZ], if not exists
if (!fs.existsSync(tmpName) && fs$sync('rename', file, tmpName)) {
origFile = file;
file = tmpName;
} else {
origFile = null;
}
}
// Final remove synchronously
$fs('rmSync', [file, options]);
}
};
/**
* Mirrors a directory to another.
*
* Copies files and directories from the origin directory into the target directory. By default:
*
* - existing files in the target directory will be overwritten,
* except if they are newer (see the `overwrite` option)
* - files in the target directory that do not exist in the source directory will not be deleted
*
* @param {string} originDir The origin directory path
* @param {string} targetDir Destination path
* @param {boolean} overwrite true for overwrite, default false
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/mirror.md
*/
fsMate.mirror = async function(originDir, targetDir, overwrite = false) {
let file, fsIterator, target;
if (!(await fs$async('isDir', originDir))) {
throw lastError;
}
// Creates first directory
await this.mkdir(targetDir);
fsIterator = await this.scandir(originDir, {
withDeepScan: true,
withFullPath: true,
withFileTypes: true
});
for(file of fsIterator) {
target = path.join(targetDir, file.name.slice(originDir.length));
if (file.isDirectory()) {
await this.mkdir(target);
} else if (file.isFile()) {
await this.copy(file.name, target, overwrite);
} else {
throw new Error(
sprintf('Unable to guess [%s] file type.', file.name)
);
}
}
};
/**
* Mirrors a directory to another.
*
* Copies files and directories from the origin directory into the target directory. By default:
*
* - existing files in the target directory will be overwritten,
* except if they are newer (see the `overwrite` option)
* - files in the target directory that do not exist in the source directory will not be deleted
*
* @param {string} originDir The origin directory path
* @param {string} targetDir Destination path
* @param {boolean} overwrite true for overwrite, default false
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/mirrorSync.md
*/
fsMate.mirrorSync = function(originDir, targetDir, overwrite = false) {
let file, fsIterator, target;
this.isDirSync(originDir);
this.mkdirSync(targetDir);
fsIterator = this.scandirSync(originDir, {
withDeepScan: true,
withFullPath: true,
withFileTypes: true
});
for(file of fsIterator) {
target = path.join(targetDir, file.name.slice(originDir.length));
if (file.isDirectory()) {
this.mkdirSync(target);
} else if (file.isFile()) {
this.copySync(file.name, target, overwrite);
} else {
throw new Error(
sprintf('Unable to guess [%s] file type.', file.name)
);
}
}
};
/**
* Copies a file.
*
* If the target file is older than the origin file, it's always overwritten.
* If the target file is newer, it is overwritten only when the
* overwrite option is set to true.
*
* @param {string} originFile The origin file path
* @param {string} targetFile Destination file path
* @param {boolean} overwrite true for overwrite, default false
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/copySync.md
*/
fsMate.copySync = function(originFile, targetFile, overwrite = false) {
let doCopy = true;
if (!this.isFileSync(originFile)) {
throw lastError;
}
if (!overwrite && this.isFileSync(targetFile)) {
doCopy = false;
}
// Creates directory
this.mkdirSync(path.dirname(targetFile));
if (doCopy) {
fs.copyFileSync(originFile, targetFile);
}
};
/**
* Copies a file.
*
* If the target file is older than the origin file, it's always overwritten.
* If the target file is newer, it is overwritten only when the
* overwrite option is set to true.
*
* @param {string} originFile The origin file path
* @param {string} targetFile Destination file path
* @param {boolean} overwrite true for overwrite, default false
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/copy.md
*/
fsMate.copy = async function(originFile, targetFile, overwrite = false) {
let source, dest, doCopy = true;
if (!(await fs$async('isFile', originFile))) {
throw lastError;
}
if (!overwrite &&
(await fs$async('isFile', targetFile))) {
doCopy = false;
}
await this.mkdir(path.dirname(targetFile));
if (doCopy) {
return new Promise(function(resolve, reject) {
source = $fs('createReadStream', [originFile]);
dest = $fs('createWriteStream', [targetFile]);
source.on("error", reject);
dest.on("error", reject);
source.pipe(dest).on("error", reject).on('finish', resolve);
});
}
};
/**
* Empty a file synchronously.
* If the file does not exist, it will be created as empty.
*
* @param {string} path The file path.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/truncateSync.md
*/
fsMate.truncateSync = function(path) {
if (fs.truncateSync) {
fs.truncateSync(path, 0);
} else {
const fd = fs.openSync(path, 'w');
fs.closeSync(fd);
}
};
/**
* Empty a file asynchronously.
* If the file does not exist, it will be created as empty.
*
* @param {string} path The file path.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/truncate.md
*/
fsMate.truncate = async function(path) {
if (fs$prom.truncate) {
await fs$prom.truncate(path, 0);
} else {
const fd = await fs$prom.open(path, 'w');
await fd.close();
}
};
/**
* Empty files or directories.
*
* @param {string} paths Path to file or files iterator
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/empty.md
*/
fsMate.empty = async function(paths) {
let path;
paths = iterable(paths);
for(path of paths) {
if (await this.isDir(path)) {
await this.remove(await this.scandir(path, {withFullPath: true}));
} else if (this.isFile(path)) {
await this.truncate(path);
} else {
throw new Error(sprintf('Unable to guess [%s] file type.', file.name));
}
}
};
/**
* Empty files or directories.
*
* @param {string} paths Path to file or files iterator
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/emptySync.md
*/
fsMate.emptySync = function(paths) {
let path;
paths = iterable(paths);
for(path of paths) {
if (this.isDirSync(path)) {
this.removeSync(this.scandirSync(path, {withFullPath: true}));
} else if (this.isFileSync(path)) {
this.truncateSync(path);
} else {
throw new Error(sprintf('Unable to guess [%s] file type.', file.name));
}
}
};
/**
* Creates a readable input stream from various data types.
*
* - Converts objects to pretty-printed JSON strings.
* - Accepts Buffer, string, number, bigint, or null/undefined.
* - Converts strings and other primitives to Buffer for efficient streaming.
*
* @param {*} data The input data to convert into a readable stream.
* @returns A readable stream representing the input data.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/createInputStream.md
*/
fsMate.createInputStream = function(data) {
let inputStream, bufferString;
// If the data is Uint8Array, then decode it
if (data instanceof Uint8Array) {
data = new TextDecoder().decode(data);
}
// If the data is an object, then convert it into a JSON string.
if (typeof data === 'object' && !Buffer.isBuffer(data)) {
try {
data = JSON.stringify(data, null, 2);
} catch(err) {
throw err;
}
}
// Case 1: Buffer (binary or raw data)
if (Buffer.isBuffer(data)) {
inputStream = Readable.from([data]);
}
// Case 2: String (text, bigint or binary string)
else if (typeof data === 'string' || typeof data === 'number' || typeof data === 'bigint' || data == null) {
// If it's a binary string, convert to buffer first for better performance
bufferString = Buffer.from(String(data == null ? '' : data), 'utf-8');
inputStream = Readable.from([bufferString]);
}
// Unsupported type
else {
throw new Error('Unsupported input type. Expected Buffer or string.');
}
return inputStream;
};
/**
* Converts various data types to a string representation.
*
* - Returns an empty string for null or undefined.
* - Converts Buffer data to UTF-8 string.
* - Serializes objects to pretty-printed JSON.
* - Converts strings, numbers, and bigints to string.
* - Throws an error for unsupported types.
*
* @param {*} data The input data to stringify.
* @returns {string} The string representation of the input data.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/stringify.md
*/
fsMate.stringify = function(data) {
// Return an empty string if data is null or undefined
if (data == null) {
return '';
}
// Case 1: Buffer (binary or raw data)
if (Buffer.isBuffer(data)) {
data = data.toString('utf-8');
}
// If the data is Uint8Array, then decode it
else if (data instanceof Uint8Array) {
data = new TextDecoder().decode(data);
}
// Case 2: If the data is an object, then convert it into a JSON string.
else if (typeof data === 'object') {
try {
data = JSON.stringify(data, null, 2);
} catch(err) {
throw err;
}
}
// Case 3: String (text, bigint or binary string)
else if (typeof data === 'string' || typeof data === 'number' || typeof data === 'bigint') {
data = data.toString();
}
// Unsupported type
else {
throw new Error('Unsupported data type.');
}
return data;
}
/**
* Reads a file asynchronously and optionally parses its content as JSON.
* Supports options for the read stream, including chunk size.
*
* @param {string} filePath Path to the file to read.
* @param {Object} options Read stream options or parsed flag.
* @param {boolean} parsed If true, parse the file content as JSON.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/readFile.md
*/
fsMate.readFile = function(filePath, options, parsed) {
return new Promise(function(resolve, reject) {
let readStream, data = '';
if (typeof options === 'boolean') {
parsed = parsed || options;
options = undefined;
}
options = options || {};
// Set default 1MB chunks for speed & safety
options.highWaterMark = options.highWaterMark || highWaterMark;
readStream = $fs('createReadStream', [filePath, options]);
readStream.on('error', reject);
// Handle data in chunks
readStream.on('data', function(chunk) {
data += chunk;
});
readStream.on('end', function() {
if (parsed) {
try {data = JSON.parse(data)} catch(e) {}
}
resolve(data);
});
});
};
/**
* Reads lines from a file asynchronously, returning a slice of lines.
*
* @param {string} filePath Path to the file to read.
* @param {boolean|number} start Starting line index (inclusive) or true for start from 0.
* @param {boolean|number} end Ending line index (exclusive) or true for all lines.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/readLine.md
*/
fsMate.readLine = function(filePath, start, end) {
return new Promise(function(resolve, reject) {
fsMate.readFile(filePath).then(function(data) {
const lines = data.split(CRLF);
let isNullStart = false;
// If start is null then sets 0
if (start == null) {
isNullStart = true;
start = 0;
}
// Sets max index of lines
if (end === true) {
end = lines.length;
}
// Retrive only self or single line
if (end == null && !isNullStart) {
end = start + 1;
}
start >= end || (typeof start !== 'number')
? reject(sprintf('Invalid line range [%s-%s]', start, end))
: resolve(lines.slice(start, end));
}).catch(reject);
});
};
/**
* Writes data to a file asynchronously using streams.
*
* If data is not a readable stream, it will be converted to one.
* Supports stream options like chunk size via `options`.
*
* @param {string} filePath Path to the file to write.
* @param {*} data Data to write; can be a readable stream or other data types.
* @param {Object} options Stream options (e.g., highWaterMark).
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/writeFile.md
*/
fsMate.writeFile = function(filePath, data, options) {
return new Promise(function(resolve, reject) {
options = options || {};
// Set default 1MB chunks for speed & safety
options.highWaterMark = options.highWaterMark || highWaterMark;
writeStream = $fs('createWriteStream', [filePath, options]);
inputStream = data instanceof Readable ? data : fsMate.createInputStream(data);
inputStream.pipe(writeStream);
writeStream.on('finish', resolve).on('error', reject);
});
}
/**
* Append data to a file asynchronously.
*
* This method wraps `writeFile` with the append (`'a'`) flag,
* allowing new data to be added at the end of the file seamlessly.
*
* @param {string} filePath The path of the target file.
* @param {*} data The content to append.
* @param {Object} options Optional stream options.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/appendFile.md
*/
fsMate.appendFile = function(filePath, data, options) {
options = options || {};
options.flags = 'a';
return this.writeFile(filePath, data, options);
};
/**
* Prepends data to a file asynchronously.
*
* Reads the existing file content, then writes the new data
* followed by the original content, effectively adding at the start.
*
* @param {string} filePath Path of the target file.
* @param {*} data options Data to prepend.
* @param {Object} options Optional read/write options.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/prependFile.md
*/
fsMate.prependFile = async function(filePath, data, options) {
const prevData = await this.readFile(filePath, options);
return this.writeFile(filePath, this.multiStream([
this.createInputStream(data),
this.createInputStream(prevData)
]), options);
};
/**
* Atomically dumps safely writes content into a file.
*
* Creates the directory if needed, writes to a temp file,
* then atomically replaces the original file.
*
* @param {string} filePath Target file path.
* @param {*} content The content to write into the file.
* @returns {Promise<any>} Resolves or rejects via callback.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/dumpFile.md
*/
fsMate.dumpFile = async function(filePath, content) {
const dir = path.dirname(filePath);
if (!(await fs$async('isDir', dir))) {
await this.mkdir(dir);
}
const tempFile = this.tempNam(dir, path.basename(filePath));
try {
// Write to temp file
await this.writeFile(tempFile, content);
// Replace original file with temp file (atomic on most OSes)
await fs$prom.rename(tempFile, filePath);
} catch(err) {
// Cleanup temp file if error
if (await this.exists(tempFile)) {
await fs$prom.unlink(tempFile);
}
throw err;
}
};
/**
* Combines multiple readable streams into a single sequential stream.
*
* Streams are read one after another, preserving their order.
* Useful for merging content (e.g. prepend + original) without buffering all at once.
*
* @param {Iterable<Readable>} streams An array or iterable of readable streams.
* @returns {Readable} A single combined readable stream.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/multiStream.md
*/
fsMate.multiStream = function(streams) {
return Readable.from((async function*() {
for(const stream of streams) {
for await(const chunk of stream) {
yield chunk;
}
}
})());
};
/**
* Reads a file asynchronously and optionally parses its content as JSON.
* Supports options for the read stream, including chunk size.
*
* @param {string} filePath Path to the file to read.
* @param {Object} options Read stream options or parsed flag.
* @param {boolean} parsed If true, parse the file content as JSON.
* @returns {string|any} The file content as a string or parsed object.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/readFileSync.md
*/
fsMate.readFileSync = function(filePath, parsed) {
let fd, buffer,
position = 0,
data = '',
read = function() {
const bytes = fs.readSync(fd, buffer, 0, buffer.length, position);
if (bytes > 0) {
data += buffer.toString('utf-8', 0, bytes);
position += bytes;
read();
} else {
fs.closeSync(fd);
}
};
fd = fs.openSync(filePath, 'r');
buffer = Buffer.alloc(highWaterMark);
read();
if (parsed) {
try {data = JSON.parse(data)} catch(e) {}
}
return data;
};
/**
* Reads lines from a file asynchronously, returning a slice of lines.
*
* @param {string} filePath Path to the file to read.
* @param {boolean|number} start Starting line index (inclusive) or true for start from 0.
* @param {boolean|number} end Ending line index (exclusive) or true for all lines.
* @returns {Array} Returns a collection of lines
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/readLineSync.md
*/
fsMate.readLineSync = function(filePath, start, end) {
const data = this.readFileSync(filePath);
const lines = data.split(CRLF);
let isNullStart = false;
// If start is null then sets 0
if (start == null) {
isNullStart = true;
start = 0;
}
// Sets max index of lines
if (end === true) {
end = lines.length;
}
// Retrive only self or single line
if (end == null && !isNullStart) {
end = start + 1;
}
if (start >= end || (typeof start !== 'number')) {
throw new Error(
sprintf('Invalid line range [%s-%s]', start, end)
);
}
return lines.slice(start, end);
};
/**
* Writes data to a file asynchronously using streams.
*
* If data is not a readable stream, it will be converted to one.
* Supports stream options like chunk size via `options`.
*
* @param {string} filePath Path to the file to write.
* @param {*} data Data to write; can be a readable stream or other data types.
* @param {Object} options Stream options (e.g., highWaterMark).
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/writeFileSync.md
*/
fsMate.writeFileSync = function(filePath, data) {
const fd = fs.openSync(filePath, 'w');
fs.writeSync(fd, this.stringify(data));
fs.closeSync(fd);
};
/**
* Append data to a file synchronously.
*
* This method wraps `writeFile` with the append (`'a'`) flag,
* allowing new data to be added at the end of the file seamlessly.
*
* @param {string} filePath The path of the target file.
* @param {*} data The content to append.
* @param {Object} options Optional stream options.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/appendFileSync.md
*/
fsMate.appendFileSync = function(filePath, data) {
const fd = fs.openSync(filePath, 'a');
fs.writeSync(fd, this.stringify(data));
fs.closeSync(fd);
};
/**
* Prepends data to a file synchronously.
*
* Reads the existing file content, then writes the new data
* followed by the original content, effectively adding at the start.
*
* @param {string} filePath Path of the target file.
* @param {*} data options Data to prepend.
* @param {Object} options Optional read/write options.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/prependFileSync.md
*/
fsMate.prependFileSync = function(filePath, data) {
const prevData = this.readFileSync(filePath);
return this.dumpFileSync(filePath, this.stringify(data) + prevData);
};
/**
* Atomically dumps safely writes content into a file.
*
* Creates the directory if needed, writes to a temp file,
* then atomically replaces the original file.
*
* @param {string} filePath Target file path.
* @param {*} content The content to write into the file.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/dumpFileSync.md
*/
fsMate.dumpFileSync = function(filePath, content) {
const dir = path.dirname(filePath);
if (!fs$sync('isDir', dir)) {
this.mkdirSync(dir);
}
const tempFile = this.tempNam(dir, path.basename(filePath));
try {
// Write to temp file
this.writeFileSync(tempFile, content);
// Replace original file with temp file (atomic on most OSes)
this.renameSync(tempFile, filePath, true);
} catch(err) {
// Cleanup temp file if error
if (this.isFileSync(filePath)) {
fs.unlinkSync(filePath);
}
throw err;
}
};
/**
* Seeks on a file pointer
*
* @param {string} filePath File path
* @param {number} position The offset
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/fseekSync.md
*/
fsMate.fseekSync = function(filePath, position) {
const pos = parseInt(position);
if (isNaN(pos) || pos < 0) {
throw new Error(sprintf(
'Failed to set position. You are trying to set an invalid position [%s].',
position
));
}
// Throw an error if file doesn't exists
if (!fs.existsSync(filePath)) {
throw new Error(sprintf('no such file or directory, fseek [%s]', filePath));
}
stream[path.resolve(filePath)] = position;
};
/**
* Returns the current position of the file read/write pointer
*
* @param {string} filePath File path
* @returns {number} Current position of file pointer
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/ftellSync.md
*/
fsMate.ftellSync = function(filePath) {
return stream[path.resolve(filePath)] || 0;
};
/**
* Gets line from file pointer
*
* @param {string} filePath File path
* @returns {string} Returns the first line
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/fgetsSync.md
*/
fsMate.fgetsSync = function(filePath) {
let content = this.readFileSync(filePath);
let pos = this.ftellSync(filePath);
// Gets first line
let line = content.slice(pos).replace(/^\r?\n/, '').split(CRLF).shift();
/**
* previous position +
* current line length +
* CRLF length (2)
*/
this.fseekSync(filePath, pos + line.length + 2);
return line;
};
/**
* Output all remaining data on a file pointer
*
* @param {string} filePath File path
* @returns {string} Remaining data content.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/fpassthruSync.md
*/
fsMate.fpassthruSync = function(filePath) {
let content = this.readFileSync(filePath);
let pos = this.ftellSync(filePath);
content = content.slice(pos);
this.fseekSync(filePath, content.length);
return content;
};
/**
* Rewind the position of a file pointer
*
* @param {string} filePath File path
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/rewindSync.md
*/
fsMate.rewindSync = function(filePath) {
this.fseekSync(filePath, 0);
};
/**
* Binary-safe file write
*
* @param {string} filePath File path
* @param {string} content The string that is to be written.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/fwriteSync.md
*/
fsMate.fwriteSync = function(filePath, content) {
let data = this.readFileSync(filePath);
let pos = this.ftellSync(filePath);
data = data.slice(0, pos) + content;
this.writeFileSync(filePath, data);
this.fseekSync(filePath, data.length);
};
/**
* Binary-safe file read
*
* @param {string} filePath File path
* @param {number} length Up to length number of bytes read.
* @returns {string} File content.
*
* @see https://github.com/jsvibe/fsmate/blob/HEAD/doc/freadSync.md
*/
fsMate.freadSync = function(filePath, length) {
if (typeof length !== 'number') {
throw new Error('Cannot read file. Length value must be a number.');
}
if (length <= 0) {
throw new Error('Cannot read file. Length value must be greater than 0.');
}
const content = this.fpassthruSync(filePath).slice(0, length);
this.fseekSync(filePath, content.length);
return content;
};
/**
* Seeks on a file pointer
*
* @param {string} filePath File path
* @param {number} position The offset
* @returns {Promise<any>} Resolves or rejects via callback.
*
*