diff --git a/src/main/events/library/extract-game-download.ts b/src/main/events/library/extract-game-download.ts new file mode 100644 index 00000000..bcc7052c --- /dev/null +++ b/src/main/events/library/extract-game-download.ts @@ -0,0 +1,46 @@ +import { registerEvent } from "../register-event"; +import { GameShop } from "@types"; +import path from "node:path"; +import { _7Zip, GameFilesManager } from "@main/services"; +import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level"; +import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared"; + +const extractGameDownload = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string +): Promise => { + const gameKey = levelKeys.game(shop, objectId); + + const [download, game] = await Promise.all([ + downloadsSublevel.get(gameKey), + gamesSublevel.get(gameKey), + ]); + + if (!download || !game) return false; + + await downloadsSublevel.put(gameKey, { + ...download, + extracting: true, + }); + + const gameFilesManager = new GameFilesManager(shop, objectId); + + if ( + FILE_EXTENSIONS_TO_EXTRACT.some((ext) => download.folderName?.endsWith(ext)) + ) { + gameFilesManager.extractDownloadedFile(); + } else { + gameFilesManager + .extractFilesInDirectory( + path.join(download.downloadPath, download.folderName!) + ) + .then(() => { + gameFilesManager.setExtractionComplete(false); + }); + } + + return true; +}; + +registerEvent("extractGameDownload", extractGameDownload); diff --git a/src/main/services/game-files-manager.ts b/src/main/services/game-files-manager.ts new file mode 100644 index 00000000..fb9c82ff --- /dev/null +++ b/src/main/services/game-files-manager.ts @@ -0,0 +1,159 @@ +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 { _7Zip } from "./7zip"; +import { WindowManager } from "./window-manager"; +import { publishExtractionCompleteNotification } from "./notifications"; +import { logger } from "./logger"; + +export class GameFilesManager { + constructor( + private readonly shop: GameShop, + private readonly objectId: string + ) {} + + private async clearExtractionState() { + const gameKey = levelKeys.game(this.shop, this.objectId); + const download = await downloadsSublevel.get(gameKey); + + await downloadsSublevel.put(gameKey, { + ...download!, + extracting: false, + }); + + WindowManager.mainWindow?.webContents.send( + "on-extraction-complete", + this.shop, + this.objectId + ); + } + + 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) + ); + + await Promise.all( + filesToExtract.map((file) => { + return new Promise((resolve, reject) => { + _7Zip.extractFile( + { + filePath: path.join(directoryPath, file), + cwd: directoryPath, + passwords: ["online-fix.me", "steamrip.com"], + }, + async (success) => { + if (success) { + resolve(true); + } else { + reject(new Error(`Failed to extract file: ${file}`)); + this.clearExtractionState(); + } + } + ); + }); + }) + ); + + compressedFiles.forEach((file) => { + const extractionPath = path.join(directoryPath, file); + + if (fs.existsSync(extractionPath)) { + fs.unlink(extractionPath, (err) => { + if (err) { + logger.error(`Failed to delete file: ${file}`, err); + + this.clearExtractionState(); + } + }); + } + }); + } + + async setExtractionComplete(publishNotification = true) { + const gameKey = levelKeys.game(this.shop, this.objectId); + + const [download, game] = await Promise.all([ + downloadsSublevel.get(gameKey), + gamesSublevel.get(gameKey), + ]); + + await downloadsSublevel.put(gameKey, { + ...download!, + extracting: false, + }); + + WindowManager.mainWindow?.webContents.send( + "on-extraction-complete", + this.shop, + this.objectId + ); + + if (publishNotification) { + publishExtractionCompleteNotification(game!); + } + } + + async extractDownloadedFile() { + const gameKey = levelKeys.game(this.shop, this.objectId); + + const [download, game] = await Promise.all([ + downloadsSublevel.get(gameKey), + gamesSublevel.get(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 + ); + + _7Zip.extractFile( + { + filePath, + outputPath: extractionPath, + }, + async (success) => { + if (success) { + await this.extractFilesInDirectory(extractionPath); + + if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) { + fs.unlink(filePath, (err) => { + if (err) { + logger.error( + `Failed to delete file: ${download.folderName}`, + err + ); + + this.clearExtractionState(); + } + }); + } + + await downloadsSublevel.put(gameKey, { + ...download!, + folderName: path.parse(download.folderName!).name, + }); + + this.setExtractionComplete(); + } else { + this.clearExtractionState(); + } + } + ); + + return true; + } +}