mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
feat(ota): verify offline update checksums
This commit is contained in:
@@ -39,10 +39,16 @@ func (s *Service) Update(c *gin.Context) {
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("update application success")
|
||||
|
||||
// Sleep for a second before restarting the device
|
||||
go restartServices()
|
||||
}
|
||||
|
||||
func restartServices() {
|
||||
// Let the HTTP response reach the client before stopping the server.
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
_ = exec.Command("sh", "-c", "/kvmapp/system/init.d/S95nanokvm restart").Run()
|
||||
if err := exec.Command("/kvmapp/system/init.d/S95nanokvm", "restart").Run(); err != nil {
|
||||
log.Errorf("failed to restart services after update: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func update() error {
|
||||
@@ -88,7 +94,7 @@ func download(url string, target string) (err error) {
|
||||
}
|
||||
|
||||
var req *http.Request
|
||||
req, err = http.NewRequest("GET", url, nil)
|
||||
req, err = utils.NewAuthenticatedRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
log.Errorf("new request err: %s", err)
|
||||
continue
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -35,11 +36,15 @@ func (s *Service) OfflineUpdate(c *gin.Context) {
|
||||
rsp.OkRsp(c)
|
||||
log.Debugf("offline update application success")
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
_ = exec.Command("sh", "-c", "/kvmapp/system/init.d/S95nanokvm restart").Run()
|
||||
go restartServices()
|
||||
}
|
||||
|
||||
func offlineUpdate(c *gin.Context) error {
|
||||
expectedSHA256, err := parseSHA256Checksum(c.GetHeader("X-SHA256-Checksum"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.RemoveAll(CacheDir)
|
||||
_ = os.MkdirAll(CacheDir, 0o755)
|
||||
defer func() {
|
||||
@@ -63,6 +68,11 @@ func offlineUpdate(c *gin.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := verifySHA256Checksum(target, expectedSHA256); err != nil {
|
||||
log.Errorf("failed to verify install package: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := installPackage(target); err != nil {
|
||||
log.Errorf("failed to install package: %v", err)
|
||||
return err
|
||||
@@ -71,6 +81,43 @@ func offlineUpdate(c *gin.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSHA256Checksum(value string) ([]byte, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
checksum, err := hex.DecodeString(value)
|
||||
if err != nil || len(checksum) != sha256.Size {
|
||||
return nil, fmt.Errorf("invalid sha256 checksum")
|
||||
}
|
||||
|
||||
return checksum, nil
|
||||
}
|
||||
|
||||
func verifySHA256Checksum(filePath string, expected []byte) error {
|
||||
if len(expected) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open uploaded file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return fmt.Errorf("failed to calculate sha256 checksum: %w", err)
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare(hasher.Sum(nil), expected) != 1 {
|
||||
return fmt.Errorf("sha256 checksum mismatch")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSentinelFile() error {
|
||||
file, err := os.OpenFile(
|
||||
sentinelPath,
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"NanoKVM-Server/proto"
|
||||
"NanoKVM-Server/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -32,7 +33,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
latestClient = &http.Client{Timeout: 15 * time.Second}
|
||||
latestClient = utils.NewUpdateHTTPClient(15 * time.Second)
|
||||
packageNamePattern = regexp.MustCompile(`^nanokvm_[0-9]+\.[0-9]+\.[0-9]+\.tar\.gz$`)
|
||||
versionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$`)
|
||||
)
|
||||
@@ -84,7 +85,11 @@ func getLatest() (*Latest, error) {
|
||||
query.Set("now", fmt.Sprintf("%d", time.Now().Unix()))
|
||||
parsedManifestURL.RawQuery = query.Encode()
|
||||
|
||||
resp, err := latestClient.Get(parsedManifestURL.String())
|
||||
request, err := utils.NewAuthenticatedRequest("GET", parsedManifestURL.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := latestClient.Do(request)
|
||||
if err != nil {
|
||||
log.Debugf("failed to request version from %s", parsedManifestURL.Redacted())
|
||||
return nil, errors.New("update server is inaccessible")
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -14,7 +16,55 @@ import (
|
||||
|
||||
const maxDownloadSize = int64(1024 * 1024 * 1024)
|
||||
|
||||
var downloadClient = &http.Client{Timeout: 15 * time.Minute}
|
||||
var downloadClient = NewUpdateHTTPClient(15 * time.Minute)
|
||||
|
||||
func NewAuthenticatedRequest(method string, rawURL string, body io.Reader) (*http.Request, error) {
|
||||
req, err := http.NewRequest(method, rawURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.URL.User != nil {
|
||||
username := req.URL.User.Username()
|
||||
password, _ := req.URL.User.Password()
|
||||
req.SetBasicAuth(username, password)
|
||||
// Keep credentials out of the request URL after copying them to the header.
|
||||
req.URL.User = nil
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func NewUpdateHTTPClient(timeout time.Duration) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: preserveBasicAuthRedirect,
|
||||
}
|
||||
}
|
||||
|
||||
func preserveBasicAuthRedirect(req *http.Request, via []*http.Request) error {
|
||||
if len(via) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
previous := via[len(via)-1]
|
||||
authorization := previous.Header.Get("Authorization")
|
||||
if authorization == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !sameUpdateHost(previous.URL, req.URL) ||
|
||||
(previous.URL.Scheme == "https" && req.URL.Scheme != "https") {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", authorization)
|
||||
return nil
|
||||
}
|
||||
|
||||
func sameUpdateHost(left *url.URL, right *url.URL) bool {
|
||||
return strings.EqualFold(left.Host, right.Host)
|
||||
}
|
||||
|
||||
func Download(req *http.Request, target string) error {
|
||||
log.Debugf("downloading %s to %s", req.URL.Redacted(), target)
|
||||
|
||||
@@ -21,11 +21,12 @@ export function update() {
|
||||
}
|
||||
|
||||
// offline update application
|
||||
export function offlineUpdate(data: FormData) {
|
||||
export function offlineUpdate(data: FormData, sha256Checksum = '') {
|
||||
const baseUrl = getBaseUrl('http');
|
||||
const url = `${baseUrl}/api/application/update/offline`;
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: sha256Checksum ? { 'X-SHA256-Checksum': sha256Checksum } : undefined,
|
||||
body: data
|
||||
});
|
||||
}
|
||||
|
||||
@@ -538,6 +538,11 @@ const ca = {
|
||||
title: 'Actualitzacions fora de línia',
|
||||
desc: "Actualització mitjançant el paquet d'instal·lació local",
|
||||
upload: 'Puja',
|
||||
checksumPlaceholder: 'Suma de verificació SHA-256 (opcional)',
|
||||
invalidChecksum:
|
||||
'La suma de verificació SHA-256 ha de contenir 64 caràcters hexadecimals.',
|
||||
checksumMismatch:
|
||||
'La verificació SHA-256 ha fallat. És possible que el paquet estigui malmès.',
|
||||
invalidName: 'Format de nom de fitxer no vàlid. Baixeu-lo des de les versions de GitHub.',
|
||||
updateFailed: 'Error en actualitzar. Torna-ho a intentar.'
|
||||
}
|
||||
|
||||
@@ -542,6 +542,9 @@ const cz = {
|
||||
title: 'Offline aktualizace',
|
||||
desc: 'Aktualizace prostřednictvím místního instalačního balíčku',
|
||||
upload: 'Nahrát',
|
||||
checksumPlaceholder: 'Kontrolní součet SHA-256 (volitelný)',
|
||||
invalidChecksum: 'Kontrolní součet SHA-256 musí obsahovat 64 hexadecimálních znaků.',
|
||||
checksumMismatch: 'Ověření SHA-256 se nezdařilo. Balíček může být poškozený.',
|
||||
invalidName: 'Neplatný formát souboru. Stáhněte si prosím z vydání GitHubu.',
|
||||
updateFailed: 'Aktualizace se nezdařila. Zkuste to prosím znovu.'
|
||||
}
|
||||
|
||||
@@ -540,6 +540,9 @@ const da = {
|
||||
title: 'Offline opdateringer',
|
||||
desc: 'Opdatering via lokal installationspakke',
|
||||
upload: 'Upload',
|
||||
checksumPlaceholder: 'SHA-256-kontrolsum (valgfri)',
|
||||
invalidChecksum: 'SHA-256-kontrolsummen skal indeholde 64 hexadecimale tegn.',
|
||||
checksumMismatch: 'SHA-256-verificeringen mislykkedes. Pakken kan være beskadiget.',
|
||||
invalidName: 'Ugyldigt filnavnsformat. Download venligst fra GitHub-udgivelser.',
|
||||
updateFailed: 'Opdatering fejlede. Prøv igen.'
|
||||
}
|
||||
|
||||
@@ -547,6 +547,10 @@ const de = {
|
||||
title: 'Offline Aktualisierung',
|
||||
desc: 'Über lokales Installationspaket aktualisieren',
|
||||
upload: 'Hochladen',
|
||||
checksumPlaceholder: 'SHA-256-Prüfsumme (optional)',
|
||||
invalidChecksum: 'Die SHA-256-Prüfsumme muss 64 hexadezimale Zeichen enthalten.',
|
||||
checksumMismatch:
|
||||
'Die SHA-256-Überprüfung ist fehlgeschlagen. Das Paket ist möglicherweise beschädigt.',
|
||||
invalidName:
|
||||
'Ungültiges Dateinamenformat. Bitte laden Sie von den GitHub-Releases herunter.',
|
||||
updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.'
|
||||
|
||||
@@ -536,6 +536,9 @@ const en = {
|
||||
title: 'Offline Updates',
|
||||
desc: 'Update through local installation package',
|
||||
upload: 'Upload',
|
||||
checksumPlaceholder: 'SHA-256 checksum (optional)',
|
||||
invalidChecksum: 'The SHA-256 checksum must contain 64 hexadecimal characters.',
|
||||
checksumMismatch: 'SHA-256 verification failed. The package may be corrupted.',
|
||||
invalidName: 'Invalid filename format. Please download from GitHub releases.',
|
||||
updateFailed: 'Update failed. Please retry.'
|
||||
}
|
||||
|
||||
@@ -545,6 +545,11 @@ const es = {
|
||||
title: 'Actualizaciones sin conexión',
|
||||
desc: 'Actualización a través del paquete de instalación local',
|
||||
upload: 'Subir',
|
||||
checksumPlaceholder: 'Suma de comprobación SHA-256 (opcional)',
|
||||
invalidChecksum:
|
||||
'La suma de comprobación SHA-256 debe contener 64 caracteres hexadecimales.',
|
||||
checksumMismatch:
|
||||
'La verificación SHA-256 ha fallado. Es posible que el paquete esté dañado.',
|
||||
invalidName:
|
||||
'Formato de nombre de archivo no válido. Descargue desde las versiones de GitHub.',
|
||||
updateFailed: 'La actualización falló. Por favor, inténtalo de nuevo.'
|
||||
|
||||
@@ -547,6 +547,9 @@ const fr = {
|
||||
title: 'Mises à jour hors ligne',
|
||||
desc: "Mise à jour via le package d'installation local",
|
||||
upload: 'Téléverser',
|
||||
checksumPlaceholder: 'Somme de contrôle SHA-256 (facultative)',
|
||||
invalidChecksum: 'La somme de contrôle SHA-256 doit contenir 64 caractères hexadécimaux.',
|
||||
checksumMismatch: 'La vérification SHA-256 a échoué. Le paquet est peut-être endommagé.',
|
||||
invalidName:
|
||||
'Format de nom de fichier invalide. Veuillez télécharger à partir des versions de GitHub.',
|
||||
updateFailed: 'Mise à jour échouée. Veuillez réessayer.'
|
||||
|
||||
@@ -544,6 +544,9 @@ const hu = {
|
||||
title: 'Offline frissítések',
|
||||
desc: 'Frissítés helyi telepítőcsomaggal',
|
||||
upload: 'Feltöltés',
|
||||
checksumPlaceholder: 'SHA-256 ellenőrzőösszeg (opcionális)',
|
||||
invalidChecksum: 'A SHA-256 ellenőrzőösszegnek 64 hexadecimális karakterből kell állnia.',
|
||||
checksumMismatch: 'Az SHA-256 ellenőrzése sikertelen. Lehet, hogy a csomag sérült.',
|
||||
invalidName: 'Érvénytelen fájlnévformátum. Kérjük, töltse le a GitHub kiadásaiból.',
|
||||
updateFailed: 'Frissítés sikertelen. Kérem, próbálja újra.'
|
||||
}
|
||||
|
||||
@@ -541,6 +541,9 @@ const id = {
|
||||
title: 'Pembaruan Offline',
|
||||
desc: 'Perbarui melalui paket instalasi lokal',
|
||||
upload: 'Mengunggah',
|
||||
checksumPlaceholder: 'Checksum SHA-256 (opsional)',
|
||||
invalidChecksum: 'Checksum SHA-256 harus berisi 64 karakter heksadesimal.',
|
||||
checksumMismatch: 'Verifikasi SHA-256 gagal. Paket mungkin rusak.',
|
||||
invalidName: 'Format nama file tidak valid. Silakan unduh dari rilis GitHub.',
|
||||
updateFailed: 'Gagal memperbarui, tolong coba lagi.'
|
||||
}
|
||||
|
||||
@@ -546,6 +546,10 @@ const it = {
|
||||
title: 'Aggiornamenti offline',
|
||||
desc: 'Aggiornamento tramite pacchetto di installazione locale',
|
||||
upload: 'Carica',
|
||||
checksumPlaceholder: 'Checksum SHA-256 (facoltativo)',
|
||||
invalidChecksum: 'Il checksum SHA-256 deve contenere 64 caratteri esadecimali.',
|
||||
checksumMismatch:
|
||||
'La verifica SHA-256 non è riuscita. Il pacchetto potrebbe essere danneggiato.',
|
||||
invalidName: 'Formato nome file non valido. Si prega di scaricare dalle versioni GitHub.',
|
||||
updateFailed: 'Aggiornamento fallito. Riprova.'
|
||||
}
|
||||
|
||||
@@ -544,6 +544,10 @@ const ja = {
|
||||
title: 'オフラインアップデート',
|
||||
desc: 'ローカルインストールパッケージでアップデートする',
|
||||
upload: 'アップロード',
|
||||
checksumPlaceholder: 'SHA-256チェックサム(任意)',
|
||||
invalidChecksum: 'SHA-256チェックサムは64文字の16進数である必要があります。',
|
||||
checksumMismatch:
|
||||
'SHA-256の検証に失敗しました。パッケージが破損している可能性があります。',
|
||||
invalidName:
|
||||
'ファイル名の形式が正しくありません。GitHub リリースページにアクセスしてインストールパッケージをダウンロードしてください。',
|
||||
updateFailed: 'アップデートに失敗しました。もう一度お試しください。'
|
||||
|
||||
@@ -535,6 +535,9 @@ const ko = {
|
||||
title: '오프라인 업데이트',
|
||||
desc: '로컬 설치 패키지를 통한 업데이트',
|
||||
upload: '업로드',
|
||||
checksumPlaceholder: 'SHA-256 체크섬(선택 사항)',
|
||||
invalidChecksum: 'SHA-256 체크섬은 64개의 16진수 문자를 포함해야 합니다.',
|
||||
checksumMismatch: 'SHA-256 검증에 실패했습니다. 패키지가 손상되었을 수 있습니다.',
|
||||
invalidName: '유효하지 않은 파일 이름 형식입니다. GitHub 릴리즈에서 다운로드하세요.',
|
||||
updateFailed: '업데이트에 실패했습니다. 재시도하세요.'
|
||||
}
|
||||
|
||||
@@ -540,6 +540,9 @@ const nb = {
|
||||
title: 'Offline oppdateringer',
|
||||
desc: 'Oppdater gjennom lokal installasjonspakke',
|
||||
upload: 'Last opp',
|
||||
checksumPlaceholder: 'SHA-256-sjekksum (valgfritt)',
|
||||
invalidChecksum: 'SHA-256-sjekksummen må inneholde 64 heksadesimale tegn.',
|
||||
checksumMismatch: 'SHA-256-verifiseringen mislyktes. Pakken kan være skadet.',
|
||||
invalidName: 'Ugyldig filnavnformat. Last ned fra GitHub-utgivelser.',
|
||||
updateFailed: 'En feil oppstod under oppdatering. Vennligst forsøk igjen.'
|
||||
}
|
||||
|
||||
@@ -546,6 +546,9 @@ const nl = {
|
||||
title: 'Offline-updates',
|
||||
desc: 'Update via lokaal installatiepakket',
|
||||
upload: 'Uploaden',
|
||||
checksumPlaceholder: 'SHA-256-controlesom (optioneel)',
|
||||
invalidChecksum: 'De SHA-256-controlesom moet 64 hexadecimale tekens bevatten.',
|
||||
checksumMismatch: 'De SHA-256-verificatie is mislukt. Het pakket is mogelijk beschadigd.',
|
||||
invalidName: 'Ongeldig bestandsnaamformaat. Download de versie van GitHub-releases.',
|
||||
updateFailed: 'Update mislukt. Probeer het opnieuw.'
|
||||
}
|
||||
|
||||
@@ -544,6 +544,9 @@ const pl = {
|
||||
title: 'Aktualizacje offline',
|
||||
desc: 'Aktualizacja poprzez lokalny pakiet instalacyjny',
|
||||
upload: 'Prześlij',
|
||||
checksumPlaceholder: 'Suma kontrolna SHA-256 (opcjonalnie)',
|
||||
invalidChecksum: 'Suma kontrolna SHA-256 musi zawierać 64 znaki szesnastkowe.',
|
||||
checksumMismatch: 'Weryfikacja SHA-256 nie powiodła się. Pakiet może być uszkodzony.',
|
||||
invalidName: 'Nieprawidłowy format nazwy pliku. Proszę pobrać z wydań GitHub.',
|
||||
updateFailed: 'Aktualizacja nie powiodła się. Spróbuj ponownie.'
|
||||
}
|
||||
|
||||
@@ -542,6 +542,9 @@ const pt_br = {
|
||||
title: 'Atualizações off-line',
|
||||
desc: 'Atualização através do pacote de instalação local',
|
||||
upload: 'Upload',
|
||||
checksumPlaceholder: 'Soma de verificação SHA-256 (opcional)',
|
||||
invalidChecksum: 'A soma de verificação SHA-256 deve conter 64 caracteres hexadecimais.',
|
||||
checksumMismatch: 'A verificação SHA-256 falhou. O pacote pode estar corrompido.',
|
||||
invalidName: 'Formato de nome de arquivo inválido. Faça download das versões do GitHub.',
|
||||
updateFailed: 'Falha na atualização. Por favor, tente novamente.'
|
||||
}
|
||||
|
||||
@@ -543,6 +543,10 @@ const ru = {
|
||||
title: 'Автономные обновления',
|
||||
desc: 'Обновление через локальный установочный пакет',
|
||||
upload: 'Загрузить',
|
||||
checksumPlaceholder: 'Контрольная сумма SHA-256 (необязательно)',
|
||||
invalidChecksum:
|
||||
'Контрольная сумма SHA-256 должна содержать 64 шестнадцатеричных символа.',
|
||||
checksumMismatch: 'Проверка SHA-256 не пройдена. Возможно, пакет повреждён.',
|
||||
invalidName: 'Неверный формат имени файла. Загрузите выпуски с GitHub.',
|
||||
updateFailed: 'Обновление не удалось. Пожалуйста, попробуйте еще раз.'
|
||||
}
|
||||
|
||||
@@ -538,6 +538,9 @@ const se = {
|
||||
title: 'Offlineuppdateringar',
|
||||
desc: 'Uppdatera genom lokalt installationspaket',
|
||||
upload: 'Ladda upp',
|
||||
checksumPlaceholder: 'SHA-256-kontrollsumma (valfri)',
|
||||
invalidChecksum: 'SHA-256-kontrollsumman måste innehålla 64 hexadecimala tecken.',
|
||||
checksumMismatch: 'SHA-256-verifieringen misslyckades. Paketet kan vara skadat.',
|
||||
invalidName: 'Ogiltigt filnamnsformat. Ladda ner från GitHub-versioner.',
|
||||
updateFailed: 'Uppdatering misslyckades. Försök igen.'
|
||||
}
|
||||
|
||||
@@ -533,6 +533,9 @@ const th = {
|
||||
title: 'อัปเดตออฟไลน์',
|
||||
desc: 'อัปเดตผ่านแพ็คเกจการติดตั้งในเครื่อง',
|
||||
upload: 'อัปโหลด',
|
||||
checksumPlaceholder: 'ผลรวมตรวจสอบ SHA-256 (ไม่บังคับ)',
|
||||
invalidChecksum: 'ผลรวมตรวจสอบ SHA-256 ต้องมีอักขระเลขฐานสิบหก 64 ตัว',
|
||||
checksumMismatch: 'การตรวจสอบ SHA-256 ล้มเหลว แพ็กเกจอาจเสียหาย',
|
||||
invalidName: 'รูปแบบชื่อไฟล์ไม่ถูกต้อง กรุณาดาวน์โหลดจากรุ่น GitHub',
|
||||
updateFailed: 'การอัปเดตล้มเหลว กรุณาลองใหม่'
|
||||
}
|
||||
|
||||
@@ -542,6 +542,9 @@ const tr = {
|
||||
title: 'Çevrimdışı Güncellemeler',
|
||||
desc: 'Yerel kurulum paketi aracılığıyla güncelleme',
|
||||
upload: 'Yükle',
|
||||
checksumPlaceholder: 'SHA-256 sağlama toplamı (isteğe bağlı)',
|
||||
invalidChecksum: 'SHA-256 sağlama toplamı 64 onaltılık karakter içermelidir.',
|
||||
checksumMismatch: 'SHA-256 doğrulaması başarısız oldu. Paket bozulmuş olabilir.',
|
||||
invalidName: 'Geçersiz dosya adı biçimi. Lütfen GitHub sürümlerinden indirin.',
|
||||
updateFailed: 'Güncelleme başarısız oldu. Lütfen tekrar deneyin.'
|
||||
}
|
||||
|
||||
@@ -541,6 +541,9 @@ const uk = {
|
||||
title: 'Оновлення в автономному режимі',
|
||||
desc: 'Оновлення через локальний інсталяційний пакет',
|
||||
upload: 'Завантажити',
|
||||
checksumPlaceholder: "Контрольна сума SHA-256 (необов'язково)",
|
||||
invalidChecksum: 'Контрольна сума SHA-256 має містити 64 шістнадцяткові символи.',
|
||||
checksumMismatch: 'Не вдалося перевірити SHA-256. Пакунок може бути пошкоджений.',
|
||||
invalidName: 'Недійсний формат імені файлу. Завантажте випуски з GitHub.',
|
||||
updateFailed: 'Оновлення не вдалося. Будь ласка, спробуйте ще раз.'
|
||||
}
|
||||
|
||||
@@ -539,6 +539,9 @@ const vi = {
|
||||
title: 'Cập nhật ngoại tuyến',
|
||||
desc: 'Cập nhật thông qua gói cài đặt cục bộ',
|
||||
upload: 'Tải lên',
|
||||
checksumPlaceholder: 'Tổng kiểm SHA-256 (không bắt buộc)',
|
||||
invalidChecksum: 'Tổng kiểm SHA-256 phải chứa 64 ký tự thập lục phân.',
|
||||
checksumMismatch: 'Xác minh SHA-256 không thành công. Gói có thể đã bị hỏng.',
|
||||
invalidName:
|
||||
'Định dạng tên tệp không hợp lệ. Vui lòng tải xuống từ bản phát hành GitHub.',
|
||||
updateFailed: 'Cập nhật thất bại. Vui lòng thử lại.'
|
||||
|
||||
@@ -525,6 +525,9 @@ const zh = {
|
||||
title: '离线更新',
|
||||
desc: '通过本地安装包进行更新',
|
||||
upload: '上传',
|
||||
checksumPlaceholder: 'SHA-256 校验和(可选)',
|
||||
invalidChecksum: 'SHA-256 校验和必须为 64 位十六进制字符。',
|
||||
checksumMismatch: 'SHA-256 校验失败,安装包可能已损坏。',
|
||||
invalidName: '文件名格式错误,请前往 GitHub 发布页下载安装包。',
|
||||
updateFailed: '更新失败,请重试'
|
||||
}
|
||||
|
||||
@@ -525,6 +525,9 @@ const zh_tw = {
|
||||
title: '離線更新',
|
||||
desc: '透過本地安裝包進行更新',
|
||||
upload: '上傳',
|
||||
checksumPlaceholder: 'SHA-256 校驗和(選填)',
|
||||
invalidChecksum: 'SHA-256 校驗和必須包含 64 個十六進位字元。',
|
||||
checksumMismatch: 'SHA-256 驗證失敗。套件可能已損毀。',
|
||||
invalidName: '檔名格式錯誤,請前往 GitHub 釋出頁下載安裝包。',
|
||||
updateFailed: '更新失敗,請重試'
|
||||
}
|
||||
|
||||
@@ -10,9 +10,14 @@ const OFFICIAL_UPDATE_SERVER = 'https://cdn.sipeed.com/nanokvm';
|
||||
interface CustomServerProps {
|
||||
checkForUpdates: () => void;
|
||||
onEnabledChange: (enabled: boolean) => void;
|
||||
onPendingChange: (pending: boolean) => void;
|
||||
}
|
||||
|
||||
export const CustomServer = ({ checkForUpdates, onEnabledChange }: CustomServerProps) => {
|
||||
export const CustomServer = ({
|
||||
checkForUpdates,
|
||||
onEnabledChange,
|
||||
onPendingChange
|
||||
}: CustomServerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [savedConfig, setSavedConfig] = useState<api.UpdateServerConfig>({
|
||||
@@ -99,7 +104,6 @@ export const CustomServer = ({ checkForUpdates, onEnabledChange }: CustomServerP
|
||||
function confirmRisk() {
|
||||
setEnabled(true);
|
||||
setError('');
|
||||
onEnabledChange(true);
|
||||
setIsConfirmOpen(false);
|
||||
}
|
||||
|
||||
@@ -136,6 +140,11 @@ export const CustomServer = ({ checkForUpdates, onEnabledChange }: CustomServerP
|
||||
}
|
||||
|
||||
const hasChanges = enabled !== savedConfig.enabled || url.trim() !== savedConfig.url;
|
||||
|
||||
useEffect(() => {
|
||||
onPendingChange(hasChanges);
|
||||
}, [hasChanges, onPendingChange]);
|
||||
|
||||
const modalTitle = (
|
||||
<div className="flex items-center space-x-1 text-red-500">
|
||||
<TriangleAlertIcon size={18} />
|
||||
|
||||
@@ -22,6 +22,7 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
|
||||
const [latestVersion, setLatestVersion] = useState('');
|
||||
const [errMsg, setErrMsg] = useState('');
|
||||
const [isCustomServerEnabled, setIsCustomServerEnabled] = useState(false);
|
||||
const [isCustomServerPending, setIsCustomServerPending] = useState(false);
|
||||
const versionRequestRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,7 +61,7 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (status !== 'outdated') return;
|
||||
if (status !== 'outdated' || isCustomServerPending) return;
|
||||
|
||||
setIsLocked(true);
|
||||
setStatus('updating');
|
||||
@@ -88,8 +89,15 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
|
||||
<div className="text-base">{t('settings.update.title')}</div>
|
||||
<Divider className="opacity-50" />
|
||||
|
||||
<Preview checkForUpdates={checkForUpdates} disabled={isCustomServerEnabled} />
|
||||
<CustomServer checkForUpdates={checkForUpdates} onEnabledChange={setIsCustomServerEnabled} />
|
||||
<Preview
|
||||
checkForUpdates={checkForUpdates}
|
||||
disabled={isCustomServerEnabled || isCustomServerPending}
|
||||
/>
|
||||
<CustomServer
|
||||
checkForUpdates={checkForUpdates}
|
||||
onEnabledChange={setIsCustomServerEnabled}
|
||||
onPendingChange={setIsCustomServerPending}
|
||||
/>
|
||||
<Offline
|
||||
status={status}
|
||||
setStatus={setStatus}
|
||||
@@ -133,7 +141,12 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
|
||||
title={`${currentVersion} -> ${latestVersion}`}
|
||||
subTitle={t('settings.update.available')}
|
||||
extra={[
|
||||
<Button key="confirm" type="primary" onClick={update}>
|
||||
<Button
|
||||
key="confirm"
|
||||
type="primary"
|
||||
disabled={isCustomServerPending}
|
||||
onClick={update}
|
||||
>
|
||||
{t('settings.update.confirm')}
|
||||
</Button>
|
||||
]}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Button, Input } from 'antd';
|
||||
import { ExternalLinkIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -16,6 +16,7 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
|
||||
const { t } = useTranslation();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [sha256Checksum, setSha256Checksum] = useState('');
|
||||
|
||||
function handleClick() {
|
||||
inputRef.current?.click();
|
||||
@@ -26,6 +27,7 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
e.target.value = '';
|
||||
|
||||
if (!validateFilename(file.name)) {
|
||||
setStatus('failed');
|
||||
@@ -39,6 +41,13 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
|
||||
function upload(file: File | null) {
|
||||
if (!file) return;
|
||||
|
||||
const checksum = sha256Checksum.trim();
|
||||
if (checksum && !/^[a-fA-F0-9]{64}$/.test(checksum)) {
|
||||
setStatus('failed');
|
||||
setErrMsg(t('settings.update.offline.invalidChecksum'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateFilename(file.name)) {
|
||||
setStatus('failed');
|
||||
setErrMsg(t('settings.update.offline.invalidName'));
|
||||
@@ -57,28 +66,30 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
|
||||
formData.append('file', file);
|
||||
|
||||
api
|
||||
.offlineUpdate(formData)
|
||||
.offlineUpdate(formData, checksum)
|
||||
.then((rsp: Response) => {
|
||||
// Prüfen ob HTTP OK
|
||||
if (!rsp.ok) throw new Error(`HTTP error ${rsp.status}`);
|
||||
return rsp.json(); // JSON-Payload parsen
|
||||
return rsp.json();
|
||||
})
|
||||
.then((rspj: any) => {
|
||||
// Jetzt rspj ist das tatsächliche JSON
|
||||
if (rspj.code !== 0 || !rspj.data) {
|
||||
setStatus('failed');
|
||||
setErrMsg(rspj.msg || t('settings.update.offline.updateFailed'));
|
||||
console.log(rspj);
|
||||
return;
|
||||
if (rspj.code !== 0) {
|
||||
const message = rspj.msg?.includes('sha256 checksum mismatch')
|
||||
? t('settings.update.offline.checksumMismatch')
|
||||
: rspj.msg || t('settings.update.offline.updateFailed');
|
||||
throw new Error(message);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
setTimeout(() => {
|
||||
setIsLocked(false);
|
||||
setErrMsg('');
|
||||
|
||||
window.location.reload();
|
||||
}, 12000);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
setIsLocked(false);
|
||||
setStatus('failed');
|
||||
setErrMsg(
|
||||
error instanceof Error ? error.message : t('settings.update.offline.updateFailed')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -89,31 +100,44 @@ export const Offline = ({ status, setStatus, setIsLocked, setErrMsg }: UpdatePro
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{t('settings.update.offline.title')}</span>
|
||||
<div className="mt-8 flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{t('settings.update.offline.title')}</span>
|
||||
|
||||
<a
|
||||
className="flex items-center text-neutral-500 hover:text-blue-500"
|
||||
href="https://github.com/sipeed/NanoKVM/releases"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLinkIcon size={15} />
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center text-neutral-500 hover:text-blue-500"
|
||||
href="https://github.com/sipeed/NanoKVM/releases"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLinkIcon size={15} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-neutral-500">{t('settings.update.offline.desc')}</span>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-neutral-500">{t('settings.update.offline.desc')}</span>
|
||||
<input
|
||||
id="file-upload"
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept=".tar.gz"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button disabled={status === 'loading' || status === 'updating'} onClick={handleClick}>
|
||||
{t('settings.update.offline.upload')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
id="file-upload"
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
<Input
|
||||
value={sha256Checksum}
|
||||
maxLength={64}
|
||||
disabled={status === 'updating'}
|
||||
placeholder={t('settings.update.offline.checksumPlaceholder')}
|
||||
onChange={(event) => setSha256Checksum(event.target.value)}
|
||||
/>
|
||||
<Button onClick={handleClick}>{t('settings.update.offline.upload')}</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user