From 10ce8dec6f7c9427c6519f241f926189997af13d Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Wed, 8 Jan 2025 01:21:58 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20vvppManager.extractVvpp=E3=81=AE?= =?UTF-8?q?=E5=87=A6=E7=90=86=E3=82=92=E9=96=A2=E6=95=B0=E5=88=87=E3=82=8A?= =?UTF-8?q?=E5=87=BA=E3=81=97=20(#2478)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/electron/manager/vvppManager.ts | 241 +++++++++++--------- 1 file changed, 135 insertions(+), 106 deletions(-) diff --git a/src/backend/electron/manager/vvppManager.ts b/src/backend/electron/manager/vvppManager.ts index 9484a8476e..adaac308bc 100644 --- a/src/backend/electron/manager/vvppManager.ts +++ b/src/backend/electron/manager/vvppManager.ts @@ -33,6 +33,136 @@ export const isVvppFile = (filePath: string) => { const lockKey = "lock-key-for-vvpp-manager"; +/** VVPPファイルが分割されている場合、それらのファイルを取得する */ +async function getArchiveFileParts( + vvppLikeFilePath: string, +): Promise { + let archiveFileParts: string[]; + // 名前.数値.vvpppの場合は分割されているとみなして連結する + if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) { + log.log("vvpp is split, finding other parts..."); + const vvpppPathGlob = vvppLikeFilePath + .replace(/\.[0-9]+\.vvppp$/, ".*.vvppp") + .replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する + const filePaths: string[] = []; + for (const p of await glob(vvpppPathGlob)) { + if (!p.match(/\.[0-9]+\.vvppp$/)) { + continue; + } + log.log(`found ${p}`); + filePaths.push(p); + } + filePaths.sort((a, b) => { + const aMatch = a.match(/\.([0-9]+)\.vvppp$/); + const bMatch = b.match(/\.([0-9]+)\.vvppp$/); + if (aMatch == null || bMatch == null) { + throw new Error(`match is null: a=${a}, b=${b}`); + } + return parseInt(aMatch[1]) - parseInt(bMatch[1]); + }); + archiveFileParts = filePaths; + } else { + log.log("Not a split file"); + archiveFileParts = [vvppLikeFilePath]; + } + return archiveFileParts; +} + +/** 分割されているVVPPファイルを連結して返す */ +async function concatenateVvppFiles( + format: "zip" | "7z", + archiveFileParts: string[], +) { + // -siオプションでの7z解凍はサポートされていないため、 + // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 + log.log(`Concatenating ${archiveFileParts.length} files...`); + const tmpConcatenatedFile = path.join( + app.getPath("temp"), + `vvpp-${new Date().getTime()}.${format}`, + ); + log.log("Temporary file:", tmpConcatenatedFile); + await new Promise((resolve, reject) => { + if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined"); + const inputStreams = archiveFileParts.map((f) => fs.createReadStream(f)); + const outputStream = fs.createWriteStream(tmpConcatenatedFile); + new MultiStream(inputStreams) + .pipe(outputStream) + .on("close", () => { + outputStream.close(); + resolve(); + }) + .on("error", reject); + }); + log.log("Concatenated"); + return tmpConcatenatedFile; +} + +/** 7zでファイルを解凍する */ +async function unarchive( + payload: { + archiveFile: string; + outputDir: string; + format: "zip" | "7z"; + }, + callbacks?: { onProgress?: ProgressCallback }, +) { + const { archiveFile, outputDir, format } = payload; + + const args = [ + "x", + "-o" + outputDir, + archiveFile, + "-t" + format, + "-bsp1", // 進捗出力 + ]; + + let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; + if (!sevenZipPath) { + throw new Error("7z path is not defined"); + } + if (import.meta.env.PROD) { + sevenZipPath = path.join(path.dirname(app.getPath("exe")), sevenZipPath); + } + log.log("Spawning 7z:", sevenZipPath, args.join(" ")); + await new Promise((resolve, reject) => { + const child = spawn(sevenZipPath, args, { + stdio: ["pipe", "pipe", "pipe"], + }); + + child.stdout?.on("data", (data: Buffer) => { + const output = data.toString("utf-8"); + log.info(`7z STDOUT: ${output}`); + + // 進捗を取得 + // NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る + // TODO: 出力が変わるかもしれないのでテストが必要 + const progressMatch = output.match( + / *(?\d+)% ?(?\d+)? ?(?.*)/, + ); + if (progressMatch?.groups?.percent) { + callbacks?.onProgress?.({ + progress: parseInt(progressMatch.groups.percent), + }); + } + }); + + child.stderr?.on("data", (data: Buffer) => { + log.error(`7z STDERR: ${data.toString("utf-8")}`); + }); + + child.on("exit", (code) => { + if (code === 0) { + callbacks?.onProgress?.({ progress: 100 }); + resolve(); + } else { + reject(new Error(`7z exited with code ${code}`)); + } + }); + // FIXME: rejectが2回呼ばれることがある + child.on("error", reject); + }); +} + // # 軽い概要 // // フォルダ名:"エンジン名+UUID" @@ -117,34 +247,7 @@ export class VvppManager { const nonce = new Date().getTime().toString(); const outputDir = path.join(this.vvppEngineDir, ".tmp", nonce); - let archiveFileParts: string[]; - // 名前.数値.vvpppの場合は分割されているとみなして連結する - if (vvppLikeFilePath.match(/\.[0-9]+\.vvppp$/)) { - log.log("vvpp is split, finding other parts..."); - const vvpppPathGlob = vvppLikeFilePath - .replace(/\.[0-9]+\.vvppp$/, ".*.vvppp") - .replace(/\\/g, "/"); // node-globはバックスラッシュを使えないので、スラッシュに置換する - const filePaths: string[] = []; - for (const p of await glob(vvpppPathGlob)) { - if (!p.match(/\.[0-9]+\.vvppp$/)) { - continue; - } - log.log(`found ${p}`); - filePaths.push(p); - } - filePaths.sort((a, b) => { - const aMatch = a.match(/\.([0-9]+)\.vvppp$/); - const bMatch = b.match(/\.([0-9]+)\.vvppp$/); - if (aMatch == null || bMatch == null) { - throw new Error(`match is null: a=${a}, b=${b}`); - } - return parseInt(aMatch[1]) - parseInt(bMatch[1]); - }); - archiveFileParts = filePaths; - } else { - log.log("Not a split file"); - archiveFileParts = [vvppLikeFilePath]; - } + const archiveFileParts = await getArchiveFileParts(vvppLikeFilePath); const format = await this.detectFileFormat(archiveFileParts[0]); if (!format) { @@ -157,91 +260,17 @@ export class VvppManager { let archiveFile: string; try { if (archiveFileParts.length > 1) { - // -siオプションでの7z解凍はサポートされていないため、 - // ファイルを連結した一次ファイルを作成し、それを7zで解凍する。 - log.log(`Concatenating ${archiveFileParts.length} files...`); - tmpConcatenatedFile = path.join( - app.getPath("temp"), - `vvpp-${new Date().getTime()}.${format}`, + tmpConcatenatedFile = await concatenateVvppFiles( + format, + archiveFileParts, ); - log.log("Temporary file:", tmpConcatenatedFile); archiveFile = tmpConcatenatedFile; - await new Promise((resolve, reject) => { - if (!tmpConcatenatedFile) throw new Error("tmpFile is undefined"); - const inputStreams = archiveFileParts.map((f) => - fs.createReadStream(f), - ); - const outputStream = fs.createWriteStream(tmpConcatenatedFile); - new MultiStream(inputStreams) - .pipe(outputStream) - .on("close", () => { - outputStream.close(); - resolve(); - }) - .on("error", reject); - }); - log.log("Concatenated"); } else { archiveFile = archiveFileParts[0]; log.log("Single file, not concatenating"); } - const args = [ - "x", - "-o" + outputDir, - archiveFile, - "-t" + format, - "-bsp1", // 進捗出力 - ]; - - let sevenZipPath = import.meta.env.VITE_7Z_BIN_NAME; - if (!sevenZipPath) { - throw new Error("7z path is not defined"); - } - if (import.meta.env.PROD) { - sevenZipPath = path.join( - path.dirname(app.getPath("exe")), - sevenZipPath, - ); - } - log.log("Spawning 7z:", sevenZipPath, args.join(" ")); - await new Promise((resolve, reject) => { - const child = spawn(sevenZipPath, args, { - stdio: ["pipe", "pipe", "pipe"], - }); - - child.stdout?.on("data", (data: Buffer) => { - const output = data.toString("utf-8"); - log.info(`7z STDOUT: ${output}`); - - // 進捗を取得 - // NOTE: ` 75% 106 - pyopenjtalk\open_jtalk_dic_utf_8-1.11\sys.dic` のような出力が来る - // TODO: 出力が変わるかもしれないのでテストが必要 - const progressMatch = output.match( - / *(?\d+)% ?(?\d+)? ?(?.*)/, - ); - if (progressMatch?.groups?.percent) { - callbacks?.onProgress?.({ - progress: parseInt(progressMatch.groups.percent), - }); - } - }); - - child.stderr?.on("data", (data: Buffer) => { - log.error(`7z STDERR: ${data.toString("utf-8")}`); - }); - - child.on("exit", (code) => { - if (code === 0) { - callbacks?.onProgress?.({ progress: 100 }); - resolve(); - } else { - reject(new Error(`7z exited with code ${code}`)); - } - }); - // FIXME: rejectが2回呼ばれることがある - child.on("error", reject); - }); + await unarchive({ archiveFile, outputDir, format }, callbacks); } finally { if (tmpConcatenatedFile) { log.log("Removing temporary file", tmpConcatenatedFile);