mirror of
https://github.com/HugoAura/Seewo-HugoAura.git
synced 2026-06-22 08:14:26 +08:00
[Feat] Download PLS from API
This commit is contained in:
186
src/aura/init/main/ipcModules/fsIpcHandler.js
Normal file
186
src/aura/init/main/ipcModules/fsIpcHandler.js
Normal file
@@ -0,0 +1,186 @@
|
||||
// @ts-check
|
||||
|
||||
const __SCOPE = "main";
|
||||
|
||||
const { exec } = require("child_process");
|
||||
const nodeHttp = require("http");
|
||||
const nodeHttps = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const { genRandomHex } = require("../../../utils/crypto");
|
||||
|
||||
const composableFunctions = {
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} targetPath
|
||||
* @param {((arg: DownloadTask) => any)} progressCallback
|
||||
*/
|
||||
downloadFile: async (url, targetPath, progressCallback) => {
|
||||
if (!progressCallback) return false;
|
||||
const taskId = genRandomHex();
|
||||
|
||||
/**
|
||||
* @type {DownloadTask}
|
||||
*/
|
||||
const failedTemplate = {
|
||||
id: taskId,
|
||||
progress: 100,
|
||||
status: "failed",
|
||||
dlUrl: url,
|
||||
savePath: targetPath,
|
||||
message: "",
|
||||
};
|
||||
|
||||
if (!url || !targetPath) {
|
||||
failedTemplate.message = "Invalid arg";
|
||||
progressCallback(failedTemplate);
|
||||
return false;
|
||||
}
|
||||
if (!fs.existsSync(path.dirname(targetPath))) {
|
||||
failedTemplate.message = "Path not exists";
|
||||
progressCallback(failedTemplate);
|
||||
}
|
||||
const httpModuleIns = url.startsWith("https") ? nodeHttps : nodeHttp;
|
||||
|
||||
global.__HUGO_AURA__.fsTasks?.downloadTasks.set(taskId, {
|
||||
status: "waiting",
|
||||
cancelReq: null,
|
||||
});
|
||||
|
||||
const fsStream = fs.createWriteStream(targetPath);
|
||||
|
||||
const dlReq = httpModuleIns.get(url, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
fsStream.close();
|
||||
failedTemplate.message = `Request error: HTTP ${response.statusCode}`;
|
||||
progressCallback(failedTemplate);
|
||||
return false;
|
||||
}
|
||||
|
||||
const contentLength = response.headers["content-length"];
|
||||
// @ts-expect-error
|
||||
const totalBytes = parseInt(contentLength, 10) || 0; // No error handling 😆
|
||||
let curRecvBytes = 0;
|
||||
|
||||
global.__HUGO_AURA__.fsTasks?.downloadTasks.set(taskId, {
|
||||
status: "progressing",
|
||||
cancelReq: () => {
|
||||
dlReq.destroy();
|
||||
fsStream.close();
|
||||
fs.unlink(targetPath, () => {});
|
||||
global.__HUGO_AURA__.fsTasks?.downloadTasks.delete(taskId);
|
||||
progressCallback({
|
||||
id: taskId,
|
||||
progress: 100,
|
||||
curBytes: curRecvBytes,
|
||||
totalBytes: totalBytes,
|
||||
status: "cancelled",
|
||||
dlUrl: url,
|
||||
savePath: targetPath,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
response.on("data", (chunk) => {
|
||||
curRecvBytes += chunk.length;
|
||||
const curProgress =
|
||||
totalBytes > 0 ? (curRecvBytes / totalBytes) * 100 : 0;
|
||||
progressCallback({
|
||||
id: taskId,
|
||||
progress: curProgress.toFixed(2),
|
||||
curBytes: curRecvBytes,
|
||||
totalBytes: totalBytes,
|
||||
status: "progressing",
|
||||
dlUrl: url,
|
||||
savePath: targetPath,
|
||||
});
|
||||
});
|
||||
|
||||
response.pipe(fsStream);
|
||||
|
||||
fsStream.on("finish", () => {
|
||||
fsStream.close();
|
||||
progressCallback({
|
||||
id: taskId,
|
||||
progress: (100).toFixed(2),
|
||||
curBytes: curRecvBytes,
|
||||
totalBytes: totalBytes,
|
||||
status: "done",
|
||||
dlUrl: url,
|
||||
savePath: targetPath,
|
||||
});
|
||||
});
|
||||
|
||||
global.__HUGO_AURA__.fsTasks?.downloadTasks.delete(taskId);
|
||||
return true;
|
||||
});
|
||||
|
||||
dlReq.on("error", (e) => {
|
||||
fsStream.close();
|
||||
fs.unlink(targetPath, () => {});
|
||||
failedTemplate.message =
|
||||
"Request error: Unexpected error while downloading file";
|
||||
failedTemplate.errorObj = e;
|
||||
progressCallback(failedTemplate);
|
||||
global.__HUGO_AURA__.fsTasks?.downloadTasks.delete(taskId);
|
||||
return false;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import("electron").IpcMain} ipcMain
|
||||
*/
|
||||
const applyFsIpcHandler = (ipcMain) => {
|
||||
const methodBase = "$aura.fs";
|
||||
|
||||
global.__HUGO_AURA__.fsTasks = {
|
||||
downloadTasks: new Map(),
|
||||
};
|
||||
|
||||
ipcMain.handle(
|
||||
`${methodBase}.dl.cancelDownloadTask`,
|
||||
/**
|
||||
*
|
||||
* @param {import("electron").IpcMainInvokeEvent} _evt
|
||||
* @param {{ targetTaskId: string }} arg
|
||||
* @returns {{ success: boolean, error: string | null }}
|
||||
*/
|
||||
(_evt, arg) => {
|
||||
if (!arg.targetTaskId) {
|
||||
return {
|
||||
success: false,
|
||||
error: "ARG_INVALID",
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.__HUGO_AURA__.fsTasks?.downloadTasks.has(arg.targetTaskId)) {
|
||||
return {
|
||||
success: false,
|
||||
error: "TASK_ID_NOT_FOUND",
|
||||
};
|
||||
}
|
||||
|
||||
const taskObj = global.__HUGO_AURA__.fsTasks.downloadTasks.get(
|
||||
arg.targetTaskId
|
||||
);
|
||||
if (!taskObj?.cancelReq) {
|
||||
return {
|
||||
success: false,
|
||||
error: "TASK_NOT_STARTED",
|
||||
};
|
||||
}
|
||||
|
||||
taskObj.cancelReq();
|
||||
return {
|
||||
success: true,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = { fsComposables: composableFunctions, applyFsIpcHandler };
|
||||
@@ -5,6 +5,8 @@ const __SCOPE = "main";
|
||||
const { exec } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const nodeHttps = require("https");
|
||||
const { fsComposables } = require("./fsIpcHandler");
|
||||
|
||||
const functions = {
|
||||
querySvcDetail: (
|
||||
@@ -47,8 +49,9 @@ const functions = {
|
||||
* @returns {Promise<{ success: boolean, errorObj?: Error }>}
|
||||
*/
|
||||
execCommand: (logHeader, binPath, command) => {
|
||||
const processedPath = binPath.includes(" ") ? `"${binPath}"` : binPath;
|
||||
return new Promise((resolve) => {
|
||||
exec(`"${binPath}" ${command}`, (error, stdout, stderr) => {
|
||||
exec(`${processedPath} ${command}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`${logHeader} Failed to execute command:`, error);
|
||||
resolve({ success: false, errorObj: error });
|
||||
@@ -58,6 +61,103 @@ const functions = {
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {"stable" | "alpha"} channel
|
||||
* @param {(arg: DownloadTask) => any} callbackFn
|
||||
* @param {string} binPath
|
||||
*/
|
||||
handlePLSDownload: async (channel, callbackFn, binPath) => {
|
||||
// TODO: Channel selection
|
||||
const apiInfo = global.__HUGO_AURA_API__;
|
||||
let plsVersionInfo = {};
|
||||
|
||||
const getVerPromise = new Promise((resolve) => {
|
||||
// ↓ 目前 channel param 没有什么用处
|
||||
nodeHttps
|
||||
.get(
|
||||
`${apiInfo.baseUrl}${apiInfo.plsUpdate}?channel=${channel}`,
|
||||
(rep) => {
|
||||
let dataChunk = "";
|
||||
rep.on("data", (chunk) => {
|
||||
dataChunk += chunk;
|
||||
});
|
||||
|
||||
rep.on("end", () => {
|
||||
resolve({
|
||||
success: true,
|
||||
data: dataChunk,
|
||||
});
|
||||
});
|
||||
}
|
||||
)
|
||||
.on("error", (e) => {
|
||||
resolve({
|
||||
success: false,
|
||||
data: null,
|
||||
errorObj: e,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const rawResInfo = await getVerPromise;
|
||||
if (!rawResInfo.success) {
|
||||
callbackFn({
|
||||
id: "",
|
||||
progress: 0,
|
||||
status: "failed",
|
||||
dlUrl: null,
|
||||
savePath: null,
|
||||
message: "未能获取 PLS 版本信息",
|
||||
errorObj: rawResInfo.errorObj ? rawResInfo.errorObj : null,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
plsVersionInfo = JSON.parse(rawResInfo.data);
|
||||
} catch (e) {
|
||||
callbackFn({
|
||||
id: "",
|
||||
progress: 0,
|
||||
status: "failed",
|
||||
dlUrl: null,
|
||||
savePath: null,
|
||||
message: "PLS 版本信息解析失败",
|
||||
errorObj: e,
|
||||
});
|
||||
console.error(
|
||||
"[HugoAura / IPC / PLS] Error querying PLS version info:",
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
let deviceArch = process.env.PROCESSOR_ARCHITEW6432
|
||||
? process.env.PROCESSOR_ARCHITEW6432
|
||||
: process.env.PROCESSOR_ARCHITECTURE;
|
||||
// @ts-expect-error
|
||||
deviceArch = deviceArch.toLowerCase();
|
||||
|
||||
if (!Object.keys(plsVersionInfo.data.downloadUrl).includes(deviceArch)) {
|
||||
callbackFn({
|
||||
id: "",
|
||||
progress: 0,
|
||||
status: "failed",
|
||||
dlUrl: null,
|
||||
savePath: null,
|
||||
message: `处理器架构识别失败, 检测到的架构: ${deviceArch}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
fsComposables.downloadFile(
|
||||
plsVersionInfo.data.downloadUrl[deviceArch],
|
||||
binPath,
|
||||
callbackFn
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -67,8 +167,8 @@ const functions = {
|
||||
const applyPlsIpcHandler = (ipcMain) => {
|
||||
const methodBase = "$aura.pls";
|
||||
|
||||
const PLS_INSTALL_DIR = path.join("C:\\Program Files", "HugoAura PLS");
|
||||
const PLS_BIN_PATH = path.join(PLS_INSTALL_DIR, "bin", "HugoAura-PLS.exe");
|
||||
const PLS_INSTALL_DIR = path.join("C:\\Program Files", "HugoAura PLS", "bin");
|
||||
const PLS_BIN_PATH = path.join(PLS_INSTALL_DIR, "HugoAura-PLS.exe");
|
||||
const PLS_SVC_NAME = "HugoAuraPLS";
|
||||
|
||||
const isPlsDetached = process.argv.includes("--pls-detach");
|
||||
@@ -106,6 +206,48 @@ const applyPlsIpcHandler = (ipcMain) => {
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
`${methodBase}.ensurePlsInstallDir`,
|
||||
/**
|
||||
*
|
||||
* @param {import("electron").IpcMainInvokeEvent} _event
|
||||
* @param {any} _arg
|
||||
* @returns {{ success: boolean; data: { alreadyExists: boolean; createdDir: string; }; error?: Error }}
|
||||
*/
|
||||
(_event, _arg) => {
|
||||
const alreadyExists = fs.existsSync(PLS_INSTALL_DIR);
|
||||
if (alreadyExists) {
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
alreadyExists: true,
|
||||
createdDir: PLS_INSTALL_DIR,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
fs.mkdirSync(PLS_INSTALL_DIR);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
alreadyExists: false,
|
||||
createdDir: PLS_INSTALL_DIR,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: {
|
||||
alreadyExists: false,
|
||||
createdDir: PLS_INSTALL_DIR,
|
||||
},
|
||||
error: error,
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
`${methodBase}.getPlsStats`,
|
||||
/**
|
||||
@@ -294,7 +436,7 @@ const applyPlsIpcHandler = (ipcMain) => {
|
||||
return await functions.execCommand(
|
||||
logHeader,
|
||||
PLS_BIN_PATH,
|
||||
"install"
|
||||
"--startup auto install"
|
||||
);
|
||||
case "rmSvc":
|
||||
return await functions.execCommand(logHeader, PLS_BIN_PATH, "remove");
|
||||
@@ -302,12 +444,59 @@ const applyPlsIpcHandler = (ipcMain) => {
|
||||
return await functions.execCommand(logHeader, PLS_BIN_PATH, "start");
|
||||
case "stopSvc":
|
||||
return await functions.execCommand(logHeader, PLS_BIN_PATH, "stop");
|
||||
case "rmBin":
|
||||
const unlinkPromise = new Promise((resolve) => {
|
||||
fs.unlink(PLS_BIN_PATH, (error) => {
|
||||
if (error) {
|
||||
resolve({
|
||||
success: false,
|
||||
errorObj: error,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
errorObj: null,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const unlinkRet = await unlinkPromise;
|
||||
|
||||
return unlinkRet;
|
||||
default:
|
||||
return { success: false, errorObj: new Error("Method not found") };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
`${methodBase}.downloadPls`,
|
||||
/**
|
||||
*
|
||||
* @param {import("electron").IpcMainInvokeEvent} _evt
|
||||
* @param {{channel?: "stable" | "alpha", reportTo?: import("../../../types/main/core").WindowName}} arg
|
||||
* @returns {void}
|
||||
*/
|
||||
(_evt, arg) => {
|
||||
const channel = arg.channel ? arg.channel : "stable";
|
||||
const reportWin = arg.reportTo ? arg.reportTo : "assistant";
|
||||
functions.handlePLSDownload(
|
||||
channel,
|
||||
(status) => {
|
||||
ipcMain.send(
|
||||
reportWin,
|
||||
`${methodBase}.post.reportPlsDownloadStatus`,
|
||||
status
|
||||
);
|
||||
},
|
||||
PLS_BIN_PATH
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
`${methodBase}.retryPlsConnect`,
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user