From 844ca12b9cda360fea80b072aa9d752899d97604 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sun, 25 Aug 2024 15:03:08 +0200 Subject: [PATCH] [bugfix] Improve folder linking reliability --- assets/jsons/translations/de.json | 9 ++++ assets/jsons/translations/en.json | 9 ++++ assets/jsons/translations/es.json | 9 ++++ assets/jsons/translations/fr.json | 10 +++++ assets/jsons/translations/ja.json | 9 ++++ assets/jsons/translations/ru.json | 9 ++++ assets/jsons/translations/zh-tw.json | 9 ++++ assets/jsons/translations/zh.json | 9 ++++ src/main/helpers/fs.helpers.ts | 23 ++++------- src/main/services/folder-linker.service.ts | 8 ++-- .../services/version-folder-linker.service.ts | 20 ++++----- .../services/version-folder-linker.service.ts | 41 ++++++++++++------- 12 files changed, 119 insertions(+), 46 deletions(-) diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index c8f82ee2..10e276c5 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -481,6 +481,15 @@ "title": "Sicherung erstellt", "msg": "Das Freigeben des Ordners „UserData“ kann zu Fehlern führen. Bei Problemen trenne den Ordner, um die Sicherung wiederherzustellen" } + }, + "linking-error": { + "title": "Fehler beim Verknüpfen des Ordners", + "msg": { + "EPERM": "BSManager hat nicht die erforderlichen Berechtigungen, um den Ordner zu verknüpfen.", + "EACCES": "BSManager hat nicht die erforderlichen Berechtigungen, um den Ordner zu verknüpfen.", + "ENOSPC": "Die Festplatte ist voll, machen Sie Platz und versuchen Sie es erneut.", + "UNKNOWN_ERROR": "Ein unbekannter Fehler ist beim Verknüpfen des Ordners aufgetreten." + } } }, "create-launch-shortcut": { diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 9315eb7d..109ed3cb 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -481,6 +481,15 @@ "title": "Backup created", "msg": "Sharing the 'UserData' folder can generate errors, in case of problems unlink the folder to restore the backup" } + }, + "linking-error": { + "title": "Error while linking folder", + "msg": { + "EPERM": "BSManager does not have the necessary permissions to link the folder.", + "EACCES": "BSManager does not have the necessary permissions to link the folder.", + "ENOSPC": "The disk is full, make some space and try again.", + "UNKNOWN_ERROR":"An unknown error has occurred while linking the folder." + } } }, "create-launch-shortcut": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index e176908e..9edd4163 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -481,6 +481,15 @@ "title": "Backup creado", "msg": "Compartir la carpeta 'UserData' puede generar errores, en caso de problemas desvincula la carpeta para restaurar la copia de seguridad" } + }, + "linking-error": { + "title": "Error al enlazar la carpeta", + "msg": { + "EPERM": "BSManager no tiene los permisos necesarios para enlazar la carpeta.", + "EACCES": "BSManager no tiene los permisos necesarios para enlazar la carpeta.", + "ENOSPC": "El disco está lleno, libera espacio e inténtalo de nuevo.", + "UNKNOWN_ERROR": "Se ha producido un error desconocido al enlazar la carpeta." + } } }, "create-launch-shortcut": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index c6672e29..998080b9 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -481,7 +481,17 @@ "title": "Sauvegarde créée", "msg": "Le partage du dossier 'UserData' peut générer des erreurs, en cas de soucis déliez le dossier pour restaurer la sauvegarde" } + }, + "linking-error": { + "title": "Erreur lors de la liaison du dossier", + "msg": { + "EPERM": "BSManager n'a pas les autorisations nécessaires pour lier le dossier.", + "EACCES": "BSManager n'a pas les autorisations nécessaires pour lier le dossier.", + "ENOSPC": "Le disque est plein, faites de la place et réessayez.", + "UNKNOWN_ERROR": "Une erreur inconnue est survenue lors de la liaison du dossier." + } } + }, "create-launch-shortcut": { "success": { diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index f4185b5e..e3c3c510 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -481,6 +481,15 @@ "title": "バックアップを作成しました", "msg": "'UserData'フォルダを共有するとエラーが発生する可能性があります。問題が発生した場合は、フォルダをリンク解除してバックアップを復元してください。" } + }, + "linking-error": { + "title": "フォルダのリンク中にエラーが発生しました", + "msg": { + "EPERM": "BSManagerにはフォルダをリンクするための必要な権限がありません。", + "EACCES": "BSManagerにはフォルダをリンクするための必要な権限がありません。", + "ENOSPC": "ディスクがいっぱいです。空き容量を作ってもう一度試してください。", + "UNKNOWN_ERROR": "フォルダをリンク中に不明なエラーが発生しました。" + } } }, "create-launch-shortcut": { diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index bcbbc6c1..fca856f2 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -481,6 +481,15 @@ "title": "Бэкап создан", "msg": "Общая папка 'UserData' может создавать ошибки. В случае проблем отвяжите папку, чтобы восстановить бэкап." } + }, + "linking-error": { + "title": "Ошибка при связывании папки", + "msg": { + "EPERM": "BSManager не имеет необходимых прав для связывания папки.", + "EACCES": "BSManager не имеет необходимых прав для связывания папки.", + "ENOSPC": "Диск заполнен, освободите место и попробуйте снова.", + "UNKNOWN_ERROR": "Произошла неизвестная ошибка при связывании папки." + } } }, "create-launch-shortcut": { diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 267473ea..73021787 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -481,6 +481,15 @@ "title": "備份已創建", "msg": "共享 “UserData” 文件夾可能會產生錯誤,如果出現問題,請取消關聯該文件夾以恢復備份" } + }, + "linking-error": { + "title": "連結文件夾時出錯", + "msg": { + "EPERM": "BSManager沒有必要的權限來連結文件夾。", + "EACCES": "BSManager沒有必要的權限來連結文件夾。", + "ENOSPC": "磁碟已滿,請騰出空間後再試。", + "UNKNOWN_ERROR": "連結文件夾時發生未知錯誤。" + } } }, "create-launch-shortcut": { diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index b55c8eff..1a1a5067 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -481,6 +481,15 @@ "title": "备份已创建", "msg": "共享 “UserData” 文件夹可能会产生错误,如果出现问题,请取消关联该文件夹以恢复备份" } + }, + "linking-error": { + "title": "链接文件夹时出错", + "msg": { + "EPERM": "BSManager没有必要的权限来链接文件夹。", + "EACCES": "BSManager没有必要的权限来链接文件夹。", + "ENOSPC": "磁盘已满,请腾出空间后再试。", + "UNKNOWN_ERROR": "链接文件夹时发生未知错误。" + } } }, "create-launch-shortcut": { diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index 7fa474f7..fe21bc52 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -1,4 +1,4 @@ -import { CopyOptions, copy, createReadStream, ensureDir, move, realpath, stat, symlink } from "fs-extra"; +import { CopyOptions, MoveOptions, copy, createReadStream, ensureDir, move, 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"; @@ -7,6 +7,7 @@ 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 { @@ -78,7 +79,7 @@ export async function getFilesInFolder(folderPath: string): Promise { return dirEntries.filter(entry => entry.isFile()).map(file => path.join(folderPath, file.name)); } -export function moveFolderContent(src: string, dest: string): Observable { +export function moveFolderContent(src: string, dest: string, option?: MoveOptions): Observable { const progress: Progression = { current: 0, total: 0 }; return new Observable(subscriber => { subscriber.next(progress); @@ -89,25 +90,19 @@ export function moveFolderContent(src: string, dest: string): Observable { + for(const file of files){ const srcFullPath = path.join(src, file); const destFullPath = path.join(dest, file); - if (await pathExist(destFullPath)) { - progress.current++; - return subscriber.next(progress); - } - await move(srcFullPath, destFullPath); + await move(srcFullPath, destFullPath, option); progress.current++; subscriber.next(progress); - }); - - Promise.allSettled(promises).then(() => subscriber.complete()); - })(); + } + })().catch(err => subscriber.error(CustomError.fromError(err, err?.code))).finally(() => subscriber.complete()); }); } @@ -175,7 +170,7 @@ export async function dirSize(dirPath: string): Promise{ const realPath = await realpath(fullPath); const stat = await lstat(realPath); - if (stat.isDirectory()) { + if (stat.isDirectory()) { return dirSize(fullPath); } diff --git a/src/main/services/folder-linker.service.ts b/src/main/services/folder-linker.service.ts index 99316a21..42f7686c 100644 --- a/src/main/services/folder-linker.service.ts +++ b/src/main/services/folder-linker.service.ts @@ -4,6 +4,8 @@ import { deleteFolder, ensureFolderExist, moveFolderContent, pathExist, unlinkPa import { lstat, symlink } from "fs/promises"; import path from "path"; import { copy, readlink } from "fs-extra"; +import { lastValueFrom } from "rxjs"; +import { noop } from "shared/helpers/function.helpers"; export class FolderLinkerService { private static instance: FolderLinkerService; @@ -72,7 +74,7 @@ export class FolderLinkerService { await ensureFolderExist(folderPath); if (options?.keepContents !== false) { - await moveFolderContent(folderPath, sharedPath).toPromise(); + await lastValueFrom(moveFolderContent(folderPath, sharedPath, { overwrite: true })); } await deleteFolder(folderPath); @@ -95,9 +97,7 @@ export class FolderLinkerService { } if (options.moveContents === true) { - return moveFolderContent(sharedPath, folderPath) - .toPromise() - .then(() => {}); + return lastValueFrom(moveFolderContent(sharedPath, folderPath, { overwrite: true })).then(noop); } if (options?.keepContents === false) { diff --git a/src/main/services/version-folder-linker.service.ts b/src/main/services/version-folder-linker.service.ts index 1a8635b0..f89751dd 100644 --- a/src/main/services/version-folder-linker.service.ts +++ b/src/main/services/version-folder-linker.service.ts @@ -1,6 +1,6 @@ import { getFoldersInFolder } from "../helpers/fs.helpers"; import path from "path"; -import { VersionLinkerAction, VersionLinkFolderAction, VersionUnlinkFolderAction } from "renderer/services/version-folder-linker.service"; +import { VersionLinkerAction, VersionUnlinkFolderAction } from "renderer/services/version-folder-linker.service"; import { BSVersion } from "shared/bs-version.interface"; import { LocalMapsManagerService } from "./additional-content/local-maps-manager.service"; import { BSLocalVersionService } from "./bs-local-version.service"; @@ -58,17 +58,14 @@ export class VersionFolderLinkerService { return path.join(parentPath, relativePath); } - public async linkVersionFolder(action: VersionLinkerAction): Promise { + public async linkVersionFolder(action: VersionLinkerAction): Promise { action.options = this.specialFolderOption(action.relativeFolder, action.options); const versionPath = await this.localVersion.getVersionPath(action.version); const folderPath = this.relativeToFullPath(versionPath, action.relativeFolder); - return this.folderLinker - .linkFolder(folderPath, action.options) - .catch(() => false) - .then(() => true); + return this.folderLinker.linkFolder(folderPath, action.options) } - public async unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise { + public async unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise { action.options = this.specialFolderOption(action.relativeFolder, action.options); const versionPath = await this.localVersion.getVersionPath(action.version); @@ -76,13 +73,10 @@ export class VersionFolderLinkerService { action.options.moveContents = !(await this.isOtherVersionHaveFolderLinked(action.relativeFolder, folderPath)); - return this.folderLinker - .unlinkFolder(folderPath, action.options) - .catch(() => false) - .then(() => true); + return this.folderLinker.unlinkFolder(folderPath, action.options); } - public async doAction(action: VersionLinkerAction): Promise { + public doAction(action: VersionLinkerAction): Promise { if (action.type === "link") { return this.linkVersionFolder(action); } @@ -123,7 +117,7 @@ export class VersionFolderLinkerService { for (const version of versions) { const linkedFolders = await this.getLinkedFolders(version, { relative: true, ignoreSymlinkTargetError: true }); - const actions = linkedFolders.map(folder => ({ type: "link", version, relativeFolder: folder } as VersionLinkFolderAction)); + const actions = linkedFolders.map(folder => ({ type: "link", version, relativeFolder: folder } as VersionLinkerAction)); await Promise.all(actions.map(action => this.doAction(action))); } } diff --git a/src/renderer/services/version-folder-linker.service.ts b/src/renderer/services/version-folder-linker.service.ts index 936de9d6..b8c0acff 100644 --- a/src/renderer/services/version-folder-linker.service.ts +++ b/src/renderer/services/version-folder-linker.service.ts @@ -1,10 +1,15 @@ import { LinkOptions, UnlinkOptions } from "main/services/folder-linker.service"; import { map, distinctUntilChanged, filter, mergeMap, shareReplay } from "rxjs/operators"; -import { BehaviorSubject, Observable, of } from "rxjs"; +import { BehaviorSubject, lastValueFrom, Observable, of } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { IpcService } from "./ipc.service"; import { ProgressBarService } from "./progress-bar.service"; import equal from "fast-deep-equal"; +import { tryit } from "shared/helpers/error.helpers"; +import { NotificationService } from "./notification.service"; +import { CustomError } from "shared/models/exceptions/custom-error.class"; + + export class VersionFolderLinkerService { private static instance: VersionFolderLinkerService; @@ -16,8 +21,11 @@ export class VersionFolderLinkerService { return VersionFolderLinkerService.instance; } + private readonly KNOWN_ERROR_CODES = ["EPERM", "EACCES", "ENOSPC"]; + private readonly ipcService: IpcService; private readonly progress: ProgressBarService; + private readonly notifications: NotificationService; private readonly _queue$ = new BehaviorSubject([]); @@ -27,6 +35,7 @@ export class VersionFolderLinkerService { private constructor() { this.ipcService = IpcService.getInstance(); this.progress = ProgressBarService.getInstance(); + this.notifications = NotificationService.getInstance(); this.currentAction$.pipe(filter(action => !!action)).subscribe(action => this.processAction(action)); } @@ -39,14 +48,22 @@ export class VersionFolderLinkerService { progressOpened = true; } - const linked = await this.doAction(action).toPromise(); + const { error } = await tryit(() => lastValueFrom(this.doAction(action))); - // Spécial notification + if(error){ + const { code } = (error as CustomError); + const message = this.KNOWN_ERROR_CODES.includes(code) ? `notifications.shared-folder.linking-error.msg.${code}` : "notifications.shared-folder.linking-error.msg.UNKNOWN_ERROR"; + this.notifications.notifyError({ + title: "notifications.shared-folder.linking-error.title", + desc: message + }) + } + // Special notification if (action.type === VersionLinkerActionType.Link) { - this.linkListeners.forEach(listener => listener(action, linked)); + this.linkListeners.forEach(listener => listener(action, !error)); } else { - this.unlinkListeners.forEach(listener => listener(action, linked)); + this.unlinkListeners.forEach(listener => listener(action, !error)); } if (progressOpened) { @@ -136,7 +153,7 @@ export class VersionFolderLinkerService { if(currentAction && equal(currentAction.version, version) && currentAction.relativeFolder === relativeFolder) { return of(FolderLinkState.Processing) } - + if(queue.some(action => equal(action.version, version) && action.relativeFolder === relativeFolder)) { return of(FolderLinkState.Pending); } @@ -146,7 +163,7 @@ export class VersionFolderLinkerService { ); }), distinctUntilChanged(), - shareReplay(1) + shareReplay(1) ); } @@ -179,14 +196,8 @@ export interface VersionLinkerAction { options?: LinkOptions; } -export interface VersionLinkFolderAction extends VersionLinkerAction { - type: VersionLinkerActionType.Link; -} - -export interface VersionUnlinkFolderAction extends VersionLinkerAction { - type: VersionLinkerActionType.Unlink; - options?: UnlinkOptions; -} +export type VersionLinkFolderAction = Omit; +export type VersionUnlinkFolderAction = Omit & { options: UnlinkOptions }; export type VersionLinkerActionListener = (action: VersionLinkerAction, linked: boolean) => void;