Skip to content

Commit

Permalink
refactor: vvppManager.extractVvppの処理を関数切り出し (#2478)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hiroshiba authored Jan 7, 2025
1 parent df4ad34 commit 10ce8de
Showing 1 changed file with 135 additions and 106 deletions.
241 changes: 135 additions & 106 deletions src/backend/electron/manager/vvppManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,136 @@ export const isVvppFile = (filePath: string) => {

const lockKey = "lock-key-for-vvpp-manager";

/** VVPPファイルが分割されている場合、それらのファイルを取得する */
async function getArchiveFileParts(
vvppLikeFilePath: string,
): Promise<string[]> {
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<void>((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<void>((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(
/ *(?<percent>\d+)% ?(?<fileCount>\d+)? ?(?<file>.*)/,
);
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"
Expand Down Expand Up @@ -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) {
Expand All @@ -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<void>((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<void>((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(
/ *(?<percent>\d+)% ?(?<fileCount>\d+)? ?(?<file>.*)/,
);
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);
Expand Down

0 comments on commit 10ce8de

Please sign in to comment.