import { getFoldersInFolder } from "../helpers/fs.helpers"; import path from "path"; import { VersionLinkerAction, VersionUnlinkFolderAction } from "renderer/services/version-folder-linker.service"; import { BSVersion } from "shared/bs-version.interface"; import { LocalMapsManagerService } from "./additional-content/maps/local-maps-manager.service"; import { BSLocalVersionService } from "./bs-local-version.service"; import { FolderLinkerService, LinkOptions } from "./folder-linker.service"; import { allSettled } from "../../shared/helpers/promise.helpers"; export class VersionFolderLinkerService { private static instance: VersionFolderLinkerService; public static getInstance(): VersionFolderLinkerService { if (!VersionFolderLinkerService.instance) { VersionFolderLinkerService.instance = new VersionFolderLinkerService(); } return VersionFolderLinkerService.instance; } private readonly localVersion: BSLocalVersionService; private readonly folderLinker: FolderLinkerService; private constructor() { this.localVersion = BSLocalVersionService.getInstance(); this.folderLinker = FolderLinkerService.getInstance(); } private specialFolderOption(relativeFolder: string, options: LinkOptions): LinkOptions { if (relativeFolder.includes(LocalMapsManagerService.CUSTOM_LEVELS_FOLDER)) { return { ...options, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER }; } if (relativeFolder.includes("UserData")) { return { ...options, backup: true }; } return options ?? {}; } private async isOtherVersionHaveFolderLinked(relativeFolder: string, ignorePath: string): Promise { const versions = await this.localVersion.getInstalledVersions(); const versionPaths = await allSettled(versions.map(version => this.localVersion.getVersionPath(version))); for (const versionPath of versionPaths) { const folderPath = this.relativeToFullPath(versionPath, relativeFolder); if (folderPath === ignorePath) { continue; } if (await this.folderLinker.isFolderSymlink(folderPath)) { return true; } } return false; } private relativeToFullPath(parentPath: string, relativePath: string): string { return path.join(parentPath, relativePath); } 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) } public async unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise { action.options = this.specialFolderOption(action.relativeFolder, action.options); const versionPath = await this.localVersion.getVersionPath(action.version); const folderPath = this.relativeToFullPath(versionPath, action.relativeFolder); action.options.moveContents = !(await this.isOtherVersionHaveFolderLinked(action.relativeFolder, folderPath)); return this.folderLinker.unlinkFolder(folderPath, action.options); } public doAction(action: VersionLinkerAction): Promise { if (action.type === "link") { return this.linkVersionFolder(action); } return this.unlinkVersionFolder(action as VersionUnlinkFolderAction); } public async isFolderLinked(version: BSVersion, relativeFolder: string): Promise { if (!version) { return Promise.reject("no version provided"); } const versionPath = await this.localVersion.getVersionPath(version); const folderPath = this.relativeToFullPath(versionPath, relativeFolder); return this.folderLinker.isFolderSymlink(folderPath); } public async getLinkedFolders(version: BSVersion, options?: { relative?: boolean; ignoreSymlinkTargetError?: boolean }): Promise { const versionPath = await this.localVersion.getVersionPath(version); const [rootFolders, beatSaberDataFolders] = await Promise.all([getFoldersInFolder(versionPath, { ignoreSymlinkTargetError: options?.ignoreSymlinkTargetError }), getFoldersInFolder(path.join(versionPath, "Beat Saber_Data"), { ignoreSymlinkTargetError: options?.ignoreSymlinkTargetError })]); const linkedFolders = await Promise.all( [...rootFolders, ...beatSaberDataFolders].map(async folder => { if (!(await this.folderLinker.isFolderSymlink(folder))) { return null; } return folder; }) ); if (options?.relative) { return linkedFolders.filter(folder => folder).map(folder => path.relative(versionPath, folder)); } return linkedFolders.filter(folder => folder); } public async relinkAllVersionsFolders(): Promise { const versions = await this.localVersion.getInstalledVersions(); 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 VersionLinkerAction)); await Promise.all(actions.map(action => this.doAction(action))); } } }