diff --git a/src/__tests__/unit/fs.helpers.test.ts b/src/__tests__/unit/fs.helpers.test.ts index 906e353a..adcb33f3 100644 --- a/src/__tests__/unit/fs.helpers.test.ts +++ b/src/__tests__/unit/fs.helpers.test.ts @@ -2,6 +2,15 @@ import { mkdir, pathExistsSync, rm, writeFile } from "fs-extra"; import { getSize } from "main/helpers/fs.helpers"; import path from "path"; +jest.mock("electron", () => ({ app: { + getPath: () => "", + getName: () => "", +}})); +jest.mock("electron-log", () => ({ + info: jest.fn(), + error: jest.fn(), +})); + const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "fs"); describe("Test fs.helpers getSize", () => { diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index 5692d32f..58f8f233 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -1,5 +1,5 @@ -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 { CopyOptions, MoveOptions, RmOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, rm, stat, symlink, unlink, unlinkSync } from "fs-extra"; +import { access, mkdir, readdir, lstat, readlink } from "fs/promises"; import path from "path"; import { Observable, concatMap, from } from "rxjs"; import log from "electron-log"; @@ -28,18 +28,53 @@ export async function ensureFolderExist(path: string): Promise { .then(() => {}); } -export async function deleteFolder(folderPath: string): Promise { +export async function deleteFile(filepath: string) { + try { + log.info("Deleting file", `"${filepath}"`); + await unlink(filepath); + } catch (error: any) { + log.error("Could not delete file", `"${filepath}"`); + throw CustomError.fromError(error, "generic.fs.delete-file"); + } +} + +export function deleteFileSync(filepath: string) { + try { + log.info("Deleting file", `"${filepath}"`); + unlinkSync(filepath); + } catch (error: any) { + log.error("Could not delete file", `"${filepath}"`); + throw CustomError.fromError(error, "generic.fs.delete-file"); + } +} + +export async function deleteFolder(folderPath: string, options?: RmOptions) { if (!(await pathExist(folderPath))) { return; } - return rm(folderPath, { recursive: true, force: true }); + + try { + options = options || { recursive: true, force: true }; + log.info("Deleting folder", `"${folderPath}"`, options); + await rm(folderPath, options); + } catch (error: any) { + log.error("Could not delete folder", `"${folderPath}"`); + throw CustomError.fromError(error, "generic.fs.delete-folder"); + } } -export async function unlinkPath(path: string): Promise { - if (!(await pathExist(path))) { +export function deleteFolderSync(folderPath: string, options?: RmOptions) { + if (!pathExistsSync(folderPath)) { return; } - return unlink(path); + + try { + options = options || { recursive: true, force: true }; + log.info("Deleting folder", `"${folderPath}"`, options); + } catch (error: any) { + log.error("Could not delete folder", `"${folderPath}"`); + throw CustomError.fromError(error, "generic.fs.delete-folder"); + } } export async function getFoldersInFolder(folderPath: string, opts?: { ignoreSymlinkTargetError?: boolean }): Promise { @@ -97,23 +132,22 @@ export function moveFolderContent(src: string, dest: string, option?: MoveOption const files = await readdir(src, { encoding: "utf-8", withFileTypes: true }); progress.total = files.length; - for(const file of files){ + 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){ + if (file.isFile() || !allChildsAlreadyExist) { const prevSize = await getSize(srcFullPath); await move(srcFullPath, destFullPath, option); const afterSize = await getSize(destFullPath); // The size after moving should be the same or greater than the size before moving but never less - if(afterSize < prevSize){ + if (afterSize < prevSize) { throw new CustomError(`File size mismath. before: ${prevSize}, after: ${afterSize} (${srcFullPath})`, "FILE_SIZE_MISMATCH"); } - } else { log.info(`Skipping ${srcFullPath} to ${destFullPath}, all child already exist in destination`); } @@ -121,7 +155,9 @@ export function moveFolderContent(src: string, dest: string, option?: MoveOption progress.current++; subscriber.next(progress); } - })().catch(err => subscriber.error(CustomError.fromError(err, err?.code))).finally(() => subscriber.complete()); + })() + .catch(err => subscriber.error(CustomError.fromError(err, err?.code))) + .finally(() => subscriber.complete()); }); } @@ -160,7 +196,7 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti await copy(sourcePath, destinationPath, options); } else if (item.isSymbolicLink()) { if (options?.overwrite) { - await unlinkPath(destinationPath); + await deleteFile(destinationPath); } const symlinkTarget = await readlink(sourcePath); const relativePath = path.relative(src, symlinkTarget); @@ -180,8 +216,7 @@ export function hashFile(filePath: string, algorithm = "sha256"): Promise{ - +export async function dirSize(dirPath: string): Promise { const entries = await readdir(dirPath); const paths = entries.map(async entry => { @@ -200,11 +235,10 @@ export async function dirSize(dirPath: string): Promise{ return 0; }); - return (await Promise.all(paths)).flat(Infinity).reduce((acc, size ) => acc + size, 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; @@ -216,15 +250,19 @@ export function rxCopy(src: string, dest: string, option?: CopyOptions): Observa 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)) - }) + 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)); + }); }) ); } @@ -257,7 +295,7 @@ export function ensurePathNotAlreadyExistSync(path: string): string { return destPath; } -export async function isJunction(path: string): Promise{ +export async function isJunction(path: string): Promise { const [stats, lstats] = await Promise.all([stat(path), lstat(path)]); return lstats.isSymbolicLink() && stats.isDirectory(); } @@ -265,7 +303,7 @@ export async function isJunction(path: string): Promise{ 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()); + const { result: driveLetter, error } = tryit(() => execSync(command).toString().trim()); if (!driveLetter || error) { throw new Error("Unable to resolve GUID path", error); } @@ -292,7 +330,7 @@ export async function getSize(targetPath: string, maxDepth = 5): Promise const visited = new Set(); const computeSize = async (currentPath: string, depth: number): Promise => { - if (visited.has(currentPath)){ + if (visited.has(currentPath)) { return 0; } @@ -309,9 +347,7 @@ export async function getSize(targetPath: string, maxDepth = 5): Promise } const entries = await readdir(currentPath); - const sizes = await Promise.all( - entries.map((entry) => computeSize(path.join(currentPath, entry), depth + 1)) - ); + const sizes = await Promise.all(entries.map(entry => computeSize(path.join(currentPath, entry), depth + 1))); return sizes.reduce((acc, cur) => acc + cur, 0); }; diff --git a/src/main/main.ts b/src/main/main.ts index ab12b37c..1dbd597a 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -23,9 +23,11 @@ import { LivShortcut } from "./services/liv/liv-shortcut.service"; import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service"; import { FileAssociationService } from "./services/file-association.service"; import { SongDetailsCacheService } from "./services/additional-content/maps/song-details-cache.service"; -import { Dirent, readdirSync, rmSync, unlinkSync } from "fs-extra"; +import { Dirent, readdirSync } from "fs-extra"; import { StaticConfigurationService } from "./services/static-configuration.service"; import { configureProxy } from './helpers/proxy.helpers'; +import { deleteFileSync, deleteFolderSync } from "./helpers/fs.helpers"; +import { tryit } from "shared/helpers/error.helpers"; const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true"; const staticConfig = StaticConfigurationService.getInstance(); @@ -238,12 +240,7 @@ function deleteOldLogs(): void { for (const folder of deleteLogFolders) { const folderPath = path.join(folder.parentPath, folder.name); - try { - rmSync(folderPath, { recursive: true, force: true }); - log.info("Deleted log folder:", folderPath); - } catch (error) { - log.error("Error deleting folder:", folderPath, error); - } + tryit(() => deleteFolderSync(folderPath)); } } @@ -255,12 +252,7 @@ function deleteOldestLogs(): void { for (const file of logs) { const filepath = path.join(file.parentPath, file.name); - try { - unlinkSync(filepath); - log.info("Deleted log file:", filepath); - } catch (error) { - log.error("Error deleting file:", filepath, error); - } + tryit(() => deleteFileSync(filepath)); } } diff --git a/src/main/services/additional-content/local-models-manager.service.ts b/src/main/services/additional-content/local-models-manager.service.ts index 2513482f..c8192bdd 100644 --- a/src/main/services/additional-content/local-models-manager.service.ts +++ b/src/main/services/additional-content/local-models-manager.service.ts @@ -8,7 +8,7 @@ import path from "path"; import { RequestService } from "../request.service"; import { copyFileSync } from "fs-extra"; import sanitize from "sanitize-filename"; -import { Progression, ensureFolderExist, unlinkPath } from "../../helpers/fs.helpers"; +import { Progression, deleteFile, ensureFolderExist } from "../../helpers/fs.helpers"; import { MODEL_FILE_EXTENSIONS, MODEL_TYPES, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants"; import { InstallationLocationService } from "../installation-location.service"; import { Observable, Subscription, lastValueFrom } from "rxjs"; @@ -105,7 +105,9 @@ export class LocalModelsManagerService { } public async oneClickDownloadModel(model: MSModel): Promise { - if (!model) { return; } + if (!model) { + return; + } const versions = await this.localVersion.getInstalledVersions(); const downloaded = await lastValueFrom(this.downloadModel(model, versions.pop())); @@ -198,7 +200,7 @@ export class LocalModelsManagerService { }; for (const model of models) { - await unlinkPath(model.path); + await deleteFile(model.path); progression.data.push(model); progression.current = progression.data.length; subscriber.next(progression); diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index 4de1b4a1..7f22684b 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -11,7 +11,7 @@ import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/mo import { readFileSync, Stats } from "fs"; import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service"; import { copy, ensureDir, pathExists, pathExistsSync, realpath, writeFileSync } from "fs-extra"; -import { Progression, getUniqueFileNamePath, unlinkPath } from "../../helpers/fs.helpers"; +import { Progression, deleteFile, getUniqueFileNamePath } from "../../helpers/fs.helpers"; import { FileAssociationService } from "../file-association.service"; import { SongDetailsCacheService } from "./maps/song-details-cache.service"; import { sToMs } from "shared/helpers/time.helpers"; @@ -409,7 +409,7 @@ export class LocalPlaylistsManagerService { } public deletePlaylistFile(bpList: LocalBPList): Observable{ - return from(unlinkPath(bpList.path)); + return from(deleteFile(bpList.path)); } public exportPlaylists(opt: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}): Observable> { diff --git a/src/main/services/additional-content/maps/local-maps-manager.service.ts b/src/main/services/additional-content/maps/local-maps-manager.service.ts index 26bac188..168a2f21 100644 --- a/src/main/services/additional-content/maps/local-maps-manager.service.ts +++ b/src/main/services/additional-content/maps/local-maps-manager.service.ts @@ -7,7 +7,7 @@ import { InstallationLocationService } from "../../installation-location.service import { UtilsService } from "../../utils.service"; import crypto, { BinaryLike } from "crypto"; import { lstatSync } from "fs"; -import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra"; +import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath } from "fs-extra"; import { RequestService } from "../../request.service"; import sanitize from "sanitize-filename"; import { DeepLinkService } from "../../deep-link.service"; @@ -15,7 +15,7 @@ import log from "electron-log"; import { WindowManagerService } from "../../window-manager.service"; import { Observable, Subject, lastValueFrom } from "rxjs"; import { Archive } from "../../../models/archive.class"; -import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers"; +import { Progression, deleteFile, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers"; import { readFile } from "fs/promises"; import { FolderLinkerService } from "../../folder-linker.service"; import { allSettled } from "../../../../shared/helpers/promise.helpers"; @@ -297,7 +297,7 @@ export class LocalMapsManagerService { observer.next(progress); } })() - .catch(e => {observer.error(e); console.log("AAAA", e)}) + .catch(e => observer.error(e)) .finally(() => observer.complete()); }); } @@ -449,7 +449,7 @@ export class LocalMapsManagerService { const zip = await BsmZipExtractor.fromPath(zipPath); await zip.extract(mapPath); zip.close(); - await unlink(zipPath); + await deleteFile(zipPath); const localMap = await this.loadMapInfoFromPath(mapPath); localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash); diff --git a/src/main/services/folder-linker.service.ts b/src/main/services/folder-linker.service.ts index 277f3c83..734368ab 100644 --- a/src/main/services/folder-linker.service.ts +++ b/src/main/services/folder-linker.service.ts @@ -1,9 +1,9 @@ import { InstallationLocationService } from "./installation-location.service"; import log from "electron-log"; -import { deleteFolder, ensureFolderExist, moveFolderContent, pathExist, unlinkPath } from "../helpers/fs.helpers"; +import { deleteFile, deleteFileSync, deleteFolder, deleteFolderSync, ensureFolderExist, moveFolderContent, pathExist } from "../helpers/fs.helpers"; import { lstat, symlink } from "fs/promises"; import path from "path"; -import { copy, mkdirSync, readlink, rmSync, symlinkSync, unlinkSync } from "fs-extra"; +import { copy, mkdirSync, readlink, symlinkSync } from "fs-extra"; import { lastValueFrom } from "rxjs"; import { noop } from "shared/helpers/function.helpers"; import { StaticConfigurationService } from "./static-configuration.service"; @@ -56,8 +56,8 @@ export class FolderLinkerService { }); tryit(() => { - rmSync(testFolder, { force: true, recursive: true }); - unlinkSync(testLink); + deleteFolderSync(testFolder); + deleteFileSync(testLink); }); if(resLink.error){ @@ -113,7 +113,7 @@ export class FolderLinkerService { if (isTargetedToSharedPath) { return; } - await unlinkPath(folderPath); + await deleteFile(folderPath); log.info(`Linking ${folderPath} to ${sharedPath}; type: ${this.linkingType}`); return symlink(sharedPath, folderPath, this.getLinkingType()); @@ -141,7 +141,7 @@ export class FolderLinkerService { if (!(await this.isFolderSymlink(folderPath))) { return; } - await unlinkPath(folderPath); + await deleteFile(folderPath); const sharedPath = await this.getSharedFolder(folderPath, options?.intermediateFolder); diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts index 9cdfa5ed..4b084653 100644 --- a/src/main/services/mods/bs-mods-manager.service.ts +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -6,11 +6,11 @@ import md5File from "md5-file"; import { RequestService } from "../request.service"; import { BS_EXECUTABLE } from "../../constants"; import log from "electron-log"; -import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers"; +import { deleteFile, deleteFolder, pathExist, Progression } from "../../helpers/fs.helpers"; import { lastValueFrom, Observable } from "rxjs"; import recursiveReadDir from "recursive-readdir"; import { sToMs } from "../../../shared/helpers/time.helpers"; -import { copyFile, ensureDir, pathExistsSync, readdirSync, rm, unlink } from "fs-extra"; +import { copyFile, ensureDir, pathExistsSync, readdirSync } from "fs-extra"; import { CustomError } from "shared/models/exceptions/custom-error.class"; import { popElement } from "shared/helpers/array.helpers"; import { LinuxService } from "../linux.service"; @@ -290,11 +290,10 @@ export class BsModsManagerService { const contentPath = path.join(ipaPath, content.name); - const res = await tryit(() => content.isDirectory() ? ( - rm(contentPath, { force: true, recursive: true }) - ) : ( - unlink(contentPath) - )); + const res = await tryit(() => content.isDirectory() + ? deleteFolder(contentPath) + : deleteFile(contentPath) + ); if(res.error){ log.error("Error while clearing IPA folder content", content.name, res.error); @@ -318,7 +317,7 @@ export class BsModsManagerService { const promises = mod.version.contentHashes.map(content => { const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data"); - return unlinkPath(path.join(verionPath, file)); + return deleteFile(path.join(verionPath, file)); }); await Promise.all(promises); @@ -331,7 +330,10 @@ export class BsModsManagerService { const versionPath = await this.bsLocalService.getVersionPath(version); const promises = mod.version.contentHashes.map(async content => { - return Promise.all([unlinkPath(path.join(versionPath, content.path)), unlinkPath(path.join(versionPath, "IPA", "Pending", content.path))]); + return Promise.all([ + deleteFile(path.join(versionPath, content.path)), + deleteFile(path.join(versionPath, "IPA", "Pending", content.path)) + ]); }); await Promise.all(promises); diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts index 8961ca8c..d51cf4a6 100644 --- a/src/main/services/request.service.ts +++ b/src/main/services/request.service.ts @@ -1,11 +1,10 @@ import { createWriteStream, WriteStream } from 'fs'; -import { Progression } from 'main/helpers/fs.helpers'; +import { deleteFileSync, Progression } from 'main/helpers/fs.helpers'; import { Observable } from 'rxjs'; import { shareReplay, tap } from 'rxjs/operators'; import log from 'electron-log'; import got from 'got'; import { IncomingHttpHeaders, IncomingMessage } from 'http'; -import { unlinkSync } from 'fs-extra'; import { tryit } from 'shared/helpers/error.helpers'; import path from 'path'; import { pipeline } from 'stream/promises'; @@ -84,7 +83,7 @@ export class RequestService { pipeline(stream, file).catch(err => { file?.destroy(); - tryit(() => unlinkSync(dest)); + tryit(() => deleteFileSync(dest)); subscriber.error(err); }); });