Files
hydra/src/main/services/game-files-manager.ts

208 lines
5.4 KiB
TypeScript

import path from "node:path";
import fs from "node:fs";
import type { GameShop } from "@types";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
import { SevenZip, ExtractionProgress } from "./7zip";
import { WindowManager } from "./window-manager";
import { publishExtractionCompleteNotification } from "./notifications";
import { logger } from "./logger";
const PROGRESS_THROTTLE_MS = 1000;
export class GameFilesManager {
private lastProgressUpdate = 0;
constructor(
private readonly shop: GameShop,
private readonly objectId: string
) {}
private get gameKey() {
return levelKeys.game(this.shop, this.objectId);
}
private async updateExtractionProgress(progress: number, force = false) {
const now = Date.now();
if (!force && now - this.lastProgressUpdate < PROGRESS_THROTTLE_MS) {
return;
}
this.lastProgressUpdate = now;
const download = await downloadsSublevel.get(this.gameKey);
if (!download) return;
await downloadsSublevel.put(this.gameKey, {
...download,
extractionProgress: progress,
});
WindowManager.mainWindow?.webContents.send(
"on-extraction-progress",
this.shop,
this.objectId,
progress
);
}
private async clearExtractionState() {
const download = await downloadsSublevel.get(this.gameKey);
if (!download) return;
await downloadsSublevel.put(this.gameKey, {
...download,
extracting: false,
extractionProgress: 0,
});
WindowManager.mainWindow?.webContents.send(
"on-extraction-complete",
this.shop,
this.objectId
);
}
private readonly handleProgress = (progress: ExtractionProgress) => {
this.updateExtractionProgress(progress.percent / 100);
};
async extractFilesInDirectory(directoryPath: string) {
if (!fs.existsSync(directoryPath)) return;
const files = await fs.promises.readdir(directoryPath);
const compressedFiles = files.filter((file) =>
FILE_EXTENSIONS_TO_EXTRACT.some((ext) => file.endsWith(ext))
);
const filesToExtract = compressedFiles.filter(
(file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file)
);
if (filesToExtract.length === 0) return;
await this.updateExtractionProgress(0, true);
const totalFiles = filesToExtract.length;
let completedFiles = 0;
for (const file of filesToExtract) {
try {
const result = await SevenZip.extractFile(
{
filePath: path.join(directoryPath, file),
cwd: directoryPath,
passwords: ["online-fix.me", "steamrip.com"],
},
(progress) => {
const overallProgress =
(completedFiles + progress.percent / 100) / totalFiles;
this.updateExtractionProgress(overallProgress);
}
);
if (result.success) {
completedFiles++;
await this.updateExtractionProgress(
completedFiles / totalFiles,
true
);
}
} catch (err) {
logger.error(`Failed to extract file: ${file}`, err);
await this.clearExtractionState();
return;
}
}
const archivePaths = compressedFiles
.map((file) => path.join(directoryPath, file))
.filter((archivePath) => fs.existsSync(archivePath));
if (archivePaths.length > 0) {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
archivePaths
);
}
}
async setExtractionComplete(publishNotification = true) {
const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey),
gamesSublevel.get(this.gameKey),
]);
if (!download) return;
await downloadsSublevel.put(this.gameKey, {
...download,
extracting: false,
extractionProgress: 0,
});
WindowManager.mainWindow?.webContents.send(
"on-extraction-complete",
this.shop,
this.objectId
);
if (publishNotification && game) {
publishExtractionCompleteNotification(game);
}
}
async extractDownloadedFile() {
const [download, game] = await Promise.all([
downloadsSublevel.get(this.gameKey),
gamesSublevel.get(this.gameKey),
]);
if (!download || !game) return false;
const filePath = path.join(download.downloadPath, download.folderName!);
const extractionPath = path.join(
download.downloadPath,
path.parse(download.folderName!).name
);
await this.updateExtractionProgress(0, true);
try {
const result = await SevenZip.extractFile(
{
filePath,
outputPath: extractionPath,
passwords: ["online-fix.me", "steamrip.com"],
},
this.handleProgress
);
if (result.success) {
await this.extractFilesInDirectory(extractionPath);
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
WindowManager.mainWindow?.webContents.send(
"on-archive-deletion-prompt",
[filePath]
);
}
await downloadsSublevel.put(this.gameKey, {
...download,
folderName: path.parse(download.folderName!).name,
});
await this.setExtractionComplete();
}
} catch (err) {
logger.error(`Failed to extract downloaded file: ${filePath}`, err);
await this.clearExtractionState();
}
return true;
}
}