mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #772 from silentrald/feat/delete-guard-rails
[feat] added logs when deleting files/folders
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
@@ -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<void> {
|
||||
.then(() => {});
|
||||
}
|
||||
|
||||
export async function deleteFolder(folderPath: string): Promise<void> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
@@ -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<string
|
||||
});
|
||||
}
|
||||
|
||||
export async function dirSize(dirPath: string): Promise<number>{
|
||||
|
||||
export async function dirSize(dirPath: string): Promise<number> {
|
||||
const entries = await readdir(dirPath);
|
||||
|
||||
const paths = entries.map(async entry => {
|
||||
@@ -200,11 +235,10 @@ export async function dirSize(dirPath: string): Promise<number>{
|
||||
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<Progression> {
|
||||
|
||||
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<Progression>(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<boolean>{
|
||||
export async function isJunction(path: string): Promise<boolean> {
|
||||
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<boolean>{
|
||||
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<number>
|
||||
const visited = new Set<string>();
|
||||
|
||||
const computeSize = async (currentPath: string, depth: number): Promise<number> => {
|
||||
if (visited.has(currentPath)){
|
||||
if (visited.has(currentPath)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -309,9 +347,7 @@ export async function getSize(targetPath: string, maxDepth = 5): Promise<number>
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
+5
-13
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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);
|
||||
|
||||
@@ -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<void>{
|
||||
return from(unlinkPath(bpList.path));
|
||||
return from(deleteFile(bpList.path));
|
||||
}
|
||||
|
||||
public exportPlaylists(opt: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}): Observable<Progression<string>> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user