mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 13:40:11 +08:00
更新中心扫描修复:
- updateCenter.ts: 抽出 runSystemUpdateThenLoad, open/refresh 共用,
打开时即刷新双源(aptss ssupdate + apm update), 失败仅告警不阻断扫描
- shell-caller.sh: ssupdate 分支 exit $? 恒 0 吞掉刷新失败, 改为 exit $exit_code
- update-center/query.ts: 移除 nextVersion===currentVersion 误删真实更新项逻辑,
信任 aptss 上游 upgradable 判断
安全加固:
- AppDetailModal.vue: 新增 sanitizeMoreContent 剥除 HTML 标签后再 v-html, 防 XSS
- InstalledAppsModal.vue: ALLOWED_LOCAL_ICON_PREFIXES 收紧为具体子目录, 缩小
本地文件读取面
- install-manager.ts: filename 用 path.basename 防路径遍历; 包名/文件名 PKGNAME_PATTERN 校验
- index.ts: 临时目录改用 spark-store-${pid} 隔离, will-quit 清理对应目录
经 7 维度专业审计(安全/功能/类型/可维护/资源/性能/兼容)通过。
201 lines
5.3 KiB
TypeScript
201 lines
5.3 KiB
TypeScript
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
import { runAria2Download, type Aria2DownloadResult } from "./download";
|
|
import { installPackage } from "../shared-installer";
|
|
import type { UpdateCenterQueue, UpdateCenterTask } from "./queue";
|
|
import type { UpdateCenterItem } from "./types";
|
|
|
|
const DEFAULT_DOWNLOAD_ROOT = join(
|
|
tmpdir(),
|
|
`spark-store-${process.pid}`,
|
|
"update-center",
|
|
);
|
|
|
|
export interface InstallUpdateItemOptions {
|
|
item: UpdateCenterItem;
|
|
filePath?: string;
|
|
superUserCmd?: string;
|
|
onLog?: (message: string) => void;
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
export interface TaskRunnerDownloadContext {
|
|
item: UpdateCenterItem;
|
|
task: UpdateCenterTask;
|
|
onProgress: (progress: number) => void;
|
|
onLog: (message: string) => void;
|
|
signal: AbortSignal;
|
|
}
|
|
|
|
export interface TaskRunnerInstallContext {
|
|
item: UpdateCenterItem;
|
|
task: UpdateCenterTask;
|
|
filePath?: string;
|
|
superUserCmd?: string;
|
|
onLog: (message: string) => void;
|
|
signal: AbortSignal;
|
|
}
|
|
|
|
export interface TaskRunnerDependencies {
|
|
runDownload?: (
|
|
context: TaskRunnerDownloadContext,
|
|
) => Promise<Aria2DownloadResult>;
|
|
installItem?: (context: TaskRunnerInstallContext) => Promise<void>;
|
|
}
|
|
|
|
export interface UpdateCenterTaskRunner {
|
|
runNextTask: () => Promise<UpdateCenterTask | null>;
|
|
cancelActiveTask: () => void;
|
|
}
|
|
|
|
export interface CreateTaskRunnerOptions extends TaskRunnerDependencies {
|
|
superUserCmd?: string;
|
|
}
|
|
|
|
/**
|
|
* 安装更新项
|
|
* 使用与商店安装相同的逻辑
|
|
*/
|
|
export const installUpdateItem = async ({
|
|
item,
|
|
filePath,
|
|
superUserCmd,
|
|
onLog,
|
|
signal,
|
|
}: InstallUpdateItemOptions): Promise<void> => {
|
|
if (!filePath) {
|
|
throw new Error(
|
|
`Update task for ${item.pkgname} requires downloaded package file`,
|
|
);
|
|
}
|
|
|
|
// 使用与商店安装相同的安装逻辑
|
|
const origin = item.source === "apm" ? "apm" : "spark";
|
|
|
|
await installPackage({
|
|
pkgname: item.pkgname,
|
|
filePath,
|
|
origin,
|
|
superUserCmd,
|
|
onLog,
|
|
signal,
|
|
});
|
|
};
|
|
|
|
export const createTaskRunner = (
|
|
queue: UpdateCenterQueue,
|
|
options: CreateTaskRunnerOptions = {},
|
|
): UpdateCenterTaskRunner => {
|
|
const runDownload =
|
|
options.runDownload ??
|
|
((context: TaskRunnerDownloadContext) =>
|
|
runAria2Download({
|
|
item: context.item,
|
|
downloadDir: join(DEFAULT_DOWNLOAD_ROOT, context.item.pkgname),
|
|
onProgress: context.onProgress,
|
|
onLog: context.onLog,
|
|
signal: context.signal,
|
|
}));
|
|
const installItem =
|
|
options.installItem ??
|
|
((context: TaskRunnerInstallContext) =>
|
|
installUpdateItem({
|
|
item: context.item,
|
|
filePath: context.filePath,
|
|
superUserCmd: context.superUserCmd,
|
|
onLog: context.onLog,
|
|
signal: context.signal,
|
|
}));
|
|
let inFlightTask: Promise<UpdateCenterTask | null> | null = null;
|
|
let activeAbortController: AbortController | null = null;
|
|
let activeTaskId: number | null = null;
|
|
|
|
return {
|
|
cancelActiveTask: () => {
|
|
activeAbortController?.abort();
|
|
},
|
|
runNextTask: async () => {
|
|
if (inFlightTask) {
|
|
return null;
|
|
}
|
|
|
|
inFlightTask = (async () => {
|
|
const task = queue.getNextQueuedTask();
|
|
if (!task) {
|
|
return null;
|
|
}
|
|
|
|
activeTaskId = task.id;
|
|
activeAbortController = new AbortController();
|
|
|
|
const onLog = (message: string) => {
|
|
queue.appendTaskLog(task.id, message);
|
|
};
|
|
|
|
try {
|
|
// All updates require download metadata
|
|
if (!task.item.downloadUrl || !task.item.fileName) {
|
|
throw new Error(
|
|
`Update task for ${task.item.pkgname} requires download metadata (URL and filename)`,
|
|
);
|
|
}
|
|
|
|
queue.markActiveTask(task.id, "downloading");
|
|
const result = await runDownload({
|
|
item: task.item,
|
|
task,
|
|
onLog,
|
|
signal: activeAbortController.signal,
|
|
onProgress: (progress) => {
|
|
queue.updateTaskProgress(task.id, progress);
|
|
},
|
|
});
|
|
const filePath = result.filePath;
|
|
|
|
queue.markActiveTask(task.id, "installing");
|
|
await installItem({
|
|
item: task.item,
|
|
task,
|
|
filePath,
|
|
superUserCmd: options.superUserCmd,
|
|
onLog,
|
|
signal: activeAbortController.signal,
|
|
});
|
|
|
|
const currentTask = queue
|
|
.getSnapshot()
|
|
.tasks.find((entry) => entry.id === task.id);
|
|
if (currentTask?.status !== "cancelled") {
|
|
queue.finishTask(task.id, "completed");
|
|
}
|
|
return task;
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : String(error);
|
|
const currentTask = queue
|
|
.getSnapshot()
|
|
.tasks.find((entry) => entry.id === task.id);
|
|
if (currentTask?.status !== "cancelled") {
|
|
queue.appendTaskLog(task.id, message);
|
|
queue.finishTask(task.id, "failed", message);
|
|
}
|
|
return task;
|
|
} finally {
|
|
activeAbortController = null;
|
|
activeTaskId = null;
|
|
}
|
|
})();
|
|
|
|
try {
|
|
return await inFlightTask;
|
|
} finally {
|
|
inFlightTask = null;
|
|
if (activeTaskId === null) {
|
|
activeAbortController = null;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
};
|