Files
bs-manager/src/renderer/services/version-folder-linker.service.ts
T
MathieuG-P 2c06184b65 Merge pull request #739 from Zagrios/bugfix/check-if-symlink-is-supported-before-linking
[bugfix] Check if symlink is supported on the current filesystem before linking

(cherry picked from commit 4b8004ea80)
2025-01-18 01:13:18 +01:00

210 lines
7.8 KiB
TypeScript

import { LinkOptions, UnlinkOptions } from "main/services/folder-linker.service";
import { map, distinctUntilChanged, filter, mergeMap, shareReplay } from "rxjs/operators";
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;
public static getInstance(): VersionFolderLinkerService {
if (!VersionFolderLinkerService.instance) {
VersionFolderLinkerService.instance = new VersionFolderLinkerService();
}
return VersionFolderLinkerService.instance;
}
private readonly KNOWN_ERROR_CODES = ["EPERM", "EACCES", "ENOSPC", "LinkingNotSupported"];
private readonly ipcService: IpcService;
private readonly progress: ProgressBarService;
private readonly notifications: NotificationService;
private readonly _queue$ = new BehaviorSubject<VersionLinkerAction[]>([]);
private readonly linkListeners = new Set<VersionLinkerActionListener>();
private readonly unlinkListeners = new Set<VersionLinkerActionListener>();
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));
}
private async processAction(action: VersionLinkerAction) {
let progressOpened = false;
if (!this.progress.isVisible) {
this.progress.showFake(0.01, null, action.relativeFolder.split(window.electron.path.sep).at(-1));
progressOpened = true;
}
const { error } = await tryit(() => lastValueFrom(this.doAction(action)));
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, !error));
} else {
this.unlinkListeners.forEach(listener => listener(action, !error));
}
if (progressOpened) {
this.progress.hide(true);
}
const newArr = [...this._queue$.value];
newArr.shift();
this._queue$.next(newArr);
}
private doAction(action: VersionLinkerAction): Observable<boolean> {
return this.ipcService.sendV2<boolean, VersionLinkerAction>("link-version-folder-action", { args: action });
}
private get currentAction$(): Observable<VersionLinkerAction> {
return this._queue$.pipe(
map(actions => actions.at(0)),
distinctUntilChanged()
);
}
public linkVersionFolder(action: VersionLinkFolderAction): Promise<boolean> {
const promise = new Promise<boolean>(resolve => {
const callBack: VersionLinkerActionListener = (performedAction, linked) => {
if (performedAction !== action) {
return;
}
this.removeVersionFolderLinkedListener(callBack);
resolve(linked);
};
this.onVersionFolderLinked(callBack);
});
this._queue$.next([...this._queue$.value, action]);
return promise;
}
public unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise<boolean> {
const promise = new Promise<boolean>(resolve => {
const callBack: VersionLinkerActionListener = (performedAction, linked) => {
if (performedAction !== action) {
return;
}
this.removeVersionFolderUnlinkedListener(callBack);
resolve(linked);
};
this.onVersionFolderUnlinked(callBack);
});
this._queue$.next([...this._queue$.value, action]);
return promise;
}
public onVersionFolderLinked(listener: VersionLinkerActionListener): void {
this.linkListeners.add(listener);
}
public removeVersionFolderLinkedListener(listener: VersionLinkerActionListener): void {
this.linkListeners.delete(listener);
}
public onVersionFolderUnlinked(listener: VersionLinkerActionListener): void {
this.unlinkListeners.add(listener);
}
public removeVersionFolderUnlinkedListener(listener: VersionLinkerActionListener): void {
this.unlinkListeners.delete(listener);
}
public cancelAction(version: BSVersion, relativeFolder: string): void {
this._queue$.next(this._queue$.value.filter(a => a.version !== version || a.relativeFolder !== relativeFolder));
}
public isVersionFolderLinked(version: BSVersion, relativeFolder: string): Observable<boolean> {
return this.ipcService.sendV2("is-version-folder-linked", { args: { version, relativeFolder } });
}
public $folderLinkedState(version: BSVersion, relativeFolder: string): Observable<FolderLinkState> {
return this._queue$.pipe(
mergeMap(queue => {
const currentAction = queue.at(0);
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);
}
return this.isVersionFolderLinked(version, relativeFolder).pipe(
map(linked => linked ? FolderLinkState.Linked : FolderLinkState.Unlinked)
);
}),
distinctUntilChanged(),
shareReplay(1)
);
}
public $isPending(version: BSVersion, relativeFolder: string): Observable<boolean> {
return this.$folderLinkedState(version, relativeFolder).pipe(map(state => state === FolderLinkState.Pending));
}
public $isProcessing(version: BSVersion, relativeFolder: string): Observable<boolean> {
return this.$folderLinkedState(version, relativeFolder).pipe(map(state => state === FolderLinkState.Processing));
}
public getLinkedFolders(version: BSVersion, options?: { relative?: boolean }): Observable<string[]> {
return this.ipcService.sendV2("get-linked-folders", { args: { version, options } });
}
public relinkAllVersionsFolders(): Observable<void> {
return this.ipcService.sendV2("relink-all-versions-folders");
}
}
export const enum VersionLinkerActionType {
Link = "link",
Unlink = "unlink",
}
export interface VersionLinkerAction {
version: BSVersion;
relativeFolder: string;
type: VersionLinkerActionType;
options?: LinkOptions;
}
export type VersionLinkFolderAction = Omit<VersionLinkerAction, "type">;
export type VersionUnlinkFolderAction = Omit<VersionLinkerAction, "type"> & { options: UnlinkOptions };
export type VersionLinkerActionListener = (action: VersionLinkerAction, linked: boolean) => void;
export enum FolderLinkState {
Linked,
Unlinked,
Pending,
Processing
}