diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 3054223e7..1814f9504 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -31,6 +31,12 @@ "generic": { "env": { "parse": "Could not properly parse the env var string." + }, + "fs": { + "write-file": "Could not write file \"{filepath}\".", + "flatpak": { + "write-file": "Could not write file \"{filepath}\". Check if the parent folder is writable in your flatpak permissions." + } } }, "title-bar": { diff --git a/src/__tests__/unit/fs.helpers.test.ts b/src/__tests__/unit/fs.helpers.test.ts index 906e353a9..adcb33f3d 100644 --- a/src/__tests__/unit/fs.helpers.test.ts +++ b/src/__tests__/unit/fs.helpers.test.ts @@ -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", () => { diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index 5692d32f5..9e46609bf 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -1,4 +1,4 @@ -import { CopyOptions, MoveOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, stat, symlink } from "fs-extra"; +import fs, { 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 path from "path"; import { Observable, concatMap, from } from "rxjs"; @@ -9,6 +9,9 @@ import { execSync } from "child_process"; import { tryit } from "../../shared/helpers/error.helpers"; import { CustomError } from "shared/models/exceptions/custom-error.class"; import { ErrorObject } from "serialize-error"; +import { IS_FLATPAK } from "main/constants"; + +// NOTE: For future fs errors, add generic.fs. to properly show them as notification to the user export async function pathExist(path: string): Promise { try { @@ -318,6 +321,35 @@ export async function getSize(targetPath: string, maxDepth = 5): Promise return computeSize(targetPath, 0); } +// fs errors in flatpak may be a result of sandbox permissions, a separate error message is needed. +const createResourceKey = (prefix: string, name: string) => IS_FLATPAK + ? `${prefix}.flatpak.${name}` + : `${prefix}.${name}`; + +export const writeFile = async ( + filepath: number | fs.PathLike, + data: any, + options?: string | fs.WriteFileOptions +) => fs.writeFile(filepath, data, options).catch((error: Error) => { + throw CustomError.fromError(error, createResourceKey("generic.fs", "write-file"), { + params: { filepath } + }); +}); + +export function writeFileSync( + filepath: number | fs.PathLike, + data: any, + options?: string | fs.WriteFileOptions +) { + try { + fs.writeFile(filepath, data, options) + } catch (error: any) { + throw CustomError.fromError(error, createResourceKey("generic.fs", "write-file"), { + params: { filepath } + }); + } +} + export interface Progression { total: number; current: number; diff --git a/src/main/models/json-cache.class.ts b/src/main/models/json-cache.class.ts index 7c0652ba9..cff254bd9 100644 --- a/src/main/models/json-cache.class.ts +++ b/src/main/models/json-cache.class.ts @@ -1,4 +1,5 @@ -import { pathExistsSync, readFileSync, writeFileSync } from "fs-extra"; +import { pathExistsSync, readFileSync } from "fs-extra"; +import { writeFileSync } from "../helpers/fs.helpers"; import { tryit } from "shared/helpers/error.helpers"; import log from "electron-log"; import { Subject, debounceTime } from "rxjs"; diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index 4de1b4a15..165c1d543 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -10,8 +10,8 @@ import { WindowManagerService } from "../window-manager.service"; import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/models/playlists/playlist.interface"; 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 { copy, ensureDir, pathExists, pathExistsSync, realpath } from "fs-extra"; +import { Progression, getUniqueFileNamePath, unlinkPath, writeFileSync } 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"; diff --git a/src/main/services/additional-content/maps/song-details-cache.service.ts b/src/main/services/additional-content/maps/song-details-cache.service.ts index 4412ae176..5035eeb2b 100644 --- a/src/main/services/additional-content/maps/song-details-cache.service.ts +++ b/src/main/services/additional-content/maps/song-details-cache.service.ts @@ -1,5 +1,6 @@ import path from "path"; -import { ensureDirSync, existsSync, readFile, writeFile } from "fs-extra"; +import { ensureDirSync, existsSync, readFile } from "fs-extra"; +import { writeFile } from "../../../helpers/fs.helpers"; import { BehaviorSubject, Observable, catchError, filter, lastValueFrom, of, take, timeout } from "rxjs"; import { RequestService } from "../../request.service"; import { tryit } from "shared/helpers/error.helpers"; diff --git a/src/main/services/bs-launcher/bs-launcher.service.ts b/src/main/services/bs-launcher/bs-launcher.service.ts index 66e4e9e51..e3c240ab2 100644 --- a/src/main/services/bs-launcher/bs-launcher.service.ts +++ b/src/main/services/bs-launcher/bs-launcher.service.ts @@ -7,7 +7,8 @@ import { Observable, of, throwError } from "rxjs"; import { BsmProtocolService } from "../bsm-protocol.service"; import { app, shell} from "electron"; import Color from "color"; -import { ensureDir, writeFile } from "fs-extra"; +import { ensureDir } from "fs-extra"; +import { writeFile } from "../../helpers/fs.helpers"; import toIco from "to-ico"; import { objectFromEntries } from "../../../shared/helpers/object.helpers"; import { WindowManagerService } from "../window-manager.service"; diff --git a/src/main/services/bs-local-version.service.ts b/src/main/services/bs-local-version.service.ts index 14d31dde3..ad13b2be7 100644 --- a/src/main/services/bs-local-version.service.ts +++ b/src/main/services/bs-local-version.service.ts @@ -9,9 +9,9 @@ import { lstat, rename } from "fs/promises"; import log from "electron-log"; import { OculusService } from "./oculus.service"; import sanitize from "sanitize-filename"; -import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, rxCopy } from "../helpers/fs.helpers"; +import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, rxCopy, writeFile } from "../helpers/fs.helpers"; import { FolderLinkerService } from "./folder-linker.service"; -import { ReadStream, createReadStream, pathExists, pathExistsSync, readFile, writeFile } from "fs-extra"; +import { ReadStream, createReadStream, pathExists, pathExistsSync, readFile } from "fs-extra"; import readline from "readline"; import { Observable, Subject, catchError, finalize, from, map, switchMap, throwError } from "rxjs"; import { BsStore } from "../../shared/models/bs-store.enum"; diff --git a/src/main/services/bs-version-lib.service.ts b/src/main/services/bs-version-lib.service.ts index b90582e17..2c74cbb1e 100644 --- a/src/main/services/bs-version-lib.service.ts +++ b/src/main/services/bs-version-lib.service.ts @@ -1,6 +1,6 @@ import { UtilsService } from "./utils.service"; import path from "path"; -import { writeFileSync } from "fs"; +import { writeFileSync } from "../helpers/fs.helpers"; import { BSVersion } from "shared/bs-version.interface"; import { RequestService } from "./request.service"; import { readJSON } from "fs-extra"; diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index 5f12896a1..7b1741e84 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -1,4 +1,5 @@ -import fs from "fs-extra"; +import fsExtra from "fs-extra"; +import { writeFile } from "../helpers/fs.helpers"; import log from "electron-log"; import path from "path"; import { BS_APP_ID, BS_EXECUTABLE, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants"; @@ -71,7 +72,7 @@ export class LinuxService { this.staticConfig.get("proton-folder"), PROTON_BINARY_PREFIX ); - if (!fs.pathExistsSync(protonPath)) { + if (!fsExtra.pathExistsSync(protonPath)) { throw CustomError.fromError( new Error("Could not locate proton binary"), BSLaunchError.PROTON_NOT_FOUND @@ -91,9 +92,9 @@ export class LinuxService { // using bsmanager, it won't exist, and proton will fail // to launch the game. const compatDataPath = this.getCompatDataPath(); - if (!fs.existsSync(compatDataPath)) { + if (!fsExtra.existsSync(compatDataPath)) { log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`); - await fs.ensureDir(compatDataPath); + await fsExtra.ensureDir(compatDataPath); } // Setup Proton environment variables @@ -126,7 +127,7 @@ export class LinuxService { const protonPath = path.join(protonFolder, PROTON_BINARY_PREFIX); const winePath = path.join(protonFolder, WINE_BINARY_PREFIX); - return fs.pathExistsSync(protonPath) && fs.pathExistsSync(winePath); + return fsExtra.pathExistsSync(protonPath) && fsExtra.pathExistsSync(winePath); } public getWinePath(): string { @@ -138,7 +139,7 @@ export class LinuxService { this.staticConfig.get("proton-folder"), WINE_BINARY_PREFIX ); - if (!fs.pathExistsSync(winePath)) { + if (!fsExtra.pathExistsSync(winePath)) { throw new Error(`"${winePath}" binary file not found`); } @@ -147,7 +148,7 @@ export class LinuxService { public getWinePrefixPath(): string { const compatDataPath = this.getCompatDataPath(); - return fs.existsSync(compatDataPath) + return fsExtra.existsSync(compatDataPath) ? path.join(compatDataPath, "pfx") : ""; } @@ -221,12 +222,12 @@ export class LinuxService { `Exec=${command}` ].join("\n"); - await fs.writeFile(shortcutPath, desktopEntry); + await writeFile(shortcutPath, desktopEntry); log.info("Created shorcut at ", `"${shortcutPath}/${name}"`); return true; - } catch (error) { + } catch (error: any) { log.error("Could not create shortcut", error); - return false; + throw CustomError.fromError(error); } } diff --git a/src/main/services/steam.service.ts b/src/main/services/steam.service.ts index 54414ff58..8ed20f8ff 100644 --- a/src/main/services/steam.service.ts +++ b/src/main/services/steam.service.ts @@ -1,13 +1,13 @@ import { RegDwordValue } from "regedit-rs" import path from "path"; import { parse } from "@node-steam/vdf"; -import { readFile } from "fs/promises"; import log from "electron-log"; import { app, shell } from "electron"; import { getProcessId, isProcessRunning } from "main/helpers/os.helpers"; import { isElevated } from "query-process"; import { execOnOs } from "../helpers/env.helpers"; -import { pathExists, pathExistsSync, readdir, writeFile } from "fs-extra"; +import { pathExists, pathExistsSync, readdir, readFile } from "fs-extra"; +import { writeFile } from "../helpers/fs.helpers"; import { SteamShortcut, SteamShortcutData } from "../../shared/models/steam/shortcut.model"; const { list } = (execOnOs({ win32: () => require("regedit-rs") }, true) ?? {}) as typeof import("regedit-rs"); diff --git a/src/main/services/supporters.service.ts b/src/main/services/supporters.service.ts index fc9be5adc..3344ad2d5 100644 --- a/src/main/services/supporters.service.ts +++ b/src/main/services/supporters.service.ts @@ -1,4 +1,4 @@ -import { writeFileSync } from "fs"; +import { writeFile } from "../helpers/fs.helpers"; import { readJSON } from "fs-extra"; import path from "path"; import { Supporter } from "shared/models/supporters/supporter.interface"; @@ -31,10 +31,10 @@ export class SupportersService { private async updateLocalSupporters(supporters: Supporter[]): Promise { const patreonsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.PATREONS_FILE); - writeFileSync(patreonsPath, JSON.stringify(supporters, null, "\t"), { encoding: "utf-8", flag: "w" }); + await writeFile(patreonsPath, JSON.stringify(supporters, null, "\t"), { encoding: "utf-8", flag: "w" }); } - private getRemoteSupporters(): Promise { + private async getRemoteSupporters(): Promise { return this.requestService.getJSON(this.PATREONS_URL).then(res => res.data); } diff --git a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx index 961380526..bb360123f 100644 --- a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx @@ -105,7 +105,10 @@ export const LocalPlaylistsListPanel = forwardRef( if(error){ logRenderError("Error occured while creating playlist", error); - notification.notifyError({ title: "playlist.error-playlist-creation-title", desc: "playlist.error-playlist-creation-desc" }); + notification.notifyError({ + title: "playlist.error-playlist-creation-title", + desc: "playlist.error-playlist-creation-desc", + }, error); return; } @@ -346,7 +349,10 @@ export const LocalPlaylistsListPanel = forwardRef( if(error){ logRenderError("Error occured while editing playlist", error); - notification.notifyError({ title: "playlist.playlist-edit-error-title", desc: "playlist.playlist-edit-error-desc" }); + notification.notifyError({ + title: "playlist.playlist-edit-error-title", + desc: "playlist.playlist-edit-error-desc" + }, error); return; } diff --git a/src/renderer/components/notification/notification-item.component.tsx b/src/renderer/components/notification/notification-item.component.tsx index a8342fd85..26313bec7 100644 --- a/src/renderer/components/notification/notification-item.component.tsx +++ b/src/renderer/components/notification/notification-item.component.tsx @@ -6,11 +6,11 @@ import BeatConflictImg from "../../../../assets/images/apngs/beat-conflict.png"; import BeatWaitingImg from "../../../../assets/images/apngs/beat-waiting.png"; import BeatImpatientImg from "../../../../assets/images/apngs/beat-impatient.png"; import { BsmButton } from "../shared/bsm-button.component"; -import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { useTranslationV2 } from "renderer/hooks/use-translation.hook"; import { ForwardedRef, forwardRef } from "react"; export const NotificationItem = forwardRef(({ resolver, notification }: { resolver?: (value: NotificationResult | string) => void; notification: Notification }, fwdRef: ForwardedRef) => { - const t = useTranslation(); + const { text: t } = useTranslationV2(); const renderImage = (() => { if (notification.type === NotificationType.SUCCESS) { @@ -44,7 +44,7 @@ export const NotificationItem = forwardRef(({ resolver, notification }: { resolv return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white"; })(); - const handleDragEnd = (e: MouseEvent, info: PanInfo) => { + const handleDragEnd = (_e: MouseEvent, info: PanInfo) => { const offset = info.offset.x; const velocity = info.velocity.x; diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index 601e3063b..e44407071 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -169,11 +169,11 @@ export function VersionViewer() { title: "notifications.create-launch-shortcut.success.title", desc: `notifications.create-launch-shortcut.success.${data.steamShortcut ? "msg-steam" : "msg"}` }); - }).catch(() => { + }).catch(error => { notification.notifyError({ title: "notifications.types.error", - desc: "notifications.create-launch-shortcut.error.msg" - }); + desc: "notifications.create-launch-shortcut.error.msg", + }, error); }); } diff --git a/src/renderer/services/notification.service.ts b/src/renderer/services/notification.service.ts index 30a414bca..a9bb33913 100644 --- a/src/renderer/services/notification.service.ts +++ b/src/renderer/services/notification.service.ts @@ -33,13 +33,16 @@ export class NotificationService { promise.then(() => { this.notifications$.next(this.notifications$.value.filter(n => n.id !== resovableNotification.id)); - }); + }); return promise; } - public notifyError(notification: Notification): Promise { + public notifyError(notification: Notification, error?: Error): Promise { notification.type = NotificationType.ERROR; + if ((error as any)?.code) { + notification.desc = (error as any).code; + } return this.notify(notification); }