@secjs/utils
Version:
Utils functions and classes for Node.js
793 lines (658 loc) • 17.5 kB
JavaScript
/**
* @secjs/utils
*
* (c) João Lenon <lenonSec7@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import minimatch from 'minimatch'
import {
existsSync,
mkdirSync,
promises,
readdirSync,
rmSync,
statSync,
} from 'node:fs'
import { randomBytes } from 'node:crypto'
import { isAbsolute, join, parse, resolve, sep } from 'node:path'
import { Json } from '#src/Helpers/Json'
import { File } from '#src/Helpers/File'
import { Parser } from '#src/Helpers/Parser'
import { Options } from '#src/Helpers/Options'
import { NotFoundFolderException } from '#src/Exceptions/NotFoundFolderException'
import { Path } from '#src/Helpers/Path'
export class Folder {
/**
* Creates a new instance of Folder.
*
* @param {string} folderPath
* @param {boolean} [mockedValues]
* @param {boolean} [isCopy]
* @return {Folder}
*/
constructor(folderPath, mockedValues = false, isCopy = false) {
const { dir, name, path } = Folder.#parsePath(folderPath)
/** @type {File[]} */
this.files = []
/** @type {Folder[]} */
this.folders = []
/** @type {string} */
this.originalDir = dir
/** @type {string} */
this.originalName = name
/** @type {string} */
this.originalPath = path
/** @type {boolean} */
this.isCopy = isCopy
/** @type {boolean} */
this.originalFolderExists =
Folder.existsSync(this.originalPath) && !this.isCopy
/** @type {boolean} */
this.folderExists = this.originalFolderExists
this.#createFolderValues(mockedValues)
}
/**
* Get the size of the folder.
*
* @param {string} folderPath
* @return {number}
*/
static folderSizeSync(folderPath) {
const files = readdirSync(folderPath)
const stats = files.map(file => statSync(join(folderPath, file)))
return stats.reduce((accumulator, { size }) => accumulator + size, 0)
}
/**
* Get the size of the folder.
*
* @param {string} folderPath
* @return {Promise<number>}
*/
static async folderSize(folderPath) {
const files = await promises.readdir(folderPath)
const stats = files.map(file => promises.stat(join(folderPath, file)))
return (await Promise.all(stats)).reduce(
(accumulator, { size }) => accumulator + size,
0,
)
}
/**
* Remove the folder it's existing or not.
*
* @param {string} folderPath
* @return {Promise<void>}
*/
static async safeRemove(folderPath) {
const { path } = Folder.#parsePath(folderPath)
if (!(await Folder.exists(path))) {
return
}
await promises.rm(path, { recursive: true })
}
/**
* Verify if folder exists.
*
* @param {string} folderPath
* @return {boolean}
*/
static existsSync(folderPath) {
const { path } = Folder.#parsePath(folderPath)
return existsSync(path)
}
/**
* Verify if folder exists.
*
* @param {string} folderPath
* @return {Promise<boolean>}
*/
static async exists(folderPath) {
const { path } = Folder.#parsePath(folderPath)
return promises
.access(path)
.then(() => true)
.catch(() => false)
}
/**
* Verify if path is from folder or file.
*
* @param {string} path
* @return {boolean}
*/
static isFolderSync(path) {
const { path: parsedPath } = Folder.#parsePath(path)
return statSync(parsedPath).isDirectory()
}
/**
* Verify if path is from folder or file.
*
* @param {string} path
* @return {Promise<boolean>}
*/
static async isFolder(path) {
const { path: parsedPath } = Folder.#parsePath(path)
return promises.stat(parsedPath).then(stat => stat.isDirectory())
}
/**
* Get sub files of folder.
*
* @param {Folder[]} folders
* @param {string} [pattern]
* @return {File[]}
*/
static #getSubFiles(folders, pattern) {
const files = []
folders.forEach(folder => {
folder.files.forEach(file => {
if (!pattern) {
files.push(file)
return
}
if (minimatch(file.path, pattern)) {
files.push(file)
}
})
if (folder.folders.length) {
files.push(...this.#getSubFiles(folder.folders, pattern))
}
})
return files
}
/**
* Get sub folders of folder.
*
* @param {Folder} folder
* @param {boolean} recursive
* @param {string} [pattern]
* @return {Folder[]}
*/
static #getSubFolders(folder, recursive, pattern) {
const subFolders = []
folder.folders.forEach(f => {
if (!pattern) {
subFolders.push(f)
}
if (recursive && f.folders.length) {
subFolders.push(...this.#getSubFolders(f, recursive, pattern))
}
if (pattern && minimatch(f.path, pattern)) {
subFolders.push(f)
}
})
return subFolders
}
/**
* Parse the folder path.
*
* @private
* @param {string} folderPath
* @return {{path: string, name: string, dir: string}}
*/
static #parsePath(folderPath) {
if (!isAbsolute(folderPath)) {
folderPath = Path.this(folderPath, 3)
}
const { dir, name, ext } = parse(folderPath)
let path = dir.concat(sep, name)
if (ext) {
path = path.concat(ext)
}
return { dir, name, path }
}
/**
* Returns the file as a JSON object.
*
* @return {{
* dir: string,
* name: string,
* base: string,
* path: string,
* files: File[],
* folders: Folder[],
* createdAt: Date,
* accessedAt: Date,
* modifiedAt: Date,
* folderSize: number,
* isCopy: boolean,
* originalDir: string,
* originalName: string,
* originalPath: string,
* originalFolderExists: boolean
* }}
*/
toJSON() {
return Json.copy({
dir: this.dir,
name: this.name,
path: this.path,
files: this.files.map(file => file.toJSON()),
folders: this.folders.map(folder => folder.toJSON()),
createdAt: this.createdAt,
accessedAt: this.accessedAt,
modifiedAt: this.modifiedAt,
folderSize: this.folderSize,
originalDir: this.originalDir,
originalName: this.originalName,
originalPath: this.originalPath,
folderExists: this.folderExists,
isCopy: this.isCopy,
originalFolderExists: this.originalFolderExists,
})
}
/**
* Load or create the folder.
*
* @param {{
* withSub?: boolean,
* withFileContent?: boolean,
* isInternalLoad?: boolean,
* }} [options]
* @return {Folder}
*/
loadSync(options) {
options = Options.create(options, {
withSub: true,
withFileContent: false,
isInternalLoad: false,
})
if (!this.folderExists) {
mkdirSync(this.path, { recursive: true })
this.folderExists = true
}
if (this.folderSize && options.isInternalLoad) {
return this
}
const folderStat = statSync(this.path)
this.createdAt = folderStat.birthtime
this.accessedAt = folderStat.atime
this.modifiedAt = folderStat.mtime
this.folderSize = Parser.sizeToByte(Folder.folderSizeSync(this.path))
if (!options.withSub) {
return this
}
this.#loadSubSync(
this.path,
readdirSync(this.path, { withFileTypes: true }),
options.withFileContent,
)
return this
}
/**
* Load or create the folder.
*
* @param {{
* withSub?: boolean,
* withFileContent?: boolean,
* isInternalLoad?: boolean,
* }} [options]
* @return {Promise<Folder>}
*/
async load(options) {
options = Options.create(options, {
withSub: true,
withFileContent: false,
isInternalLoad: false,
})
if (!this.folderExists) {
await promises.mkdir(this.path, { recursive: true })
this.folderExists = true
}
if (this.folderSize && options.isInternalLoad) {
return this
}
const folderStat = await promises.stat(this.path)
this.createdAt = folderStat.birthtime
this.accessedAt = folderStat.atime
this.modifiedAt = folderStat.mtime
this.folderSize = Parser.sizeToByte(await Folder.folderSize(this.path))
if (!options.withSub) {
return this
}
await this.#loadSub(
this.path,
await promises.readdir(this.path, { withFileTypes: true }),
options.withFileContent,
)
return this
}
/**
* Remove the folder.
*
* @return {void}
*/
removeSync() {
if (!this.folderExists) {
throw new NotFoundFolderException(this.name)
}
this.createdAt = undefined
this.accessedAt = undefined
this.modifiedAt = undefined
this.folderSize = undefined
this.folderExists = false
this.originalFolderExists = false
this.files = []
this.folders = []
rmSync(this.path, { recursive: true })
}
/**
* Remove the folder.
*
* @return {Promise<void>}
*/
async remove() {
if (!this.folderExists) {
throw new NotFoundFolderException(this.name)
}
this.createdAt = undefined
this.accessedAt = undefined
this.modifiedAt = undefined
this.folderSize = undefined
this.folderExists = false
this.originalFolderExists = false
this.files = []
this.folders = []
await promises.rm(this.path, { recursive: true })
}
/**
* Create a copy of the folder.
*
* @param {string} path
* @param {{
* withSub?: boolean,
* withFileContent?: boolean,
* mockedValues?: boolean
* }} [options]
* @return {Folder}
*/
copySync(path, options) {
path = Folder.#parsePath(path).path
options = Options.create(options, {
withSub: true,
withFileContent: false,
mockedValues: false,
})
this.loadSync({
withSub: options.withSub,
withContent: options.withFileContent,
isInternalLoad: true,
})
const folder = new Folder(path, options.mockedValues, true).loadSync(
options,
)
folder.files = this.files.map(f => {
return f.copySync(`${folder.path}/${f.base}`, {
mockedValues: options.mockedValues,
withContent: options.withFileContent,
})
})
folder.folders = this.folders.map(f => {
return f.copySync(`${folder.path}/${f.base}`, {
withSub: options.withSub,
mockedValues: options.mockedValues,
withContent: options.withFileContent,
})
})
return folder
}
/**
* Create a copy of the folder.
*
* @param {string} path
* @param {{
* withSub?: boolean,
* withFileContent?: boolean,
* mockedValues?: boolean
* }} [options]
* @return {Promise<Folder>}
*/
async copy(path, options) {
path = Folder.#parsePath(path).path
options = Options.create(options, {
withSub: true,
withFileContent: false,
mockedValues: false,
})
await this.load({
withSub: options.withSub,
withContent: options.withFileContent,
isInternalLoad: true,
})
const folder = await new Folder(path, options.mockedValues, true).load(
options,
)
folder.files = await Promise.all(
this.files.map(f => {
return f.copy(`${folder.path}/${f.base}`, {
mockedValues: options.mockedValues,
withContent: options.withFileContent,
})
}),
)
folder.folders = await Promise.all(
this.folders.map(f => {
return f.copy(`${folder.path}/${f.name}`, {
withSub: options.withSub,
mockedValues: options.mockedValues,
withContent: options.withFileContent,
})
}),
)
return folder
}
/**
* Move the folder to other path.
*
* @param {string} path
* @param {{
* withSub?: boolean,
* withFileContent?: boolean,
* mockedValues?: boolean
* }} [options]
* @return {Folder}
*/
moveSync(path, options) {
path = Folder.#parsePath(path).path
options = Options.create(options, {
withSub: true,
withFileContent: false,
mockedValues: false,
})
this.loadSync({
withSub: options.withSub,
withContent: options.withFileContent,
isInternalLoad: true,
})
const folder = new Folder(path, options.mockedValues, true).loadSync(
options,
)
folder.files = this.files.map(f => {
return f.moveSync(`${folder.path}/${f.base}`, {
mockedValues: options.mockedValues,
withContent: options.withFileContent,
})
})
folder.folders = this.folders.map(f => {
return f.moveSync(`${folder.path}/${f.name}`, {
withSub: options.withSub,
mockedValues: options.mockedValues,
withContent: options.withFileContent,
})
})
this.removeSync()
return folder
}
/**
* Move the folder to other path.
*
* @param {string} path
* @param {{
* withSub?: boolean,
* withFileContent?: boolean,
* mockedValues?: boolean
* }} [options]
* @return {Promise<Folder>}
*/
async move(path, options) {
path = Folder.#parsePath(path).path
options = Options.create(options, {
withSub: true,
withFileContent: false,
mockedValues: false,
})
await this.load({
withSub: options.withSub,
withContent: options.withFileContent,
isInternalLoad: true,
})
const folder = await new Folder(path, options.mockedValues, true).load(
options,
)
folder.files = await Promise.all(
this.files.map(f => {
return f.move(`${folder.path}/${f.base}`, {
mockedValues: options.mockedValues,
withContent: options.withFileContent,
})
}),
)
folder.folders = await Promise.all(
this.folders.map(f => {
return f.move(`${folder.path}/${f.name}`, {
withSub: options.withSub,
mockedValues: options.mockedValues,
withContent: options.withFileContent,
})
}),
)
await this.remove()
return folder
}
/**
* Get all the files of folder using glob pattern.
*
* @param {string} [pattern]
* @param {boolean} [recursive]
* @return {File[]}
*/
getFilesByPattern(pattern, recursive = false) {
this.loadSync({ withSub: true, isInternalLoad: true })
if (pattern) {
pattern = `${this.path.replace(/\\/g, '/')}/${pattern}`
}
const files = []
this.files.forEach(file => {
if (pattern && minimatch(file.path, pattern)) {
files.push(file)
return
}
files.push(file)
})
if (recursive) {
files.push(...Folder.#getSubFiles(this.folders, pattern))
}
return files
}
/**
* Get all the folders of folder using glob pattern.
*
* @param {string} [pattern]
* @param {boolean} [recursive]
* @return {Folder[]}
*/
getFoldersByPattern(pattern, recursive = false) {
this.loadSync({ withSub: true, isInternalLoad: true })
if (pattern) {
pattern = `${this.path.replace(/\\/g, '/')}/${pattern}`
}
const folders = []
this.folders.forEach(folder => {
if (recursive && folder.folders.length) {
folders.push(...Folder.#getSubFolders(folder, recursive, pattern))
}
if (pattern && minimatch(folder.path, pattern)) {
folders.push(folder)
return
}
folders.push(folder)
})
return folders
}
/**
* Create folder values.
*
* @param {boolean?} mockedValues
* @return {void}
*/
#createFolderValues(mockedValues) {
if (mockedValues && !this.originalFolderExists) {
const bytes = randomBytes(8)
const buffer = Buffer.from(bytes)
this.dir = this.originalDir
this.name = buffer.toString('base64').replace(/[^a-zA-Z0-9]/g, '')
this.path = this.dir + '/' + this.name
return
}
this.dir = this.originalDir
this.name = this.originalName
this.path = this.originalPath
}
/**
* Load sub files/folder of folder.
*
* @param {string} path
* @param {Dirent[]} dirents
* @param {boolean} withFileContent
* @return {void}
*/
#loadSubSync(path, dirents, withFileContent) {
dirents.forEach(dirent => {
const name = resolve(path, dirent.name)
if (dirent.isDirectory()) {
const folder = new Folder(name).loadSync({
withSub: true,
withFileContent,
isInternalLoad: true,
})
this.folders.push(folder)
return
}
const file = new File(name).loadSync({
withContent: withFileContent,
isInternalLoad: true,
})
this.files.push(file)
})
}
/**
* Load sub files/folder of folder.
*
* @param {string} path
* @param {Dirent[]} dirents
* @param {boolean} withFileContent
* @return {Promise<void>}
*/
async #loadSub(path, dirents, withFileContent) {
const files = []
const folders = []
dirents.forEach(dirent => {
const name = resolve(path, dirent.name)
if (dirent.isDirectory()) {
const folder = new Folder(name).load({
withSub: true,
withFileContent,
isInternalLoad: true,
})
folders.push(folder)
return
}
const file = new File(name).load({
withContent: withFileContent,
isInternalLoad: true,
})
files.push(file)
})
this.files = await Promise.all(files)
this.folders = await Promise.all(folders)
}
}