mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-30 03:10:16 +08:00
复用下载中心(1/2)
This commit is contained in:
368
electron/main/backend/shared-installer.ts
Normal file
368
electron/main/backend/shared-installer.ts
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
/**
|
||||||
|
* 共享的安装/下载逻辑
|
||||||
|
* 被 install-manager.ts 和 update-center 共同使用
|
||||||
|
*/
|
||||||
|
import { spawn, ChildProcess } from "node:child_process";
|
||||||
|
import { createWriteStream } from "node:fs";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import axios from "axios";
|
||||||
|
import pino from "pino";
|
||||||
|
|
||||||
|
const logger = pino({ name: "shared-installer" });
|
||||||
|
|
||||||
|
export const SHELL_CALLER_PATH = "/opt/spark-store/extras/shell-caller.sh";
|
||||||
|
|
||||||
|
export interface DownloadOptions {
|
||||||
|
pkgname: string;
|
||||||
|
metalinkUrl: string;
|
||||||
|
filename: string;
|
||||||
|
downloadDir: string;
|
||||||
|
onLog?: (msg: string) => void;
|
||||||
|
onProgress?: (progress: number) => void;
|
||||||
|
onStatus?: (status: string) => void;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadResult {
|
||||||
|
filePath: string;
|
||||||
|
downloadDir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载 metalink 文件并使用 aria2c 下载 deb 包
|
||||||
|
* 与 install-manager.ts 中的下载逻辑保持一致
|
||||||
|
*/
|
||||||
|
export const downloadPackage = async ({
|
||||||
|
pkgname,
|
||||||
|
metalinkUrl,
|
||||||
|
filename,
|
||||||
|
downloadDir,
|
||||||
|
onLog,
|
||||||
|
onProgress,
|
||||||
|
onStatus,
|
||||||
|
signal,
|
||||||
|
}: DownloadOptions): Promise<DownloadResult> => {
|
||||||
|
// 1. 创建下载目录
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(downloadDir)) {
|
||||||
|
fs.mkdirSync(downloadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`无法创建目录 ${downloadDir}: ${err}`);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metalinkPath = path.join(downloadDir, `${filename}.metalink`);
|
||||||
|
|
||||||
|
onLog?.(`正在获取 Metalink 文件: ${metalinkUrl}`);
|
||||||
|
|
||||||
|
// 2. 下载 metalink 文件
|
||||||
|
const response = await axios.get(metalinkUrl, {
|
||||||
|
baseURL: "https://erotica.spark-app.store",
|
||||||
|
responseType: "stream",
|
||||||
|
});
|
||||||
|
|
||||||
|
const writer = createWriteStream(metalinkPath);
|
||||||
|
response.data.pipe(writer);
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
writer.on("finish", resolve);
|
||||||
|
writer.on("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
onLog?.("Metalink 文件下载完成");
|
||||||
|
|
||||||
|
// 3. 清理下载目录中的旧文件(保留 .metalink 文件)
|
||||||
|
const existingFiles = fs.readdirSync(downloadDir);
|
||||||
|
for (const file of existingFiles) {
|
||||||
|
if (file.endsWith(".metalink")) continue;
|
||||||
|
const filePath = path.join(downloadDir, file);
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
onLog?.(`已清理旧文件: ${file}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(`清理文件失败 ${filePath}: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 使用 aria2c 下载 deb 文件
|
||||||
|
const aria2Args = [
|
||||||
|
`--dir=${downloadDir}`,
|
||||||
|
"--allow-overwrite=true",
|
||||||
|
"--summary-interval=1",
|
||||||
|
"--connect-timeout=10",
|
||||||
|
"--timeout=15",
|
||||||
|
"--max-tries=3",
|
||||||
|
"--retry-wait=5",
|
||||||
|
"--max-concurrent-downloads=4",
|
||||||
|
"--min-split-size=1M",
|
||||||
|
"--lowest-speed-limit=1K",
|
||||||
|
"--auto-file-renaming=false",
|
||||||
|
"-M",
|
||||||
|
metalinkPath,
|
||||||
|
];
|
||||||
|
|
||||||
|
onStatus?.("downloading");
|
||||||
|
|
||||||
|
// 下载重试逻辑:每次超时时间递增,最多3次
|
||||||
|
const timeoutList = [3000, 5000, 15000];
|
||||||
|
let retryCount = 0;
|
||||||
|
let downloadSuccess = false;
|
||||||
|
|
||||||
|
while (retryCount < timeoutList.length && !downloadSuccess) {
|
||||||
|
const currentTimeout = timeoutList[retryCount];
|
||||||
|
|
||||||
|
if (retryCount > 0) {
|
||||||
|
onLog?.(`第 ${retryCount} 次重试下载...`);
|
||||||
|
onProgress?.(0);
|
||||||
|
// 重试前清理旧文件
|
||||||
|
const retryFiles = fs.readdirSync(downloadDir);
|
||||||
|
for (const file of retryFiles) {
|
||||||
|
if (file.endsWith(".metalink")) continue;
|
||||||
|
const filePath = path.join(downloadDir, file);
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(filePath);
|
||||||
|
} catch (cleanErr) {
|
||||||
|
logger.warn(`重试清理文件失败 ${filePath}: ${cleanErr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
onLog?.(`启动下载: aria2c ${aria2Args.join(" ")}`);
|
||||||
|
const child = spawn("aria2c", aria2Args);
|
||||||
|
|
||||||
|
let lastProgressTime = Date.now();
|
||||||
|
let lastProgress = 0;
|
||||||
|
const progressCheckInterval = 1000;
|
||||||
|
|
||||||
|
// 设置超时检测定时器
|
||||||
|
const timeoutChecker = setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
// 只在进度为0时检查超时
|
||||||
|
if (lastProgress === 0 && now - lastProgressTime > currentTimeout) {
|
||||||
|
clearInterval(timeoutChecker);
|
||||||
|
child.kill();
|
||||||
|
reject(new Error(`下载卡在0%超过 ${currentTimeout / 1000} 秒`));
|
||||||
|
}
|
||||||
|
}, progressCheckInterval);
|
||||||
|
|
||||||
|
child.stdout.on("data", (data) => {
|
||||||
|
const str = data.toString();
|
||||||
|
// Match ( 12%) or (12%)
|
||||||
|
const match = str.match(/[0-9]+(\.[0-9]+)?%/g);
|
||||||
|
if (match) {
|
||||||
|
const p = parseFloat(match.at(-1)) / 100;
|
||||||
|
if (p > lastProgress) {
|
||||||
|
lastProgress = p;
|
||||||
|
lastProgressTime = Date.now();
|
||||||
|
}
|
||||||
|
onProgress?.(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (d) => onLog?.(`aria2c: ${d}`));
|
||||||
|
|
||||||
|
// 处理取消信号
|
||||||
|
const abortHandler = () => {
|
||||||
|
clearInterval(timeoutChecker);
|
||||||
|
child.kill();
|
||||||
|
reject(new Error("下载已取消"));
|
||||||
|
};
|
||||||
|
|
||||||
|
signal?.addEventListener("abort", abortHandler, { once: true });
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
clearInterval(timeoutChecker);
|
||||||
|
signal?.removeEventListener("abort", abortHandler);
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
onProgress?.(1);
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Aria2c exited with code ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.on("error", (err) => {
|
||||||
|
clearInterval(timeoutChecker);
|
||||||
|
signal?.removeEventListener("abort", abortHandler);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 检查是否已取消
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw new Error("下载已取消");
|
||||||
|
}
|
||||||
|
downloadSuccess = true;
|
||||||
|
} catch (err) {
|
||||||
|
retryCount++;
|
||||||
|
if (retryCount >= timeoutList.length) {
|
||||||
|
throw new Error(`下载失败,已重试 ${timeoutList.length} 次: ${err}`);
|
||||||
|
}
|
||||||
|
onLog?.(`下载失败,准备重试 (${retryCount}/${timeoutList.length})`);
|
||||||
|
// 等待2秒后重试
|
||||||
|
await new Promise((r) => setTimeout(r, 2000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(downloadDir, filename);
|
||||||
|
return { filePath, downloadDir };
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InstallOptions {
|
||||||
|
pkgname: string;
|
||||||
|
filePath: string;
|
||||||
|
origin: "spark" | "apm";
|
||||||
|
superUserCmd?: string;
|
||||||
|
onLog?: (msg: string) => void;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装已下载的包
|
||||||
|
* 与 install-manager.ts 中的安装逻辑保持一致
|
||||||
|
*/
|
||||||
|
export const installPackage = async ({
|
||||||
|
pkgname,
|
||||||
|
filePath,
|
||||||
|
origin,
|
||||||
|
superUserCmd,
|
||||||
|
onLog,
|
||||||
|
signal,
|
||||||
|
}: InstallOptions): Promise<void> => {
|
||||||
|
// 构建安装命令
|
||||||
|
let execCommand = "";
|
||||||
|
const execParams: string[] = [];
|
||||||
|
|
||||||
|
if (origin === "spark") {
|
||||||
|
execCommand = superUserCmd || SHELL_CALLER_PATH;
|
||||||
|
if (superUserCmd) execParams.push(SHELL_CALLER_PATH);
|
||||||
|
execParams.push(
|
||||||
|
"ssinstall",
|
||||||
|
filePath,
|
||||||
|
"--delete-after-install",
|
||||||
|
"--no-create-desktop-entry",
|
||||||
|
"--native"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// APM
|
||||||
|
execCommand = superUserCmd || SHELL_CALLER_PATH;
|
||||||
|
if (superUserCmd) {
|
||||||
|
execParams.push(SHELL_CALLER_PATH);
|
||||||
|
}
|
||||||
|
execParams.push("apm", "ssinstall", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmdString = `${execCommand} ${execParams.join(" ")}`;
|
||||||
|
onLog?.(`执行安装: ${cmdString}`);
|
||||||
|
logger.info(`启动安装: ${cmdString}`);
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const child = spawn(execCommand, execParams, {
|
||||||
|
shell: false,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
let logBuffer = "";
|
||||||
|
let logBufferTimer: NodeJS.Timeout | null = null;
|
||||||
|
const LOG_FLUSH_MS = 100;
|
||||||
|
|
||||||
|
const flushLogBuffer = () => {
|
||||||
|
if (logBuffer.length > 0) {
|
||||||
|
onLog?.(logBuffer);
|
||||||
|
logBuffer = "";
|
||||||
|
}
|
||||||
|
logBufferTimer = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bufferedSendLog = (message: string) => {
|
||||||
|
logBuffer += message;
|
||||||
|
if (!logBufferTimer) {
|
||||||
|
logBufferTimer = setTimeout(flushLogBuffer, LOG_FLUSH_MS);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理取消信号
|
||||||
|
const abortHandler = () => {
|
||||||
|
child.kill();
|
||||||
|
reject(new Error("安装已取消"));
|
||||||
|
};
|
||||||
|
|
||||||
|
signal?.addEventListener("abort", abortHandler, { once: true });
|
||||||
|
|
||||||
|
child.stdout?.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
bufferedSendLog(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
bufferedSendLog(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (err) => {
|
||||||
|
signal?.removeEventListener("abort", abortHandler);
|
||||||
|
if (logBufferTimer) clearTimeout(logBufferTimer);
|
||||||
|
flushLogBuffer();
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
signal?.removeEventListener("abort", abortHandler);
|
||||||
|
if (logBufferTimer) clearTimeout(logBufferTimer);
|
||||||
|
flushLogBuffer();
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`安装失败,退出码: ${code}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否有 apm 命令
|
||||||
|
*/
|
||||||
|
export const checkApmAvailable = async (): Promise<boolean> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn("which", ["apm"]);
|
||||||
|
let stdout = "";
|
||||||
|
child.stdout?.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
child.on("close", (code) => {
|
||||||
|
resolve(code === 0 && stdout.trim().length > 0);
|
||||||
|
});
|
||||||
|
child.on("error", () => {
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查提权命令
|
||||||
|
*/
|
||||||
|
export const checkSuperUserCommand = async (): Promise<string> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn("which", ["/usr/bin/pkexec"]);
|
||||||
|
let stdout = "";
|
||||||
|
child.stdout?.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve(stdout.trim());
|
||||||
|
} else {
|
||||||
|
resolve("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.on("error", () => {
|
||||||
|
resolve("");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { mkdir } from "node:fs/promises";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { spawn } from "node:child_process";
|
import { downloadPackage, type DownloadResult } from "../shared-installer";
|
||||||
|
|
||||||
import type { UpdateCenterItem } from "./types";
|
import type { UpdateCenterItem } from "./types";
|
||||||
|
|
||||||
export interface Aria2DownloadResult {
|
export interface Aria2DownloadResult {
|
||||||
@@ -16,8 +14,6 @@ export interface RunAria2DownloadOptions {
|
|||||||
signal?: AbortSignal;
|
signal?: AbortSignal;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROGRESS_PATTERN = /(\d{1,3}(?:\.\d+)?)%/;
|
|
||||||
|
|
||||||
export const runAria2Download = async ({
|
export const runAria2Download = async ({
|
||||||
item,
|
item,
|
||||||
downloadDir,
|
downloadDir,
|
||||||
@@ -29,68 +25,18 @@ export const runAria2Download = async ({
|
|||||||
throw new Error(`Missing download metadata for ${item.pkgname}`);
|
throw new Error(`Missing download metadata for ${item.pkgname}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await mkdir(downloadDir, { recursive: true });
|
// 使用与商店安装相同的下载逻辑
|
||||||
|
|
||||||
const filePath = join(downloadDir, item.fileName);
|
|
||||||
|
|
||||||
// Use .metalink URL for download (same as Qt version)
|
|
||||||
const metalinkUrl = `${item.downloadUrl}.metalink`;
|
const metalinkUrl = `${item.downloadUrl}.metalink`;
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
const result = await downloadPackage({
|
||||||
const child = spawn("aria2c", [
|
pkgname: item.pkgname,
|
||||||
"--dir",
|
metalinkUrl,
|
||||||
downloadDir,
|
filename: item.fileName,
|
||||||
"--out",
|
downloadDir,
|
||||||
item.fileName,
|
onLog,
|
||||||
"--enable-rpc=false",
|
onProgress,
|
||||||
"--console-log-level=warn",
|
signal,
|
||||||
"--summary-interval=1",
|
|
||||||
"--allow-overwrite=true",
|
|
||||||
"--connect-timeout=30",
|
|
||||||
"--max-tries=3",
|
|
||||||
metalinkUrl,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const abortDownload = () => {
|
|
||||||
child.kill();
|
|
||||||
reject(new Error(`Update task cancelled: ${item.pkgname}`));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (signal?.aborted) {
|
|
||||||
abortDownload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
signal?.addEventListener("abort", abortDownload, { once: true });
|
|
||||||
|
|
||||||
const handleOutput = (chunk: Buffer) => {
|
|
||||||
const message = chunk.toString().trim();
|
|
||||||
if (!message) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onLog?.(message);
|
|
||||||
const progressMatch = message.match(PROGRESS_PATTERN);
|
|
||||||
if (progressMatch) {
|
|
||||||
onProgress?.(Number(progressMatch[1]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
child.stdout?.on("data", handleOutput);
|
|
||||||
child.stderr?.on("data", handleOutput);
|
|
||||||
child.on("error", reject);
|
|
||||||
child.on("close", (code) => {
|
|
||||||
signal?.removeEventListener("abort", abortDownload);
|
|
||||||
if (code === 0) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reject(new Error(`aria2c exited with code ${code ?? -1}`));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onProgress?.(100);
|
return { filePath: result.filePath };
|
||||||
|
|
||||||
return { filePath };
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -155,20 +155,30 @@ const loadAptssItemMetadata = async (
|
|||||||
| { item: UpdateCenterItem; warning?: undefined }
|
| { item: UpdateCenterItem; warning?: undefined }
|
||||||
| { item: null; warning: string }
|
| { item: null; warning: string }
|
||||||
> => {
|
> => {
|
||||||
|
console.log(`[DEBUG] Loading APTSS metadata for ${item.pkgname}`);
|
||||||
const printUrisCommand = getAptssPrintUrisCommand(item.pkgname);
|
const printUrisCommand = getAptssPrintUrisCommand(item.pkgname);
|
||||||
|
console.log(`[DEBUG] APTSS command: ${printUrisCommand.command} ${printUrisCommand.args.join(' ')}`);
|
||||||
|
|
||||||
const metadataResult = await runCommand(
|
const metadataResult = await runCommand(
|
||||||
printUrisCommand.command,
|
printUrisCommand.command,
|
||||||
printUrisCommand.args,
|
printUrisCommand.args,
|
||||||
);
|
);
|
||||||
|
console.log(`[DEBUG] APTSS metadata result code: ${metadataResult.code}`);
|
||||||
|
console.log(`[DEBUG] APTSS metadata stdout: ${metadataResult.stdout.substring(0, 500)}`);
|
||||||
|
console.log(`[DEBUG] APTSS metadata stderr: ${metadataResult.stderr.substring(0, 500)}`);
|
||||||
|
|
||||||
const commandError = getCommandError(
|
const commandError = getCommandError(
|
||||||
`aptss metadata query for ${item.pkgname}`,
|
`aptss metadata query for ${item.pkgname}`,
|
||||||
metadataResult,
|
metadataResult,
|
||||||
);
|
);
|
||||||
if (commandError) {
|
if (commandError) {
|
||||||
|
console.log(`[DEBUG] APTSS metadata error: ${commandError}`);
|
||||||
return { item: null, warning: commandError };
|
return { item: null, warning: commandError };
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = parsePrintUrisOutput(metadataResult.stdout);
|
const metadata = parsePrintUrisOutput(metadataResult.stdout);
|
||||||
|
console.log(`[DEBUG] APTSS parsed metadata:`, metadata);
|
||||||
|
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
return {
|
return {
|
||||||
item: null,
|
item: null,
|
||||||
@@ -424,14 +434,8 @@ export const initializeUpdateCenter = (): UpdateCenterService => {
|
|||||||
return updateCenterService;
|
return updateCenterService;
|
||||||
}
|
}
|
||||||
|
|
||||||
const superUserCmdProvider = async (): Promise<string> => {
|
|
||||||
const installManager = await import("../install-manager.js");
|
|
||||||
return installManager.checkSuperUserCommand();
|
|
||||||
};
|
|
||||||
|
|
||||||
updateCenterService = createUpdateCenterService({
|
updateCenterService = createUpdateCenterService({
|
||||||
loadItems: loadUpdateCenterItems,
|
loadItems: loadUpdateCenterItems,
|
||||||
superUserCmdProvider,
|
|
||||||
});
|
});
|
||||||
registerUpdateCenterIpc(ipcMain, updateCenterService);
|
registerUpdateCenterIpc(ipcMain, updateCenterService);
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
import { spawn } from "node:child_process";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { runAria2Download, type Aria2DownloadResult } from "./download";
|
import { runAria2Download, type Aria2DownloadResult } from "./download";
|
||||||
|
import { installPackage } from "../shared-installer";
|
||||||
import type { UpdateCenterQueue, UpdateCenterTask } from "./queue";
|
import type { UpdateCenterQueue, UpdateCenterTask } from "./queue";
|
||||||
import type { UpdateCenterItem } from "./types";
|
import type { UpdateCenterItem } from "./types";
|
||||||
|
|
||||||
const SHELL_CALLER_PATH = "/opt/spark-store/extras/shell-caller.sh";
|
|
||||||
const SSINSTALL_PATH = "/usr/bin/ssinstall";
|
|
||||||
const DEFAULT_DOWNLOAD_ROOT = "/tmp/spark-store/update-center";
|
const DEFAULT_DOWNLOAD_ROOT = "/tmp/spark-store/update-center";
|
||||||
|
|
||||||
export interface UpdateCommand {
|
|
||||||
execCommand: string;
|
|
||||||
execParams: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface InstallUpdateItemOptions {
|
export interface InstallUpdateItemOptions {
|
||||||
item: UpdateCenterItem;
|
item: UpdateCenterItem;
|
||||||
filePath?: string;
|
filePath?: string;
|
||||||
@@ -55,73 +48,10 @@ export interface CreateTaskRunnerOptions extends TaskRunnerDependencies {
|
|||||||
superUserCmd?: string;
|
superUserCmd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const runCommand = async (
|
/**
|
||||||
execCommand: string,
|
* 安装更新项
|
||||||
execParams: string[],
|
* 使用与商店安装相同的逻辑
|
||||||
onLog?: (message: string) => void,
|
*/
|
||||||
signal?: AbortSignal,
|
|
||||||
): Promise<void> => {
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const child = spawn(execCommand, execParams, {
|
|
||||||
shell: false,
|
|
||||||
env: process.env,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleOutput = (chunk: Buffer) => {
|
|
||||||
const message = chunk.toString().trim();
|
|
||||||
if (message) {
|
|
||||||
onLog?.(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const abortCommand = () => {
|
|
||||||
child.kill();
|
|
||||||
reject(new Error(`Update task cancelled: ${execParams.join(" ")}`));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (signal?.aborted) {
|
|
||||||
abortCommand();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
signal?.addEventListener("abort", abortCommand, { once: true });
|
|
||||||
|
|
||||||
child.stdout?.on("data", handleOutput);
|
|
||||||
child.stderr?.on("data", handleOutput);
|
|
||||||
child.on("error", reject);
|
|
||||||
child.on("close", (code) => {
|
|
||||||
signal?.removeEventListener("abort", abortCommand);
|
|
||||||
if (code === 0) {
|
|
||||||
resolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reject(new Error(`${execCommand} exited with code ${code ?? -1}`));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildPrivilegedCommand = (
|
|
||||||
command: string,
|
|
||||||
args: string[],
|
|
||||||
superUserCmd?: string,
|
|
||||||
): UpdateCommand => {
|
|
||||||
if (superUserCmd) {
|
|
||||||
return {
|
|
||||||
execCommand: superUserCmd,
|
|
||||||
execParams: [command, ...args],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
execCommand: command,
|
|
||||||
execParams: args,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Removed buildLegacySparkUpgradeCommand - all updates now require downloading the deb package first
|
|
||||||
// to avoid aptss install popup. Use ssinstall with downloaded deb file instead.
|
|
||||||
|
|
||||||
export const installUpdateItem = async ({
|
export const installUpdateItem = async ({
|
||||||
item,
|
item,
|
||||||
filePath,
|
filePath,
|
||||||
@@ -135,33 +65,17 @@ export const installUpdateItem = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.source === "apm") {
|
// 使用与商店安装相同的安装逻辑
|
||||||
const installCommand = buildPrivilegedCommand(
|
const origin = item.source === "apm" ? "apm" : "spark";
|
||||||
SHELL_CALLER_PATH,
|
|
||||||
["apm", "ssinstall", filePath],
|
await installPackage({
|
||||||
superUserCmd,
|
pkgname: item.pkgname,
|
||||||
);
|
filePath,
|
||||||
await runCommand(
|
origin,
|
||||||
installCommand.execCommand,
|
|
||||||
installCommand.execParams,
|
|
||||||
onLog,
|
|
||||||
signal,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// APTSS (Spark Store) packages use ssinstall
|
|
||||||
const installCommand = buildPrivilegedCommand(
|
|
||||||
SSINSTALL_PATH,
|
|
||||||
[filePath, "--delete-after-install", "--no-create-desktop-entry", "--native"],
|
|
||||||
superUserCmd,
|
superUserCmd,
|
||||||
);
|
|
||||||
await runCommand(
|
|
||||||
installCommand.execCommand,
|
|
||||||
installCommand.execParams,
|
|
||||||
onLog,
|
onLog,
|
||||||
signal,
|
signal,
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createTaskRunner = (
|
export const createTaskRunner = (
|
||||||
@@ -194,11 +108,7 @@ export const createTaskRunner = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
cancelActiveTask: () => {
|
cancelActiveTask: () => {
|
||||||
if (!activeAbortController || activeAbortController.signal.aborted) {
|
activeAbortController?.abort();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeAbortController.abort();
|
|
||||||
},
|
},
|
||||||
runNextTask: async () => {
|
runNextTask: async () => {
|
||||||
if (inFlightTask) {
|
if (inFlightTask) {
|
||||||
|
|||||||
@@ -270,7 +270,9 @@ export const parsePrintUrisOutput = (
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, downloadUrl, fileName, size, sha512] = match;
|
const [, rawDownloadUrl, fileName, size, sha512] = match;
|
||||||
|
// Clean up the URL: remove backticks and extra spaces
|
||||||
|
const downloadUrl = rawDownloadUrl.replace(/[`'"]/g, "").trim();
|
||||||
return {
|
return {
|
||||||
downloadUrl,
|
downloadUrl,
|
||||||
fileName,
|
fileName,
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ export const createUpdateCenterQueue = (): UpdateCenterQueue => {
|
|||||||
let refreshing = false;
|
let refreshing = false;
|
||||||
let nextTaskId = 1;
|
let nextTaskId = 1;
|
||||||
|
|
||||||
const getTask = (taskId: number): UpdateCenterTask | undefined =>
|
const getTaskIndex = (taskId: number): number =>
|
||||||
tasks.find((task) => task.id === taskId);
|
tasks.findIndex((task) => task.id === taskId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setItems: (nextItems) => {
|
setItems: (nextItems) => {
|
||||||
@@ -117,40 +117,59 @@ export const createUpdateCenterQueue = (): UpdateCenterQueue => {
|
|||||||
return task;
|
return task;
|
||||||
},
|
},
|
||||||
markActiveTask: (taskId, status) => {
|
markActiveTask: (taskId, status) => {
|
||||||
const task = getTask(taskId);
|
const taskIndex = getTaskIndex(taskId);
|
||||||
if (!task) {
|
if (taskIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
task.status = status;
|
// 创建新的 task 对象和新的 tasks 数组以触发状态更新
|
||||||
|
tasks = tasks.map((task, index) =>
|
||||||
|
index === taskIndex ? { ...task, status } : task,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
updateTaskProgress: (taskId, progress) => {
|
updateTaskProgress: (taskId, progress) => {
|
||||||
const task = getTask(taskId);
|
const taskIndex = getTaskIndex(taskId);
|
||||||
if (!task) {
|
if (taskIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
task.progress = clampProgress(progress);
|
// 创建新的 task 对象和新的 tasks 数组以触发状态更新
|
||||||
|
tasks = tasks.map((task, index) =>
|
||||||
|
index === taskIndex
|
||||||
|
? { ...task, progress: clampProgress(progress) }
|
||||||
|
: task,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
appendTaskLog: (taskId, message, time = Date.now()) => {
|
appendTaskLog: (taskId, message, time = Date.now()) => {
|
||||||
const task = getTask(taskId);
|
const taskIndex = getTaskIndex(taskId);
|
||||||
if (!task) {
|
if (taskIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
task.logs = [...task.logs, { time, message }];
|
// 创建新的 task 对象和新的 tasks 数组以触发状态更新
|
||||||
|
tasks = tasks.map((task, index) =>
|
||||||
|
index === taskIndex
|
||||||
|
? { ...task, logs: [...task.logs, { time, message }] }
|
||||||
|
: task,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
finishTask: (taskId, status, error) => {
|
finishTask: (taskId, status, error) => {
|
||||||
const task = getTask(taskId);
|
const taskIndex = getTaskIndex(taskId);
|
||||||
if (!task) {
|
if (taskIndex === -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
task.status = status;
|
// 创建新的 task 对象和新的 tasks 数组以触发状态更新
|
||||||
task.error = error;
|
tasks = tasks.map((task, index) =>
|
||||||
if (status === "completed") {
|
index === taskIndex
|
||||||
task.progress = 100;
|
? {
|
||||||
}
|
...task,
|
||||||
|
status,
|
||||||
|
error,
|
||||||
|
progress: status === "completed" ? 100 : task.progress,
|
||||||
|
}
|
||||||
|
: task,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
getNextQueuedTask: () => tasks.find((task) => task.status === "queued"),
|
getNextQueuedTask: () => tasks.find((task) => task.status === "queued"),
|
||||||
getSnapshot: () => createSnapshot(items, tasks, warnings, refreshing),
|
getSnapshot: () => createSnapshot(items, tasks, warnings, refreshing),
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { BrowserWindow, ipcMain } from "electron";
|
||||||
import {
|
import {
|
||||||
LEGACY_IGNORE_CONFIG_PATH,
|
LEGACY_IGNORE_CONFIG_PATH,
|
||||||
applyIgnoredEntries,
|
applyIgnoredEntries,
|
||||||
@@ -5,7 +6,6 @@ import {
|
|||||||
loadIgnoredEntries,
|
loadIgnoredEntries,
|
||||||
saveIgnoredEntries,
|
saveIgnoredEntries,
|
||||||
} from "./ignore-config";
|
} from "./ignore-config";
|
||||||
import { createTaskRunner, type UpdateCenterTaskRunner } from "./install";
|
|
||||||
import {
|
import {
|
||||||
createUpdateCenterQueue,
|
createUpdateCenterQueue,
|
||||||
type UpdateCenterQueue,
|
type UpdateCenterQueue,
|
||||||
@@ -79,11 +79,6 @@ export interface CreateUpdateCenterServiceOptions {
|
|||||||
loadItems: () => Promise<UpdateCenterItem[] | UpdateCenterLoadedItems>;
|
loadItems: () => Promise<UpdateCenterItem[] | UpdateCenterLoadedItems>;
|
||||||
loadIgnoredEntries?: () => Promise<Set<string>>;
|
loadIgnoredEntries?: () => Promise<Set<string>>;
|
||||||
saveIgnoredEntries?: (entries: ReadonlySet<string>) => Promise<void>;
|
saveIgnoredEntries?: (entries: ReadonlySet<string>) => Promise<void>;
|
||||||
createTaskRunner?: (
|
|
||||||
queue: UpdateCenterQueue,
|
|
||||||
superUserCmd?: string,
|
|
||||||
) => UpdateCenterTaskRunner;
|
|
||||||
superUserCmdProvider?: () => Promise<string>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTaskKey = (
|
const getTaskKey = (
|
||||||
@@ -112,19 +107,9 @@ const toState = (
|
|||||||
migrationTarget: item.migrationTarget,
|
migrationTarget: item.migrationTarget,
|
||||||
aptssVersion: item.aptssVersion,
|
aptssVersion: item.aptssVersion,
|
||||||
})),
|
})),
|
||||||
tasks: snapshot.tasks.map((task) => ({
|
tasks: [], // 不再展示任务日志
|
||||||
taskKey: getTaskKey(task.item),
|
|
||||||
packageName: task.pkgname,
|
|
||||||
source: task.item.source,
|
|
||||||
localIcon: task.item.localIcon,
|
|
||||||
remoteIcon: task.item.remoteIcon,
|
|
||||||
status: task.status,
|
|
||||||
progress: task.progress,
|
|
||||||
logs: task.logs.map((log) => ({ ...log })),
|
|
||||||
errorMessage: task.error ?? "",
|
|
||||||
})),
|
|
||||||
warnings: [...snapshot.warnings],
|
warnings: [...snapshot.warnings],
|
||||||
hasRunningTasks: snapshot.hasRunningTasks,
|
hasRunningTasks: false, // 任务不在更新中心执行
|
||||||
});
|
});
|
||||||
|
|
||||||
const normalizeLoadedItems = (
|
const normalizeLoadedItems = (
|
||||||
@@ -152,12 +137,8 @@ export const createUpdateCenterService = (
|
|||||||
options.saveIgnoredEntries ??
|
options.saveIgnoredEntries ??
|
||||||
((entries: ReadonlySet<string>) =>
|
((entries: ReadonlySet<string>) =>
|
||||||
saveIgnoredEntries(LEGACY_IGNORE_CONFIG_PATH, entries));
|
saveIgnoredEntries(LEGACY_IGNORE_CONFIG_PATH, entries));
|
||||||
const createRunner =
|
|
||||||
options.createTaskRunner ??
|
let nextUpdateTaskId = 1;
|
||||||
((currentQueue: UpdateCenterQueue, superUserCmd?: string) =>
|
|
||||||
createTaskRunner(currentQueue, { superUserCmd }));
|
|
||||||
let processingPromise: Promise<void> | null = null;
|
|
||||||
let activeRunner: UpdateCenterTaskRunner | null = null;
|
|
||||||
|
|
||||||
const applyWarning = (message: string): void => {
|
const applyWarning = (message: string): void => {
|
||||||
queue.finishRefresh([message]);
|
queue.finishRefresh([message]);
|
||||||
@@ -167,9 +148,9 @@ export const createUpdateCenterService = (
|
|||||||
|
|
||||||
const emit = (): UpdateCenterServiceState => {
|
const emit = (): UpdateCenterServiceState => {
|
||||||
const snapshot = getState();
|
const snapshot = getState();
|
||||||
for (const listener of listeners) {
|
listeners.forEach((listener) => {
|
||||||
listener(snapshot);
|
listener(snapshot);
|
||||||
}
|
});
|
||||||
return snapshot;
|
return snapshot;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -192,47 +173,6 @@ export const createUpdateCenterService = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const failQueuedTasks = (message: string): void => {
|
|
||||||
for (const task of queue.getSnapshot().tasks) {
|
|
||||||
if (task.status === "queued") {
|
|
||||||
queue.appendTaskLog(task.id, message);
|
|
||||||
queue.finishTask(task.id, "failed", message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureProcessing = async (): Promise<void> => {
|
|
||||||
if (processingPromise) {
|
|
||||||
return processingPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
processingPromise = (async () => {
|
|
||||||
let superUserCmd = "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
superUserCmd = (await options.superUserCmdProvider?.()) ?? "";
|
|
||||||
} catch (error) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
failQueuedTasks(message);
|
|
||||||
applyWarning(message);
|
|
||||||
emit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeRunner = createRunner(queue, superUserCmd);
|
|
||||||
|
|
||||||
while (queue.getNextQueuedTask()) {
|
|
||||||
await activeRunner.runNextTask();
|
|
||||||
emit();
|
|
||||||
}
|
|
||||||
})().finally(() => {
|
|
||||||
processingPromise = null;
|
|
||||||
activeRunner = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return processingPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
open: refresh,
|
open: refresh,
|
||||||
refresh,
|
refresh,
|
||||||
@@ -250,47 +190,63 @@ export const createUpdateCenterService = (
|
|||||||
},
|
},
|
||||||
async start(taskKeys) {
|
async start(taskKeys) {
|
||||||
const snapshot = queue.getSnapshot();
|
const snapshot = queue.getSnapshot();
|
||||||
const existingTaskKeys = new Set(
|
|
||||||
snapshot.tasks
|
|
||||||
.filter(
|
|
||||||
(task) =>
|
|
||||||
!["completed", "failed", "cancelled"].includes(task.status),
|
|
||||||
)
|
|
||||||
.map((task) => getTaskKey(task.item)),
|
|
||||||
);
|
|
||||||
const selectedItems = snapshot.items.filter(
|
const selectedItems = snapshot.items.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
taskKeys.includes(getTaskKey(item)) &&
|
taskKeys.includes(getTaskKey(item)) &&
|
||||||
!item.ignored &&
|
!item.ignored,
|
||||||
!existingTaskKeys.has(getTaskKey(item)),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedItems.length === 0) {
|
if (selectedItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of selectedItems) {
|
// 获取主窗口的 webContents
|
||||||
queue.enqueueItem(item);
|
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||||
}
|
const webContents = mainWindow?.webContents;
|
||||||
emit();
|
|
||||||
|
|
||||||
await ensureProcessing();
|
if (!webContents) {
|
||||||
},
|
console.error("No main window found");
|
||||||
async cancel(taskKey) {
|
|
||||||
const task = queue
|
|
||||||
.getSnapshot()
|
|
||||||
.tasks.find((entry) => getTaskKey(entry.item) === taskKey);
|
|
||||||
|
|
||||||
if (!task) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.finishTask(task.id, "cancelled", "Cancelled");
|
// 获取当前 items
|
||||||
if (["downloading", "installing"].includes(task.status)) {
|
let currentItems = snapshot.items;
|
||||||
activeRunner?.cancelActiveTask();
|
|
||||||
|
for (const item of selectedItems) {
|
||||||
|
const updateTaskId = nextUpdateTaskId++;
|
||||||
|
|
||||||
|
// 构建 metalink URL
|
||||||
|
const metalinkUrl = item.downloadUrl
|
||||||
|
? `${item.downloadUrl}.metalink`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
// 发送到主下载队列
|
||||||
|
const installTaskData = {
|
||||||
|
id: updateTaskId,
|
||||||
|
pkgname: item.pkgname,
|
||||||
|
metalinkUrl,
|
||||||
|
filename: item.fileName,
|
||||||
|
upgradeOnly: true,
|
||||||
|
origin: item.source === "apm" ? "apm" : "spark",
|
||||||
|
retry: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 通过 IPC 发送到主下载队列
|
||||||
|
webContents.send("queue-install", JSON.stringify(installTaskData));
|
||||||
|
|
||||||
|
// 从更新中心的 items 中移除该应用(不再显示在更新列表中)
|
||||||
|
currentItems = currentItems.filter((i) => getTaskKey(i) !== getTaskKey(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新队列中的 items
|
||||||
|
queue.setItems(currentItems);
|
||||||
|
|
||||||
emit();
|
emit();
|
||||||
},
|
},
|
||||||
|
async cancel(taskKey) {
|
||||||
|
// 取消功能不再需要通过更新中心,直接忽略
|
||||||
|
console.log("Cancel not needed for task:", taskKey);
|
||||||
|
},
|
||||||
getState,
|
getState,
|
||||||
subscribe(listener) {
|
subscribe(listener) {
|
||||||
listeners.add(listener);
|
listeners.add(listener);
|
||||||
|
|||||||
@@ -36,16 +36,13 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="min-h-0 flex-1">
|
||||||
class="grid min-h-0 flex-1 gap-0 lg:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]"
|
|
||||||
>
|
|
||||||
<UpdateCenterList
|
<UpdateCenterList
|
||||||
:items="store.filteredItems.value"
|
:items="store.filteredItems.value"
|
||||||
:tasks="store.snapshot.value.tasks"
|
:tasks="store.snapshot.value.tasks"
|
||||||
:selected-task-keys="store.selectedTaskKeys.value"
|
:selected-task-keys="store.selectedTaskKeys.value"
|
||||||
@toggle-selection="emit('toggle-selection', $event)"
|
@toggle-selection="emit('toggle-selection', $event)"
|
||||||
/>
|
/>
|
||||||
<UpdateCenterLogPanel :tasks="store.snapshot.value.tasks" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UpdateCenterMigrationConfirm
|
<UpdateCenterMigrationConfirm
|
||||||
@@ -53,11 +50,6 @@
|
|||||||
@close="emit('dismiss-migration-confirm')"
|
@close="emit('dismiss-migration-confirm')"
|
||||||
@confirm="emit('confirm-migration-start')"
|
@confirm="emit('confirm-migration-start')"
|
||||||
/>
|
/>
|
||||||
<UpdateCenterCloseConfirm
|
|
||||||
:show="store.showCloseConfirm.value"
|
|
||||||
@close="emit('dismiss-close-confirm')"
|
|
||||||
@confirm="emit('confirm-close')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
@@ -68,9 +60,7 @@ import { computed } from "vue";
|
|||||||
|
|
||||||
import type { UpdateCenterStore } from "@/modules/updateCenter";
|
import type { UpdateCenterStore } from "@/modules/updateCenter";
|
||||||
|
|
||||||
import UpdateCenterCloseConfirm from "./update-center/UpdateCenterCloseConfirm.vue";
|
|
||||||
import UpdateCenterList from "./update-center/UpdateCenterList.vue";
|
import UpdateCenterList from "./update-center/UpdateCenterList.vue";
|
||||||
import UpdateCenterLogPanel from "./update-center/UpdateCenterLogPanel.vue";
|
|
||||||
import UpdateCenterMigrationConfirm from "./update-center/UpdateCenterMigrationConfirm.vue";
|
import UpdateCenterMigrationConfirm from "./update-center/UpdateCenterMigrationConfirm.vue";
|
||||||
import UpdateCenterToolbar from "./update-center/UpdateCenterToolbar.vue";
|
import UpdateCenterToolbar from "./update-center/UpdateCenterToolbar.vue";
|
||||||
|
|
||||||
@@ -80,8 +70,6 @@ const emit = defineEmits<{
|
|||||||
(e: "request-start-selected"): void;
|
(e: "request-start-selected"): void;
|
||||||
(e: "confirm-migration-start"): void;
|
(e: "confirm-migration-start"): void;
|
||||||
(e: "dismiss-migration-confirm"): void;
|
(e: "dismiss-migration-confirm"): void;
|
||||||
(e: "confirm-close"): void;
|
|
||||||
(e: "dismiss-close-confirm"): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import { computed, ref, type ComputedRef, type Ref } from "vue";
|
|||||||
import type {
|
import type {
|
||||||
UpdateCenterItem,
|
UpdateCenterItem,
|
||||||
UpdateCenterSnapshot,
|
UpdateCenterSnapshot,
|
||||||
|
DownloadItem,
|
||||||
} from "@/global/typedefinition";
|
} from "@/global/typedefinition";
|
||||||
|
import { downloads } from "@/global/downloadStatus";
|
||||||
|
import { APM_STORE_BASE_URL } from "@/global/storeConfig";
|
||||||
|
|
||||||
const EMPTY_SNAPSHOT: UpdateCenterSnapshot = {
|
const EMPTY_SNAPSHOT: UpdateCenterSnapshot = {
|
||||||
items: [],
|
items: [],
|
||||||
@@ -143,21 +146,51 @@ export const createUpdateCenterStore = (): UpdateCenterStore => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startSelected = async (): Promise<void> => {
|
const startSelected = async (): Promise<void> => {
|
||||||
const taskKeys = getSelectedItems().map((item) => item.taskKey);
|
const selectedItems = getSelectedItems();
|
||||||
|
const taskKeys = selectedItems.map((item) => item.taskKey);
|
||||||
|
|
||||||
if (taskKeys.length === 0) {
|
if (taskKeys.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 在前端创建下载项,这样用户能在下载列表中看到更新任务
|
||||||
|
const arch = window.apm_store.arch || "amd64";
|
||||||
|
let downloadIdCounter = downloads.value.length > 0 ? Math.max(...downloads.value.map(d => d.id)) + 1 : 1;
|
||||||
|
|
||||||
|
selectedItems.forEach((item) => {
|
||||||
|
// 检查任务是否已存在
|
||||||
|
if (!downloads.value.find(d => d.pkgname === item.packageName && d.origin === (item.source === "apm" ? "apm" : "spark"))) {
|
||||||
|
const finalArch = item.source === "apm" ? `${arch}-apm` : `${arch}-store`;
|
||||||
|
const download: DownloadItem = {
|
||||||
|
id: downloadIdCounter++,
|
||||||
|
name: item.displayName,
|
||||||
|
pkgname: item.packageName,
|
||||||
|
version: item.newVersion,
|
||||||
|
icon: `${APM_STORE_BASE_URL}/${finalArch}/unknown/${item.packageName}/icon.png`,
|
||||||
|
origin: item.source === "apm" ? "apm" : "spark",
|
||||||
|
status: "queued",
|
||||||
|
progress: 0,
|
||||||
|
downloadedSize: 0,
|
||||||
|
totalSize: item.size || 0,
|
||||||
|
speed: 0,
|
||||||
|
timeRemaining: 0,
|
||||||
|
startTime: Date.now(),
|
||||||
|
logs: [{ time: Date.now(), message: "开始更新..." }],
|
||||||
|
source: "Update Center",
|
||||||
|
retry: false,
|
||||||
|
upgradeOnly: true,
|
||||||
|
filename: item.fileName,
|
||||||
|
metalinkUrl: item.downloadUrl ? `${item.downloadUrl}.metalink` : undefined,
|
||||||
|
};
|
||||||
|
downloads.value.push(download);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await window.updateCenter.start(taskKeys);
|
await window.updateCenter.start(taskKeys);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestClose = (): void => {
|
const requestClose = (): void => {
|
||||||
if (snapshot.value.hasRunningTasks) {
|
// 直接关闭,不需要确认,因为任务在主下载队列中执行
|
||||||
showCloseConfirm.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
closeNow();
|
closeNow();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user