import { CopyOptions, MoveOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, stat, symlink } from "fs-extra"; import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises"; import path from "path"; import { Observable, concatMap, from } from "rxjs"; import log from "electron-log"; import { BsmException } from "shared/models/bsm-exception.model"; import crypto from "crypto"; import { execSync } from "child_process"; import { tryit } from "../../shared/helpers/error.helpers"; import { CustomError } from "shared/models/exceptions/custom-error.class"; export async function pathExist(path: string): Promise { try { await access(path); return true; } catch (e) { return false; } } export async function ensureFolderExist(path: string): Promise { if (await pathExist(path)) { return Promise.resolve(); } return mkdir(path, { recursive: true }) .catch(log.error) .then(() => {}); } export async function deleteFolder(folderPath: string): Promise { if (!(await pathExist(folderPath))) { return; } return rm(folderPath, { recursive: true, force: true }); } export async function unlinkPath(path: string): Promise { if (!(await pathExist(path))) { return; } return unlink(path); } export async function getFoldersInFolder(folderPath: string, opts?: { ignoreSymlinkTargetError?: boolean }): Promise { if (!(await pathExist(folderPath))) { return []; } const files = await readdir(folderPath, { withFileTypes: true }); const promises = files.map(async file => { if (file.isDirectory()) { return path.join(folderPath, file.name); } if (!file.isSymbolicLink()) { return undefined; } try { const targetPath = await readlink(path.join(folderPath, file.name)); return (await lstat(targetPath)).isDirectory() ? path.join(folderPath, file.name) : undefined; } catch (e: any) { if (e.code === "ENOENT" && opts?.ignoreSymlinkTargetError === true && !path.extname(file.name)) { return path.join(folderPath, file.name); } return undefined; } }); return (await Promise.all(promises)).filter(folder => folder); } export async function getFilesInFolder(folderPath: string): Promise { if (!(await pathExist(folderPath))) { return []; } const dirEntries = await readdir(folderPath, { withFileTypes: true }); return dirEntries.filter(entry => entry.isFile()).map(file => path.join(folderPath, file.name)); } export function moveFolderContent(src: string, dest: string, option?: MoveOptions): Observable { const progress: Progression = { current: 0, total: 0 }; return new Observable(subscriber => { subscriber.next(progress); (async () => { const srcExist = await pathExists(src); if (!srcExist) { return subscriber.complete(); } await ensureFolderExist(dest); const files = await readdir(src, { encoding: "utf-8", withFileTypes: true }); progress.total = files.length; for(const file of files){ const srcFullPath = path.join(src, file.name); const destFullPath = path.join(dest, file.name); const srcChilds = file.isDirectory() ? await readdir(srcFullPath, { encoding: "utf-8", recursive: true }) : []; const allChildsAlreadyExist = srcChilds.every(child => pathExistsSync(path.join(destFullPath, child))); if(file.isFile() || !allChildsAlreadyExist){ await move(srcFullPath, destFullPath, option); } progress.current++; subscriber.next(progress); } })().catch(err => subscriber.error(CustomError.fromError(err, err?.code))).finally(() => subscriber.complete()); }); } export function isSubdirectory(parent: string, child: string): boolean { const parentNormalized = path.resolve(parent); const childNormalized = path.resolve(child); if (parentNormalized === childNormalized) { return false; } const relativePath = path.relative(parentNormalized, childNormalized); if (path.parse(parentNormalized).root !== path.parse(childNormalized).root) { return false; } return relativePath && !relativePath.startsWith("..") && !path.isAbsolute(relativePath); } export async function copyDirectoryWithJunctions(src: string, dest: string, options?: CopyOptions): Promise { if (isSubdirectory(src, dest)) { throw { message: `Cannot copy directory '${src}' into itself '${dest}'.`, code: "COPY_TO_SUBPATH" } as BsmException; } await ensureDir(dest); const items = await readdir(src, { withFileTypes: true }); for (const item of items) { const sourcePath = path.join(src, item.name); const destinationPath = path.join(dest, item.name); if (item.isDirectory()) { await copyDirectoryWithJunctions(sourcePath, destinationPath, options); } else if (item.isFile()) { await copy(sourcePath, destinationPath, options); } else if (item.isSymbolicLink()) { if (options?.overwrite) { await unlinkPath(destinationPath); } const symlinkTarget = await readlink(sourcePath); const relativePath = path.relative(src, symlinkTarget); const newTarget = path.join(dest, relativePath); await symlink(newTarget, destinationPath, "junction"); // Only junction to avoid right issues while copying content of BSManager folder } } } export function hashFile(filePath: string, algorithm = "sha256"): Promise { return new Promise((resolve, reject) => { const shasum = crypto.createHash(algorithm); const stream = createReadStream(filePath); stream.on("data", data => shasum.update(data)); stream.on("error", reject); stream.on("close", () => resolve(shasum.digest("hex"))); }); } export async function dirSize(dirPath: string): Promise{ const entries = await readdir(dirPath); const paths = entries.map(async entry => { const fullPath = path.join(dirPath, entry); const realPath = await realpath(fullPath); const stat = await lstat(realPath); if (stat.isDirectory()) { return dirSize(fullPath); } if (stat.isFile()) { return stat.size; } return 0; }); return (await Promise.all(paths)).flat(Infinity).reduce((acc, size ) => acc + size, 0); } export function rxCopy(src: string, dest: string, option?: CopyOptions): Observable { const dirSizePromise = dirSize(src).catch(err => { log.error("dirSizePromise", err); return 0; }); return from(dirSizePromise).pipe( concatMap(totalSize => { const progress: Progression = { current: 0, total: totalSize }; return new Observable(sub => { sub.next(progress); copy(src, dest, {...option, filter: (src) => { stat(src).then(stats => { progress.current += stats.size; sub.next(progress); }); return true; }}) .then(() => sub.complete()).catch(err => sub.error(err)) }) }) ); } export async function ensurePathNotAlreadyExist(path: string): Promise { let destPath = path; let folderExist = await pathExists(destPath); let i = 0; while (folderExist) { i++; destPath = `${path} (${i})`; folderExist = await pathExists(destPath); } return destPath; } export function ensurePathNotAlreadyExistSync(path: string): string { let destPath = path; let folderExist = pathExistsSync(destPath); let i = 0; while (folderExist) { i++; destPath = `${path} (${i})`; folderExist = pathExistsSync(destPath); } return destPath; } export async function isJunction(path: string): Promise{ const [stats, lstats] = await Promise.all([stat(path), lstat(path)]); return lstats.isSymbolicLink() && stats.isDirectory(); } export function resolveGUIDPath(guidPath: string): string { const guidVolume = path.parse(guidPath).root; const command = `powershell -command "(Get-WmiObject -Class Win32_Volume | Where-Object { $_.DeviceID -like '${guidVolume}' }).DriveLetter"`; const {result: driveLetter, error} = tryit(() => execSync(command).toString().trim()); if (!driveLetter || error) { throw new Error("Unable to resolve GUID path", error); } return path.join(driveLetter, path.relative(guidVolume, guidPath)); } export function getUniqueFileNamePath(filePath: string): string { const { dir, name, ext } = path.parse(filePath); let i = 0; let newFileName = `${name}${ext}`; while (pathExistsSync(path.join(dir, newFileName))) { i++; newFileName = `${name} (${i})${ext}`; } return path.join(dir, newFileName); } export interface Progression { total: number; current: number; diff?: number; data?: T; extra?: D; }