mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge pull request #180 from Zagrios/hotfix/backup-userdata-on-link
[hotfix] create backup of userdata folder when linking
This commit is contained in:
@@ -421,10 +421,10 @@
|
||||
}
|
||||
},
|
||||
"shared-folder": {
|
||||
"success": {
|
||||
"yeet-mods-disabled": {
|
||||
"title": "YeetMods disabled",
|
||||
"msg": "To prevent mod removal, the value of the 'YeetMods' variable in the 'Beat Saber IPA.json' file has been disabled"
|
||||
"info": {
|
||||
"userdata-backup-created":{
|
||||
"title": "Backup created",
|
||||
"msg": "Sharing the 'UserData' folder can generate errors, in case of problems unlink the folder to restore the backup"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,10 +420,10 @@
|
||||
}
|
||||
},
|
||||
"shared-folder": {
|
||||
"success": {
|
||||
"yeet-mods-disabled": {
|
||||
"title": "YeetMods desactivado",
|
||||
"msg": "Para evitar la eliminación de mods, se ha desactivado el valor de la variable 'YeetMods' en el archivo 'Beat Saber IPA.json'"
|
||||
"info": {
|
||||
"userdata-backup-created":{
|
||||
"title": "Backup creado",
|
||||
"msg": "Compartir la carpeta 'UserData' puede generar errores, en caso de problemas desvincular la carpeta para restaurar la copia de seguridad"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,10 +420,10 @@
|
||||
}
|
||||
},
|
||||
"shared-folder":{
|
||||
"success": {
|
||||
"yeet-mods-disabled":{
|
||||
"title": "YeetMods désactivé",
|
||||
"msg": "Pour éviter la suppression des mods, la valeur de la variable 'YeetMods' dans le fichier 'Beat Saber IPA.json' a été désactivée"
|
||||
"info": {
|
||||
"userdata-backup-created":{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IpcService } from '../services/ipc.service';
|
||||
import { from } from 'rxjs';
|
||||
import path from 'path';
|
||||
import { pathExist } from '../helpers/fs.helpers';
|
||||
import { FolderLinkerService } from '../services/folder-linker.service';
|
||||
import { FolderLinkerService, LinkOptions } from '../services/folder-linker.service';
|
||||
import { LocalMapsManagerService } from '../services/additional-content/local-maps-manager.service';
|
||||
import { readJSON, writeJSON } from 'fs-extra';
|
||||
import log from "electron-log"
|
||||
@@ -85,16 +85,23 @@ ipc.on("get-linked-folders", async (req: IpcRequest<BSVersion>, reply) => {
|
||||
reply(from(localVersions.getLinkedFolders(req.args)));
|
||||
});
|
||||
|
||||
ipc.on("link-folder", async (req: IpcRequest<{ folder: string, keepContents?: boolean}>, reply) => {
|
||||
ipc.on("link-folder", async (req: IpcRequest<{ folder: string, options?: LinkOptions}>, reply) => {
|
||||
|
||||
req.args.options ??= {};
|
||||
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
|
||||
const relativeMapsFolder = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER)
|
||||
|
||||
if(req.args.folder.includes(relativeMapsFolder)){
|
||||
return reply(from(linker.linkFolder(req.args.folder, {keepContents: req.args.keepContents, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER})));
|
||||
return reply(from(linker.linkFolder(req.args.folder, {keepContents: req.args.options?.keepContents, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER})));
|
||||
}
|
||||
|
||||
const res = from(linker.linkFolder(req.args.folder, {keepContents: true}));
|
||||
if(req.args.folder.includes("UserData")){
|
||||
req.args.options = { ...req.args.options, backup: true };
|
||||
}
|
||||
|
||||
const res = from(linker.linkFolder(req.args.folder, req.args.options));
|
||||
|
||||
const jsonIPAPath = path.join(req.args.folder, "Beat Saber IPA.json");
|
||||
|
||||
@@ -114,16 +121,23 @@ ipc.on("link-folder", async (req: IpcRequest<{ folder: string, keepContents?: bo
|
||||
|
||||
});
|
||||
|
||||
ipc.on("unlink-folder", async (req: IpcRequest<{folder: string, keepContents?: boolean}>, reply) => {
|
||||
ipc.on("unlink-folder", async (req: IpcRequest<{ folder: string, options?: LinkOptions}>, reply) => {
|
||||
|
||||
req.args.options ??= {};
|
||||
|
||||
const linker = FolderLinkerService.getInstance();
|
||||
|
||||
const relativeMapsFolder = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER)
|
||||
|
||||
if(req.args.folder.includes(relativeMapsFolder)){
|
||||
return reply(from(linker.unlinkFolder(req.args.folder, {keepContents: req.args.keepContents, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER})));
|
||||
return reply(from(linker.unlinkFolder(req.args.folder, {...req.args.options, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER})));
|
||||
}
|
||||
|
||||
reply(from(linker.unlinkFolder(req.args.folder, {keepContents: req.args.keepContents})));
|
||||
if(req.args.folder.includes("UserData")){
|
||||
req.args.options = { ...req.args.options, backup: true };
|
||||
}
|
||||
|
||||
reply(from(linker.unlinkFolder(req.args.folder, req.args.options)));
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,22 @@ export class FolderLinkerService {
|
||||
return path.join(this.sharedFolder, intermediateFolder ?? "", path.basename(folderPath));
|
||||
}
|
||||
|
||||
private getBackupFolder(folderPath: string): string {
|
||||
return folderPath + "_backup";
|
||||
}
|
||||
|
||||
private async backupFolder(folderPath: string): Promise<void> {
|
||||
if(!await pathExist(folderPath)){ return; }
|
||||
return copy(folderPath, this.getBackupFolder(folderPath), { overwrite: true, errorOnExist: false });
|
||||
}
|
||||
|
||||
private async restoreFolder(folderPath: string): Promise<void> {
|
||||
if(!await pathExist( this.getBackupFolder(folderPath) )){ return; }
|
||||
return copy(this.getBackupFolder(folderPath), folderPath, { overwrite: true, errorOnExist: false }).then(() => {
|
||||
return deleteFolder(this.getBackupFolder(folderPath));
|
||||
});
|
||||
}
|
||||
|
||||
public async linkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
|
||||
|
||||
if(await this.isFolderSymlink(folderPath)){ return; }
|
||||
@@ -39,10 +55,14 @@ export class FolderLinkerService {
|
||||
await ensureFolderExist(folderPath);
|
||||
await ensureFolderExist(sharedPath);
|
||||
|
||||
if(options?.backup === true){
|
||||
await this.backupFolder(folderPath);
|
||||
}
|
||||
|
||||
if(options?.keepContents !== false){
|
||||
await moveFolderContent(folderPath, sharedPath).toPromise();
|
||||
}
|
||||
|
||||
|
||||
await deleteFolder(folderPath);
|
||||
|
||||
return symlink(sharedPath, folderPath, "junction");
|
||||
@@ -57,11 +77,15 @@ export class FolderLinkerService {
|
||||
|
||||
await ensureFolderExist(folderPath);
|
||||
|
||||
if(options?.backup === true){
|
||||
return this.restoreFolder(folderPath);
|
||||
}
|
||||
|
||||
if(options?.keepContents === false){ return; }
|
||||
|
||||
await ensureFolderExist(sharedPath);
|
||||
|
||||
return copy(sharedPath, folderPath, { errorOnExist: false });
|
||||
return copy(sharedPath, folderPath, { errorOnExist: false, recursive: true });
|
||||
}
|
||||
|
||||
public async isFolderSymlink(folder: string): Promise<boolean> {
|
||||
@@ -80,4 +104,5 @@ export class FolderLinkerService {
|
||||
export interface LinkOptions {
|
||||
keepContents?: boolean,
|
||||
intermediateFolder?: string,
|
||||
backup?: boolean
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IpcResponse, IpcRequest } from "shared/models/ipc";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { ipcMain } from "electron";
|
||||
import { Observable } from "rxjs";
|
||||
import { IpcChannel, IpcCompleteChannel, IpcErrorChannel } from "shared/models/ipc/ipc-response.interface";
|
||||
import { AppWindow } from "shared/models/window-manager/app-window.model";
|
||||
import { WindowManagerService } from "./window-manager.service";
|
||||
import { IpcReplier } from "shared/models/ipc/ipc-request.interface";
|
||||
import log from "electron-log";
|
||||
|
||||
export class IpcService {
|
||||
|
||||
@@ -47,6 +48,7 @@ export class IpcService {
|
||||
observable.subscribe(data => {
|
||||
this.send(channel, window, data);
|
||||
}, error => {
|
||||
log.error(error);
|
||||
this.send(this.getErrorChannel(channel), window, error);
|
||||
}, () => {
|
||||
this.send(this.getCompleteChannel(channel), window);
|
||||
|
||||
@@ -16,6 +16,7 @@ export const NotificationItem = forwardRef(({resolver, notification}: {resolver?
|
||||
const renderImage = (() => {
|
||||
if(notification.type === NotificationType.SUCCESS){ return BeatRunningImg; }
|
||||
if(notification.type === NotificationType.WARNING){ return BeatWaitingImg; }
|
||||
if(notification.type === NotificationType.INFO){ return BeatWaitingImg; }
|
||||
if(notification.type === NotificationType.ERROR){ return BeatConflictImg; }
|
||||
return BeatImpatientImg;
|
||||
})();
|
||||
@@ -23,6 +24,7 @@ export const NotificationItem = forwardRef(({resolver, notification}: {resolver?
|
||||
const renderNeonColors = (() => {
|
||||
if(notification.type === NotificationType.SUCCESS){ return "bg-green-400 shadow-green-400"; }
|
||||
if(notification.type === NotificationType.WARNING){ return "bg-yellow-400 shadow-yellow-400"; }
|
||||
if(notification.type === NotificationType.INFO){ return "bg-blue-500 shadow-blue-500"; }
|
||||
if(notification.type === NotificationType.ERROR){ return "bg-red-500 shadow-red-500"; }
|
||||
return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white";
|
||||
})();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { of, timer } from "rxjs";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { map, distinctUntilChanged, filter } from "rxjs/operators";
|
||||
import { from } from "rxjs";
|
||||
@@ -6,6 +5,7 @@ import { Observable } from "rxjs";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { LinkOptions } from "main/services/folder-linker.service";
|
||||
|
||||
export class FolderLinkerService {
|
||||
|
||||
@@ -86,20 +86,22 @@ export class FolderLinkerService {
|
||||
|
||||
private specialFolderNotification(action: LinkAction): void{
|
||||
if(action.type === LinkActionType.Link && action.folder.includes("UserData")){
|
||||
this.notification.notifySuccess({
|
||||
title: "notifications.shared-folder.success.yeet-mods-disabled.title",
|
||||
desc: "notifications.shared-folder.success.yeet-mods-disabled.msg"
|
||||
})
|
||||
this.notification.notifyInfo({
|
||||
title: "notifications.shared-folder.info.userdata-backup-created.title",
|
||||
desc: "notifications.shared-folder.info.userdata-backup-created.msg",
|
||||
duration: 7000
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private doAction(action: LinkAction): Observable<void>{
|
||||
return this.ipc.sendV2(action.type === LinkActionType.Link ? "link-folder" : "unlink-folder", { args: { folder: action.folder, keepContents: action.keepContents } });
|
||||
return this.ipc.sendV2<void, {folder: string, options?: LinkOptions}>(action.type === LinkActionType.Link ? "link-folder" : "unlink-folder", { args: { folder: action.folder, options: action.options } });
|
||||
}
|
||||
|
||||
private addFolderToQueue(folder: string, type: LinkActionType, keepContents: boolean){
|
||||
private addFolderToQueue(folder: string, type: LinkActionType, options: LinkOptions){
|
||||
if(this._queue$.value.some(action => action.folder === folder)){ return; }
|
||||
this._queue$.next([...this._queue$.value, {folder, type, keepContents}]);
|
||||
this._queue$.next([...this._queue$.value, {folder, type, options}]);
|
||||
}
|
||||
|
||||
private removeFolderFromQueue(folder: string){
|
||||
@@ -123,13 +125,14 @@ export class FolderLinkerService {
|
||||
return new Map(folders.map(folder => [folder, this.linkFolder(folder)]));
|
||||
}
|
||||
|
||||
public linkFolder(folder: string, keepContents = true): Observable<void> {
|
||||
public linkFolder(folder: string, options: LinkOptions = {}): Observable<void> {
|
||||
options.keepContents ??= true;
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.onLinked(folder, resolve);
|
||||
this.onRemovedFromQueue(folder, resolve);
|
||||
});
|
||||
|
||||
this.addFolderToQueue(folder, LinkActionType.Link, keepContents);
|
||||
this.addFolderToQueue(folder, LinkActionType.Link, options);
|
||||
return from(promise);
|
||||
}
|
||||
|
||||
@@ -137,13 +140,14 @@ export class FolderLinkerService {
|
||||
return new Map(folders.map(folder => [folder, this.unlinkFolder(folder)]));
|
||||
}
|
||||
|
||||
public unlinkFolder(folder: string, keepContents = true): Observable<void> {
|
||||
public unlinkFolder(folder: string, options: LinkOptions = {}): Observable<void> {
|
||||
options.keepContents ??= true;
|
||||
const promise = new Promise<void>(resolve => {
|
||||
this.onUnlinked(folder, resolve);
|
||||
this.onRemovedFromQueue(folder, resolve);
|
||||
});
|
||||
|
||||
this.addFolderToQueue(folder, LinkActionType.Unlink, keepContents);
|
||||
this.addFolderToQueue(folder, LinkActionType.Unlink, options);
|
||||
return from(promise);
|
||||
}
|
||||
|
||||
@@ -172,7 +176,7 @@ export class FolderLinkerService {
|
||||
export interface LinkAction {
|
||||
folder: string;
|
||||
type: LinkActionType;
|
||||
keepContents?: boolean;
|
||||
options?: LinkOptions;
|
||||
}
|
||||
|
||||
export enum LinkActionType {
|
||||
|
||||
@@ -66,7 +66,7 @@ export class MapsManagerService {
|
||||
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
|
||||
return this.linker.linkFolder(versionMapsPath, !!modalRes.data).toPromise();
|
||||
return this.linker.linkFolder(versionMapsPath, {keepContents: !!modalRes.data}).toPromise();
|
||||
}
|
||||
|
||||
public async unlinkVersion(version: BSVersion): Promise<void>{
|
||||
@@ -77,7 +77,7 @@ export class MapsManagerService {
|
||||
|
||||
const versionMapsPath = await this.getVersionMapsPath(version);
|
||||
|
||||
return this.linker.unlinkFolder(versionMapsPath, !!modalRes.data).toPromise();
|
||||
return this.linker.unlinkFolder(versionMapsPath, {keepContents: !!modalRes.data}).toPromise();
|
||||
}
|
||||
|
||||
public async deleteMaps(maps: BsmLocalMap[], version?: BSVersion): Promise<boolean>{
|
||||
|
||||
@@ -53,6 +53,11 @@ export class NotificationService{
|
||||
return this.notify(notification);
|
||||
}
|
||||
|
||||
public notifyInfo(notification: Notification): Promise<NotificationResult|string>{
|
||||
notification.type = NotificationType.INFO;
|
||||
return this.notify(notification);
|
||||
}
|
||||
|
||||
public notifySystem(options: SystemNotificationOptions){
|
||||
this.ipc.sendLazy<SystemNotificationOptions>("notify-system", {args: options});
|
||||
}
|
||||
@@ -71,6 +76,7 @@ export enum NotificationType {
|
||||
SUCCESS = 0,
|
||||
WARNING = 1,
|
||||
ERROR = 2,
|
||||
INFO = 3,
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
|
||||
Reference in New Issue
Block a user