mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-28 13:21:02 +00:00
feat: adding cross cloud save
This commit is contained in:
@@ -7,7 +7,7 @@ import os from "node:os";
|
||||
import type { GameShop, User } from "@types";
|
||||
import { backupsPath } from "@main/constants";
|
||||
import { HydraApi } from "./hydra-api";
|
||||
import { normalizePath } from "@main/helpers";
|
||||
import { normalizePath, parseRegFile } from "@main/helpers";
|
||||
import { logger } from "./logger";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import axios from "axios";
|
||||
@@ -17,6 +17,53 @@ import i18next, { t } from "i18next";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export class CloudSync {
|
||||
public static getProfilePaths(winePrefixPath?: string | null) {
|
||||
const currentHomeDir = normalizePath(SystemPath.getPath("home"));
|
||||
|
||||
if (process.platform === "linux") {
|
||||
if (!winePrefixPath) {
|
||||
throw new Error("Wine prefix path is required");
|
||||
}
|
||||
|
||||
const userReg = fs.readFileSync(
|
||||
path.join(winePrefixPath, "user.reg"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const entries = parseRegFile(userReg);
|
||||
const volatileEnvironment = entries.find(
|
||||
(entry) => entry.path === "Volatile Environment"
|
||||
);
|
||||
|
||||
if (!volatileEnvironment) {
|
||||
throw new Error("Volatile environment not found in user.reg");
|
||||
}
|
||||
|
||||
const { values } = volatileEnvironment;
|
||||
const userProfile = String(values["USERPROFILE"]);
|
||||
|
||||
if (userProfile) {
|
||||
return {
|
||||
userProfilePath: path.join(
|
||||
winePrefixPath,
|
||||
normalizePath(userProfile.replace("C:", "drive_c"))
|
||||
),
|
||||
publicProfilePath: path.join(
|
||||
winePrefixPath,
|
||||
"drive_c",
|
||||
"users",
|
||||
"Public"
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
userProfilePath: currentHomeDir,
|
||||
publicProfilePath: path.join("C:", "Users", "Public"),
|
||||
};
|
||||
}
|
||||
|
||||
public static getBackupLabel(automatic: boolean) {
|
||||
const language = i18next.language;
|
||||
|
||||
@@ -102,7 +149,9 @@ export class CloudSync {
|
||||
shop,
|
||||
objectId,
|
||||
hostname: os.hostname(),
|
||||
homeDir: normalizePath(SystemPath.getPath("home")),
|
||||
winePrefixPath: game?.winePrefixPath ?? null,
|
||||
homeDir: this.getProfilePaths(game?.winePrefixPath ?? null)
|
||||
.userProfilePath,
|
||||
downloadOptionTitle,
|
||||
platform: os.platform(),
|
||||
label,
|
||||
|
||||
@@ -15,3 +15,4 @@ export * from "./aria2";
|
||||
export * from "./ws";
|
||||
export * from "./system-path";
|
||||
export * from "./library-sync";
|
||||
export * from "./wine";
|
||||
|
||||
@@ -1,70 +1,83 @@
|
||||
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
|
||||
import Piscina from "piscina";
|
||||
|
||||
import { app } from "electron";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import YAML from "yaml";
|
||||
|
||||
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
|
||||
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
|
||||
import cp from "node:child_process";
|
||||
import { SystemPath } from "./system-path";
|
||||
|
||||
export class Ludusavi {
|
||||
private static ludusaviPath = path.join(
|
||||
SystemPath.getPath("appData"),
|
||||
"ludusavi"
|
||||
);
|
||||
private static ludusaviConfigPath = path.join(
|
||||
this.ludusaviPath,
|
||||
private static ludusaviPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "ludusavi")
|
||||
: path.join(__dirname, "..", "..", "ludusavi");
|
||||
|
||||
private static binaryPath = path.join(this.ludusaviPath, "ludusavi");
|
||||
private static configPath = path.join(
|
||||
SystemPath.getPath("userData"),
|
||||
"config.yaml"
|
||||
);
|
||||
private static binaryPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, "ludusavi", "ludusavi")
|
||||
: path.join(__dirname, "..", "..", "ludusavi", "ludusavi");
|
||||
|
||||
private static worker = new Piscina({
|
||||
filename: ludusaviWorkerPath,
|
||||
workerData: {
|
||||
binaryPath: this.binaryPath,
|
||||
},
|
||||
maxThreads: 1,
|
||||
});
|
||||
|
||||
static async getConfig() {
|
||||
if (!fs.existsSync(this.ludusaviConfigPath)) {
|
||||
await this.worker.run(undefined, { name: "generateConfig" });
|
||||
}
|
||||
|
||||
public static async getConfig() {
|
||||
const config = YAML.parse(
|
||||
fs.readFileSync(this.ludusaviConfigPath, "utf-8")
|
||||
fs.readFileSync(this.configPath, "utf-8")
|
||||
) as LudusaviConfig;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
static async backupGame(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
backupPath: string,
|
||||
winePrefix?: string | null
|
||||
): Promise<LudusaviBackup> {
|
||||
return this.worker.run(
|
||||
{ title: objectId, backupPath, winePrefix },
|
||||
{ name: "backupGame" }
|
||||
);
|
||||
public static async copyConfigFileToUserData() {
|
||||
fs.cpSync(path.join(this.ludusaviPath, "config.yaml"), this.configPath);
|
||||
}
|
||||
|
||||
static async getBackupPreview(
|
||||
public static async backupGame(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
backupPath?: string | null,
|
||||
winePrefix?: string | null,
|
||||
preview?: boolean
|
||||
): Promise<LudusaviBackup> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = [
|
||||
"--config",
|
||||
this.ludusaviPath,
|
||||
"backup",
|
||||
objectId,
|
||||
"--api",
|
||||
"--force",
|
||||
];
|
||||
|
||||
if (preview) args.push("--preview");
|
||||
if (backupPath) args.push("--path", backupPath);
|
||||
if (winePrefix) args.push("--wine-prefix", winePrefix);
|
||||
|
||||
cp.execFile(
|
||||
this.binaryPath,
|
||||
args,
|
||||
(err: cp.ExecFileException | null, stdout: string) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
return resolve(JSON.parse(stdout) as LudusaviBackup);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public static async getBackupPreview(
|
||||
_shop: GameShop,
|
||||
objectId: string,
|
||||
winePrefix?: string | null
|
||||
): Promise<LudusaviBackup | null> {
|
||||
const config = await this.getConfig();
|
||||
|
||||
const backupData = await this.worker.run(
|
||||
{ title: objectId, winePrefix, preview: true },
|
||||
{ name: "backupGame" }
|
||||
const backupData = await this.backupGame(
|
||||
_shop,
|
||||
objectId,
|
||||
null,
|
||||
winePrefix,
|
||||
true
|
||||
);
|
||||
|
||||
const customGame = config.customGames.find(
|
||||
@@ -77,19 +90,6 @@ export class Ludusavi {
|
||||
};
|
||||
}
|
||||
|
||||
static async restoreBackup(backupPath: string) {
|
||||
return this.worker.run(backupPath, { name: "restoreBackup" });
|
||||
}
|
||||
|
||||
static async addManifestToLudusaviConfig() {
|
||||
const config = await this.getConfig();
|
||||
|
||||
config.manifest.enable = false;
|
||||
config.manifest.secondary = [{ url: LUDUSAVI_MANIFEST_URL, enable: true }];
|
||||
|
||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||
}
|
||||
|
||||
static async addCustomGame(title: string, savePath: string | null) {
|
||||
const config = await this.getConfig();
|
||||
const filteredGames = config.customGames.filter(
|
||||
@@ -105,6 +105,10 @@ export class Ludusavi {
|
||||
}
|
||||
|
||||
config.customGames = filteredGames;
|
||||
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(this.ludusaviPath, "config.yaml"),
|
||||
YAML.stringify(config)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
23
src/main/services/wine.ts
Normal file
23
src/main/services/wine.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
export class Wine {
|
||||
public static validatePrefix(winePrefixPath: string) {
|
||||
const requiredFiles = [
|
||||
"system.reg",
|
||||
"user.reg",
|
||||
"userdef.reg",
|
||||
"dosdevices",
|
||||
"drive_c",
|
||||
];
|
||||
|
||||
for (const file of requiredFiles) {
|
||||
const filePath = path.join(winePrefixPath, file);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user