mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-04-12 23:50:31 +08:00
feat: add download progress and sha256 integrity verification for updates
This commit is contained in:
parent
f5cff160f2
commit
9ddb0f15ff
@ -4,8 +4,10 @@ import { existsSync } from 'fs'
|
||||
import os from 'os'
|
||||
import { exec, execSync, spawn } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { createHash } from 'crypto'
|
||||
import { app, shell } from 'electron'
|
||||
import i18next from 'i18next'
|
||||
import { mainWindow } from '../window'
|
||||
import { appLogger } from '../utils/logger'
|
||||
import { dataDir, exeDir, exePath, isPortable, resourcesFilesDir } from '../utils/dirs'
|
||||
import { getControledMihomoConfig } from '../config'
|
||||
@ -84,6 +86,15 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
||||
}
|
||||
try {
|
||||
if (!existsSync(path.join(dataDir(), file))) {
|
||||
const sha256Res = await chromeRequest.get(`${baseUrl}${file}.sha256`, {
|
||||
proxy: {
|
||||
protocol: 'http',
|
||||
host: '127.0.0.1',
|
||||
port: mixedPort
|
||||
},
|
||||
responseType: 'text'
|
||||
})
|
||||
const expectedHash = (sha256Res.data as string).trim().split(/\s+/)[0]
|
||||
const res = await chromeRequest.get(`${baseUrl}${file}`, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 0,
|
||||
@ -94,9 +105,21 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream'
|
||||
},
|
||||
onProgress: (loaded, total) => {
|
||||
mainWindow?.webContents.send('updateDownloadProgress', {
|
||||
status: 'downloading',
|
||||
percent: Math.round((loaded / total) * 100)
|
||||
})
|
||||
}
|
||||
})
|
||||
await writeFile(path.join(dataDir(), file), res.data as string | Buffer)
|
||||
mainWindow?.webContents.send('updateDownloadProgress', { status: 'verifying' })
|
||||
const fileBuffer = Buffer.from(res.data as ArrayBuffer)
|
||||
const actualHash = createHash('sha256').update(fileBuffer).digest('hex')
|
||||
if (actualHash.toLowerCase() !== expectedHash.toLowerCase()) {
|
||||
throw new Error(`File integrity check failed: expected ${expectedHash}, got ${actualHash}`)
|
||||
}
|
||||
await writeFile(path.join(dataDir(), file), fileBuffer)
|
||||
}
|
||||
if (file.endsWith('.exe')) {
|
||||
try {
|
||||
|
||||
@ -15,6 +15,7 @@ export interface RequestOptions {
|
||||
responseType?: 'text' | 'json' | 'arraybuffer'
|
||||
followRedirect?: boolean
|
||||
maxRedirects?: number
|
||||
onProgress?: (loaded: number, total: number) => void
|
||||
}
|
||||
|
||||
export interface Response<T = unknown> {
|
||||
@ -60,7 +61,8 @@ export async function request<T = unknown>(
|
||||
timeout = 30000,
|
||||
responseType = 'text',
|
||||
followRedirect = true,
|
||||
maxRedirects = 20
|
||||
maxRedirects = 20,
|
||||
onProgress
|
||||
} = options
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -124,8 +126,15 @@ export async function request<T = unknown>(
|
||||
responseHeaders[rawHeaders[i].toLowerCase()] = rawHeaders[i + 1]
|
||||
}
|
||||
|
||||
const totalSize = parseInt(responseHeaders['content-length'] || '0', 10)
|
||||
let loadedSize = 0
|
||||
|
||||
res.on('data', (chunk: Buffer) => {
|
||||
chunks.push(chunk)
|
||||
if (onProgress && totalSize > 0) {
|
||||
loadedSize += chunk.length
|
||||
onProgress(loadedSize, totalSize)
|
||||
}
|
||||
})
|
||||
|
||||
res.on('end', () => {
|
||||
|
||||
@ -162,7 +162,8 @@ const validListenChannels = [
|
||||
'controledMihomoConfigUpdated',
|
||||
'profileConfigUpdated',
|
||||
'groupsUpdated',
|
||||
'rulesUpdated'
|
||||
'rulesUpdated',
|
||||
'updateDownloadProgress'
|
||||
] as const
|
||||
|
||||
// 允许的 send channels 白名单
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '@heroui/react'
|
||||
import { toast } from '@renderer/components/base/toast'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { downloadAndInstallUpdate } from '@renderer/utils/ipc'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -22,15 +22,18 @@ interface Props {
|
||||
const UpdaterModal: React.FC<Props> = (props) => {
|
||||
const { version, changelog, onClose } = props
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const [progress, setProgress] = useState<{ status: 'downloading' | 'verifying'; percent?: number } | null>(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onUpdate = async (): Promise<void> => {
|
||||
try {
|
||||
await downloadAndInstallUpdate(version)
|
||||
} catch (e) {
|
||||
toast.error(String(e))
|
||||
useEffect(() => {
|
||||
const handler = (_e: Electron.IpcRendererEvent, ...args: unknown[]): void => {
|
||||
setProgress(args[0] as { status: 'downloading' | 'verifying'; percent?: number })
|
||||
}
|
||||
}
|
||||
window.electron.ipcRenderer.on('updateDownloadProgress', handler)
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeListener('updateDownloadProgress', handler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -69,28 +72,46 @@ const UpdaterModal: React.FC<Props> = (props) => {
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
isLoading={downloading}
|
||||
onPress={async () => {
|
||||
try {
|
||||
setDownloading(true)
|
||||
await onUpdate()
|
||||
onClose()
|
||||
} catch (e) {
|
||||
toast.error(String(e))
|
||||
} finally {
|
||||
setDownloading(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common.updater.update')}
|
||||
</Button>
|
||||
<ModalFooter className="flex-col gap-2 items-stretch">
|
||||
{downloading && progress && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="w-full bg-default-200 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-primary h-1.5 rounded-full transition-all duration-300"
|
||||
style={{ width: `${progress.status === 'verifying' ? 100 : (progress.percent ?? 0)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-foreground-400 text-center">
|
||||
{progress.status === 'verifying'
|
||||
? t('common.updater.verifying')
|
||||
: `${progress.percent ?? 0}%`}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
isLoading={downloading}
|
||||
onPress={async () => {
|
||||
try {
|
||||
setDownloading(true)
|
||||
await downloadAndInstallUpdate(version)
|
||||
onClose()
|
||||
} catch (e) {
|
||||
toast.detailedError(String(e))
|
||||
} finally {
|
||||
setDownloading(false)
|
||||
setProgress(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('common.updater.update')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"common.updater.versionReady": "v{{version}} Version Ready",
|
||||
"common.updater.goToDownload": "Go to Download",
|
||||
"common.updater.update": "Update",
|
||||
"common.updater.verifying": "Verifying...",
|
||||
"common.generateSecret": "Generate Secret",
|
||||
"common.refresh": "Refresh",
|
||||
"settings.general": "General Settings",
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"common.updater.versionReady": "نسخه v{{version}} آماده است",
|
||||
"common.updater.goToDownload": "دانلود",
|
||||
"common.updater.update": "بهروزرسانی",
|
||||
"common.updater.verifying": "در حال تأیید...",
|
||||
"settings.general": "تنظیمات عمومی",
|
||||
"settings.mihomo": "تنظیمات Mihomo",
|
||||
"settings.language": "زبان",
|
||||
|
||||
@ -67,6 +67,7 @@
|
||||
"common.updater.versionReady": "Обнаружено ядро с повышенными привилегиями",
|
||||
"common.updater.goToDownload": "Перейти к загрузке",
|
||||
"common.updater.update": "Обновить",
|
||||
"common.updater.verifying": "Проверка...",
|
||||
"settings.general": "Общие настройки",
|
||||
"settings.mihomo": "Настройки Mihomo",
|
||||
"settings.language": "Язык",
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"common.updater.versionReady": "v{{version}} 版本就绪",
|
||||
"common.updater.goToDownload": "前往下载",
|
||||
"common.updater.update": "更新",
|
||||
"common.updater.verifying": "正在校验...",
|
||||
"common.generateSecret": "生成密钥",
|
||||
"common.refresh": "刷新",
|
||||
"settings.general": "通用设置",
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
"common.updater.versionReady": "v{{version}} 版本就緒",
|
||||
"common.updater.goToDownload": "前往下載",
|
||||
"common.updater.update": "更新",
|
||||
"common.updater.verifying": "正在校驗...",
|
||||
"common.generateSecret": "生成密鑰",
|
||||
"common.refresh": "重新整理",
|
||||
"settings.general": "通用設置",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user