From 8da7a70bfc7daa1fc4c7dac8cc487a5d9a2d9632 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:23:40 +0100 Subject: [PATCH 01/43] Update download.tsx --- web/src/pages/desktop/menu/download.tsx | 89 ++++++++++++++++++++----- 1 file changed, 74 insertions(+), 15 deletions(-) diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index dd21e9a..a69ed55 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -21,6 +21,7 @@ export const DownloadImage = () => { const [popoverKey, setPopoverKey] = useState(0); const inputRef = useRef(null); + const [selectedFile, setSelectedFile] = useState(null); const intervalId = useRef(undefined); @@ -110,6 +111,22 @@ export const DownloadImage = () => { }); } + function handleFileChange(e: React.ChangeEvent) { + setSelectedFile(e.target.files?.[0] ?? null); + } + + function upload(file: File | null) { + if (!file) return; + + const formData = new FormData(); + formData.append("file", file); + + fetch("/api/download/file", { + method: "POST", + body: formData, + }); + } + const content = (
@@ -122,21 +139,63 @@ export const DownloadImage = () => {
{t('download.disabled')}
) : ( <> -
{t('download.input')}
-
- - +
+
{t('download.input')}
+
+ + +
+
+
+
{t('upload.input')}
+
+
{ + e.preventDefault(); + }} + onDrop={(e) => { + e.preventDefault(); + const file = e.dataTransfer.files?.[0] ?? null; + setSelectedFile(file); + }} + onClick={() => document.getElementById("file-upload")?.click()} + > + + {selectedFile ? selectedFile.name : "Datei hier ablegen oder klicken zum Auswählen"} + + + +
+ +
)} From 9b629c8dc089980b6ff7722c126729cce93f44f6 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Tue, 9 Dec 2025 23:49:34 +0100 Subject: [PATCH 02/43] Update download.tsx --- web/src/pages/desktop/menu/download.tsx | 91 +++++++++++++++++++++---- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index dd21e9a..5839ad1 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -21,6 +21,7 @@ export const DownloadImage = () => { const [popoverKey, setPopoverKey] = useState(0); const inputRef = useRef(null); + const [selectedFile, setSelectedFile] = useState(null); const intervalId = useRef(undefined); @@ -110,6 +111,24 @@ export const DownloadImage = () => { }); } + function handleFileChange(e: React.ChangeEvent) { + setSelectedFile(e.target.files?.[0] ?? null); + } + + function upload(file: File | null) { + if (!file) return; + + const formData = new FormData(); + formData.append("file", file); + + console.log(file); + + fetch("/api/download/file", { + method: "POST", + body: formData, + }); + } + const content = (
@@ -122,21 +141,63 @@ export const DownloadImage = () => {
{t('download.disabled')}
) : ( <> -
{t('download.input')}
-
- - +
+
{t('download.input')}
+
+ + +
+
+
+
{t('upload.input')}
+
+
{ + e.preventDefault(); + }} + onDrop={(e) => { + e.preventDefault(); + const file = e.dataTransfer.files?.[0] ?? null; + setSelectedFile(file); + }} + onClick={() => document.getElementById("file-upload")?.click()} + > + + {selectedFile ? selectedFile.name : t('download.uploadbox')} + + + +
+ +
)} From 9e4408409f18df1adec8285156d662bf1aaf0541 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Wed, 10 Dec 2025 00:10:04 +0100 Subject: [PATCH 03/43] Update de.ts --- web/src/i18n/locales/de.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index 5658f42..2733d79 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -168,7 +168,8 @@ const de = { title: 'Systemabbild Downloader', input: 'Bitte geben Sie die URL für das Remote-Systemabbild ein', ok: 'Ok', - disabled: '/data Partition ist nur-lesbar, daher kann das Systemabbild nicht heruntergeladen werden' + disabled: '/data Partition ist nur-lesbar, daher kann das Systemabbild nicht heruntergeladen werden', + uploadbox: "Datei hier ablegen oder klicken zum Auswählen" }, power: { title: 'Power', From 3d5c14405c36f6033c9c096d08b2f5fc4b052c7f Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Wed, 10 Dec 2025 00:10:29 +0100 Subject: [PATCH 04/43] Update en.ts --- web/src/i18n/locales/en.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index b1fc6f5..4bf8ae1 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -183,7 +183,8 @@ const en = { title: 'Image Downloader', input: 'Please enter a remote image URL', ok: 'Ok', - disabled: '/data partition is RO, so we cannot download the image' + disabled: '/data partition is RO, so we cannot download the image', + uploadbox: "Drop file here or click to select" }, power: { title: 'Power', From cb3834bb71d3ccbdbb60def7402db0e86f913a9c Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Wed, 10 Dec 2025 00:24:04 +0100 Subject: [PATCH 05/43] Update en.ts --- web/src/i18n/locales/en.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index b1fc6f5..4bf8ae1 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -183,7 +183,8 @@ const en = { title: 'Image Downloader', input: 'Please enter a remote image URL', ok: 'Ok', - disabled: '/data partition is RO, so we cannot download the image' + disabled: '/data partition is RO, so we cannot download the image', + uploadbox: "Drop file here or click to select" }, power: { title: 'Power', From f27102d344441db580f805a53108534dc54b8f4c Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Wed, 10 Dec 2025 00:25:33 +0100 Subject: [PATCH 06/43] Update download.tsx --- web/src/pages/desktop/menu/download.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index 5345f85..fb7d92e 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -22,6 +22,7 @@ export const DownloadImage = () => { const inputRef = useRef(null); const [selectedFile, setSelectedFile] = useState(null); + const [isDragging, setIsDragging] = useState(false); const intervalId = useRef(undefined); @@ -121,6 +122,8 @@ export const DownloadImage = () => { const formData = new FormData(); formData.append("file", file); + console.log(file); + fetch("/api/download/file", { method: "POST", body: formData, @@ -163,16 +166,24 @@ export const DownloadImage = () => {
{ - e.preventDefault(); - }} + style={isDragging ? { borderColor: "#1668dc" } : {}} onDrop={(e) => { e.preventDefault(); + setIsDragging(false); const file = e.dataTransfer.files?.[0] ?? null; setSelectedFile(file); }} + onDragOver={(e) => { + e.preventDefault(); + setIsDragging(true); // Datei wird über den Bereich gezogen + }} + onDragLeave={(e) => { + e.preventDefault(); + setIsDragging(false); // Maus verlässt Bereich + }} onClick={() => document.getElementById("file-upload")?.click()} > From cd954b6f246098292faefbc83f9f8386e1854c54 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Wed, 10 Dec 2025 19:27:20 +0100 Subject: [PATCH 07/43] Update download.go --- server/router/download.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/router/download.go b/server/router/download.go index 16e9db2..4e618d3 100644 --- a/server/router/download.go +++ b/server/router/download.go @@ -14,4 +14,5 @@ func downloadRouter(r *gin.Engine) { api.POST("/download/image", service.DownloadImage) // download image api.GET("/download/image/status", service.StatusImage) // download image api.GET("/download/image/enabled", service.ImageEnabled) // download image + api.POST("/download/file", service.DownloadImageFile) // download image } From b5866088fc3c5d510551e8b7d2d10e9a31670cdf Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Thu, 11 Dec 2025 00:01:27 +0100 Subject: [PATCH 08/43] Update download.tsx --- web/src/pages/desktop/menu/download.tsx | 47 ++++++++++++++++++++----- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index fb7d92e..25c3802 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -114,20 +114,41 @@ export const DownloadImage = () => { function handleFileChange(e: React.ChangeEvent) { setSelectedFile(e.target.files?.[0] ?? null); + clearInterval(intervalId.current); + intervalId.current = undefined; } function upload(file: File | null) { if (!file) return; + setStatus('in_progress'); + setLog('Downloading: ' + file.name); + const formData = new FormData(); formData.append("file", file); - console.log(file); - fetch("/api/download/file", { method: "POST", body: formData, - }); + }).catch(() => { + clearInterval(intervalId.current); // Clear the interval when the download is complete or fails + setStatus('failed'); + setLog('Failed'); + }).then(() => { + clearInterval(intervalId.current); // Clear the interval when the download is complete or fails + setStatus('idle'); + setLog(''); + }); + + console.log(intervalId.current); + // Start the interval to check the download status + if (!intervalId.current) { + getDownloadStatus(); + setTimeout(() => { + intervalId.current = setInterval(getDownloadStatus, 2500); + }, 2500); + } + } const content = ( @@ -165,26 +186,36 @@ export const DownloadImage = () => {
{ + if (status === "in_progress") return; // deaktiviert e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files?.[0] ?? null; setSelectedFile(file); }} onDragOver={(e) => { + if (status === "in_progress") return; // deaktiviert e.preventDefault(); setIsDragging(true); // Datei wird über den Bereich gezogen }} onDragLeave={(e) => { + if (status === "in_progress") return; // deaktiviert e.preventDefault(); setIsDragging(false); // Maus verlässt Bereich }} - onClick={() => document.getElementById("file-upload")?.click()} + onClick={() => { + if (status === "in_progress") return; // deaktiviert + document.getElementById("file-upload")?.click() + }} > {selectedFile ? selectedFile.name : t('download.uploadbox')} From 648c9f866e1c73445564acf4723ae9d9d4cd7bc8 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Thu, 11 Dec 2025 00:02:01 +0100 Subject: [PATCH 09/43] Update service.go --- server/service/download/service.go | 138 +++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/server/service/download/service.go b/server/service/download/service.go index 3f63081..4fae1f3 100644 --- a/server/service/download/service.go +++ b/server/service/download/service.go @@ -93,6 +93,144 @@ func (s *Service) StatusImage(c *gin.Context) { }) } +func (s *Service) DownloadImageFile(c *gin.Context) { + var rsp proto.Response + + log.Debug("DownloadImage") + + // Set a sentinel file to mark that there is a download in progress + // This is to prevent multiple downloads at the same time + if _, err := os.Stat(sentinelPath); err == nil { + log.Debug("Download in progress") + rsp.ErrRsp(c, -1, "download in progress") + return + } + + // Create the sentinel file + err := os.WriteFile(sentinelPath, []byte("start"), 0644) + if err != nil { + log.Error("Failed to create sentinel file") + rsp.ErrRsp(c, -1, "failed to create sentinel file") + return + } + + // Multipart Reader direkt nutzen (keine FormFile!) + reader, err := c.Request.MultipartReader() + if err != nil { + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "invalid multipart data", + File: "", + Percentage: "", + }) + defer os.Remove(sentinelPath) + return + } + + + var lw *loggingWriter + + for { + part, err := reader.NextPart() + if err == io.EOF { + break + } + if err != nil { + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "failed to read part", + File: "", + Percentage: "", + }) + defer os.Remove(sentinelPath) + return + } + + if part.FormName() != "file" { + continue + } + + filename := part.FileName() + if filename == "" { + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "no filename", + File: "", + Percentage: "", + }) + defer os.Remove(sentinelPath) + return + } + + data, err := os.ReadFile(sentinelPath) + if err != nil { + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "error", + File: "", + Percentage: "", + }) + defer os.Remove(sentinelPath) + return + } + + outPath := "/data/" + filename + out, err := os.Create(outPath) + if err != nil { + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "cannot create file", + File: "", + Percentage: "", + }) + defer os.Remove(sentinelPath) + return + } + defer out.Close() + + if strings.Contains(string(data), "start") { + err = os.WriteFile(sentinelPath, []byte(filename), 0644) + if err != nil { + log.Error("Failed to create sentinel file") + rsp.ErrRsp(c, -1, "failed to create sentinel file") + defer os.Remove(sentinelPath) + return + } + + lw = &loggingWriter{writer: out, totalSize: c.Request.ContentLength} + lw.startTicker() + } else { + if !strings.Contains(string(data), filename) { + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "error", + File: "", + Percentage: "", + }) + defer os.Remove(sentinelPath) + return + } + } + + // Direkt streamen → kein RAM-Bedarf außer kleinem Buffer + _, err = io.Copy(lw, part) + if err != nil { + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "write failed", + File: "", + Percentage: "", + }) + defer os.Remove(sentinelPath) + return + } + } + lw.stopTicker() + + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "idle", + File: "", + Percentage: "", + }) + + + defer os.Remove(sentinelPath) + return +} + func (s *Service) DownloadImage(c *gin.Context) { var req proto.MountImageReq var rsp proto.Response From 346315b4bbe1517d04bc38cb5df35cbd251538dd Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Thu, 11 Dec 2025 00:03:26 +0100 Subject: [PATCH 10/43] Update download.tsx --- web/src/pages/desktop/menu/download.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index 25c3802..6ee22ec 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -182,7 +182,7 @@ export const DownloadImage = () => {
-
{t('upload.input')}
+
{t('download.input')}
Date: Thu, 11 Dec 2025 00:03:56 +0100 Subject: [PATCH 11/43] Update download.tsx --- web/src/pages/desktop/menu/download.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index 6ee22ec..90a3fa2 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -182,7 +182,7 @@ export const DownloadImage = () => {
-
{t('download.input')}
+
{t('download.inputfile')}
Date: Thu, 11 Dec 2025 00:06:54 +0100 Subject: [PATCH 12/43] Update de.ts --- web/src/i18n/locales/de.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index 2733d79..8867817 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -169,7 +169,8 @@ const de = { input: 'Bitte geben Sie die URL für das Remote-Systemabbild ein', ok: 'Ok', disabled: '/data Partition ist nur-lesbar, daher kann das Systemabbild nicht heruntergeladen werden', - uploadbox: "Datei hier ablegen oder klicken zum Auswählen" + uploadbox: "Datei hier ablegen oder klicken zum Auswählen", + inputfile: "Bitte geben Sie die Datei für das Systemabbild an" }, power: { title: 'Power', From 19076b250879894a7f694af64228d617596d6d0a Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Thu, 11 Dec 2025 00:07:29 +0100 Subject: [PATCH 13/43] Update en.ts --- web/src/i18n/locales/en.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 4bf8ae1..4a72340 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -184,7 +184,8 @@ const en = { input: 'Please enter a remote image URL', ok: 'Ok', disabled: '/data partition is RO, so we cannot download the image', - uploadbox: "Drop file here or click to select" + uploadbox: "Drop file here or click to select", + inputfile: "Please enter the image File" }, power: { title: 'Power', From 61ff3c5b234d7a87053afa80a52c55ecc2e3b72c Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:40:16 +0100 Subject: [PATCH 14/43] Update en.ts --- web/src/i18n/locales/en.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 4a72340..854bd69 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -185,7 +185,8 @@ const en = { ok: 'Ok', disabled: '/data partition is RO, so we cannot download the image', uploadbox: "Drop file here or click to select", - inputfile: "Please enter the image File" + inputfile: "Please enter the image File", + NoISO: "No ISO" }, power: { title: 'Power', From ade9ac863fcc14f9e6a5aeff47cd488ac827b93a Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Thu, 11 Dec 2025 22:40:30 +0100 Subject: [PATCH 15/43] Update de.ts --- web/src/i18n/locales/de.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index 8867817..b8a024e 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -170,7 +170,8 @@ const de = { ok: 'Ok', disabled: '/data Partition ist nur-lesbar, daher kann das Systemabbild nicht heruntergeladen werden', uploadbox: "Datei hier ablegen oder klicken zum Auswählen", - inputfile: "Bitte geben Sie die Datei für das Systemabbild an" + inputfile: "Bitte geben Sie die Datei für das Systemabbild an", + NoISO: "Keine ISO" }, power: { title: 'Power', From 0610f6fd1ac23a75345ec5484eff50b7ad4f1722 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:17:33 +0100 Subject: [PATCH 16/43] Update download.tsx --- web/src/pages/desktop/menu/download.tsx | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index 90a3fa2..ecd43e8 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -113,7 +113,15 @@ export const DownloadImage = () => { } function handleFileChange(e: React.ChangeEvent) { - setSelectedFile(e.target.files?.[0] ?? null); + const file = e.target.files?.[0] ?? null; + if (!file || !file.name.toLowerCase().endsWith(".iso")) { + setStatus('failed'); + setLog(t('download.NoISO')); + return; + } + setStatus('idle'); + setLog(''); + setSelectedFile(file); clearInterval(intervalId.current); intervalId.current = undefined; } @@ -121,6 +129,12 @@ export const DownloadImage = () => { function upload(file: File | null) { if (!file) return; + if (!file || !file.name.toLowerCase().endsWith(".iso")) { + setStatus('failed'); + setLog(t('download.NoISO')); + return; + } + setStatus('in_progress'); setLog('Downloading: ' + file.name); @@ -140,7 +154,6 @@ export const DownloadImage = () => { setLog(''); }); - console.log(intervalId.current); // Start the interval to check the download status if (!intervalId.current) { getDownloadStatus(); @@ -200,6 +213,13 @@ export const DownloadImage = () => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files?.[0] ?? null; + if (!file || !file.name.toLowerCase().endsWith(".iso")) { + setStatus('failed'); + setLog(t('download.NoISO')); + return; + } + setStatus('idle'); + setLog(''); setSelectedFile(file); }} onDragOver={(e) => { From 1fa6f48d4e8000468c48781391b109441f64d058 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Thu, 11 Dec 2025 23:17:57 +0100 Subject: [PATCH 17/43] Update service.go --- server/service/download/service.go | 60 ++++++++++++------------------ 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/server/service/download/service.go b/server/service/download/service.go index 4fae1f3..d677435 100644 --- a/server/service/download/service.go +++ b/server/service/download/service.go @@ -117,11 +117,8 @@ func (s *Service) DownloadImageFile(c *gin.Context) { // Multipart Reader direkt nutzen (keine FormFile!) reader, err := c.Request.MultipartReader() if err != nil { - rsp.OkRspWithData(c, &proto.StatusImageRsp{ - Status: "invalid multipart data", - File: "", - Percentage: "", - }) + log.Error("invalid multipart data") + rsp.ErrRsp(c, -1, "invalid multipart data") defer os.Remove(sentinelPath) return } @@ -135,11 +132,9 @@ func (s *Service) DownloadImageFile(c *gin.Context) { break } if err != nil { - rsp.OkRspWithData(c, &proto.StatusImageRsp{ - Status: "failed to read part", - File: "", - Percentage: "", - }) + log.Error("failed to read part") + rsp.ErrRsp(c, -1, "failed to read part") + lw.stopTicker() defer os.Remove(sentinelPath) return } @@ -150,22 +145,18 @@ func (s *Service) DownloadImageFile(c *gin.Context) { filename := part.FileName() if filename == "" { - rsp.OkRspWithData(c, &proto.StatusImageRsp{ - Status: "no filename", - File: "", - Percentage: "", - }) + log.Error("no filename") + rsp.ErrRsp(c, -1, "no filename") + lw.stopTicker() defer os.Remove(sentinelPath) return } data, err := os.ReadFile(sentinelPath) if err != nil { - rsp.OkRspWithData(c, &proto.StatusImageRsp{ - Status: "error", - File: "", - Percentage: "", - }) + log.Error("Read failed") + rsp.ErrRsp(c, -1, "Read failed") + lw.stopTicker() defer os.Remove(sentinelPath) return } @@ -173,11 +164,9 @@ func (s *Service) DownloadImageFile(c *gin.Context) { outPath := "/data/" + filename out, err := os.Create(outPath) if err != nil { - rsp.OkRspWithData(c, &proto.StatusImageRsp{ - Status: "cannot create file", - File: "", - Percentage: "", - }) + log.Error("cannot create file") + rsp.ErrRsp(c, -1, "cannot create file") + lw.stopTicker() defer os.Remove(sentinelPath) return } @@ -188,6 +177,8 @@ func (s *Service) DownloadImageFile(c *gin.Context) { if err != nil { log.Error("Failed to create sentinel file") rsp.ErrRsp(c, -1, "failed to create sentinel file") + lw.stopTicker() + defer os.Remove(outPath) defer os.Remove(sentinelPath) return } @@ -196,11 +187,10 @@ func (s *Service) DownloadImageFile(c *gin.Context) { lw.startTicker() } else { if !strings.Contains(string(data), filename) { - rsp.OkRspWithData(c, &proto.StatusImageRsp{ - Status: "error", - File: "", - Percentage: "", - }) + log.Error("failed") + rsp.ErrRsp(c, -1, "failed") + lw.stopTicker() + defer os.Remove(outPath) defer os.Remove(sentinelPath) return } @@ -209,11 +199,10 @@ func (s *Service) DownloadImageFile(c *gin.Context) { // Direkt streamen → kein RAM-Bedarf außer kleinem Buffer _, err = io.Copy(lw, part) if err != nil { - rsp.OkRspWithData(c, &proto.StatusImageRsp{ - Status: "write failed", - File: "", - Percentage: "", - }) + log.Error("write failed") + rsp.ErrRsp(c, -1, "write failed") + lw.stopTicker() + defer os.Remove(outPath) defer os.Remove(sentinelPath) return } @@ -226,7 +215,6 @@ func (s *Service) DownloadImageFile(c *gin.Context) { Percentage: "", }) - defer os.Remove(sentinelPath) return } From 116bb50364c74c8024aab6178db4ba1c19d52ba0 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:04:10 +0100 Subject: [PATCH 18/43] Update service.go --- server/service/download/service.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/service/download/service.go b/server/service/download/service.go index d677435..97949e4 100644 --- a/server/service/download/service.go +++ b/server/service/download/service.go @@ -152,6 +152,22 @@ func (s *Service) DownloadImageFile(c *gin.Context) { return } + filename = filepath.Base(filename) + + if filename != part.FileName() { + log.Warn("path detected in filename") + rsp.ErrRsp(c, -1, "invalid filename") + defer os.Remove(sentinelPath) + return + } + + if strings.Contains(filename, "..") { + log.Warn("path traversal attempt") + rsp.ErrRsp(c, -1, "invalid filename") + defer os.Remove(sentinelPath) + return + } + data, err := os.ReadFile(sentinelPath) if err != nil { log.Error("Read failed") From dec1621850142b95a64a9a082361e290605610e3 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:49:15 +0100 Subject: [PATCH 19/43] Update version.go --- server/service/application/version.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/service/application/version.go b/server/service/application/version.go index 569696e..a5ca5c8 100644 --- a/server/service/application/version.go +++ b/server/service/application/version.go @@ -48,6 +48,24 @@ func (s *Service) GetVersion(c *gin.Context) { }) } +func (s *Service) GetCurrentVersion(c *gin.Context) { + var rsp proto.Response + + // current version + currentVersion := "1.0.0" + + versionFile := fmt.Sprintf("%s/version", AppDir) + if version, err := os.ReadFile(versionFile); err == nil { + currentVersion = strings.ReplaceAll(string(version), "\n", "") + } + + log.Debugf("current version: %s", currentVersion) + + rsp.OkRspWithData(c, &proto.GetVersionRsp{ + Current: currentVersion, + }) +} + func getLatest() (*Latest, error) { baseURL := StableURL if isPreviewEnabled() { From fed92254d2c2ef667f936d3bbd07af75ef49d2ea Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:50:09 +0100 Subject: [PATCH 20/43] Update application.go --- server/router/application.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/router/application.go b/server/router/application.go index 1c4d269..151d034 100644 --- a/server/router/application.go +++ b/server/router/application.go @@ -12,6 +12,7 @@ func applicationRouter(r *gin.Engine) { api := r.Group("/api").Use(middleware.CheckToken()) api.GET("/application/version", service.GetVersion) // get application version + api.GET("/application/version", service.GetCurrentVersion) // get application current version api.POST("/application/update", service.Update) // update application api.GET("/application/preview", service.GetPreview) // get preview updates state From 3ff64483df7782e76fd9a5e69683d4b78730b0a7 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:51:36 +0100 Subject: [PATCH 21/43] Update application.go --- server/router/application.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/router/application.go b/server/router/application.go index 151d034..1748298 100644 --- a/server/router/application.go +++ b/server/router/application.go @@ -12,7 +12,7 @@ func applicationRouter(r *gin.Engine) { api := r.Group("/api").Use(middleware.CheckToken()) api.GET("/application/version", service.GetVersion) // get application version - api.GET("/application/version", service.GetCurrentVersion) // get application current version + api.GET("/application/currentversion", service.GetCurrentVersion) // get application current version api.POST("/application/update", service.Update) // update application api.GET("/application/preview", service.GetPreview) // get preview updates state From 0e1fae8751415c8348fad8e00e26590cac8dcf19 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 10:53:28 +0100 Subject: [PATCH 22/43] Update application.ts --- web/src/api/application.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/src/api/application.ts b/web/src/api/application.ts index f3192ac..cf13883 100644 --- a/web/src/api/application.ts +++ b/web/src/api/application.ts @@ -5,6 +5,11 @@ export function getVersion() { return http.get('/api/application/version'); } +// get application Current version +export function getCurrentVersion() { + return http.get('/api/application/currentversion'); +} + // update application to latest version export function update() { return http.request({ From d774bf8c58df26ef1af793c753a917a7f81b6f90 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:42:55 +0100 Subject: [PATCH 23/43] Update download.tsx --- web/src/pages/desktop/menu/download.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/web/src/pages/desktop/menu/download.tsx b/web/src/pages/desktop/menu/download.tsx index ecd43e8..e26fa39 100644 --- a/web/src/pages/desktop/menu/download.tsx +++ b/web/src/pages/desktop/menu/download.tsx @@ -200,14 +200,9 @@ export const DownloadImage = () => {
{ if (status === "in_progress") return; // deaktiviert e.preventDefault(); From 7404a205a560cc9e1820fdcfcfd3ebcade3754e6 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:43:29 +0100 Subject: [PATCH 24/43] Update de.ts --- web/src/i18n/locales/de.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index b8a024e..cbb03fc 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -316,8 +316,17 @@ const de = { cancel: 'Abbrechen', preview: 'Vorab-Versionen', previewDesc: 'Erhalten Sie vorab Zugriff auf neue Funktionen und Verbesserungen', - previewTip: - 'Bitte beachten Sie, dass Vorab-Versionen womöglich noch Fehler oder unvollständige Funktionen enthalten!' + previewTip: 'Bitte beachten Sie, dass Vorab-Versionen womöglich noch Fehler oder unvollständige Funktionen enthalten!' + }, + offlineupdate: { + title: 'Offline Aktualisierung', + queryFailed: 'Version konnte nicht abgefragt werden', + updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.', + updating: 'Aktualisierung gestartet. Bitte warten...', + confirm: 'Bestätigen', + cancel: 'Abbrechen', + inputfile: "Bitte geben Sie die Datei für das Update an", + noupdatefile: "Keine Update Datei" }, account: { title: 'Konto', From 8d822948cf3cdc63b8d34baa23f0ea067b66fec1 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:43:44 +0100 Subject: [PATCH 25/43] Update en.ts --- web/src/i18n/locales/en.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 854bd69..6c7accd 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -351,6 +351,16 @@ const en = { previewTip: 'Please be aware that preview releases may contain bugs or incomplete functionality!' }, + offlineupdate: { + title: 'Offline Updates', + queryFailed: 'Get version failed', + updateFailed: 'Update failed. Please retry.', + updating: 'Update started. Please wait...', + confirm: 'Confirm', + cancel: 'Cancel', + inputfile: "Please enter the Update File", + noupdatefile: "No update file" + }, account: { title: 'Account', webAccount: 'Web Account Name', From c1a21c2d69ac16eb43a60da6bc96c8cdf48418ab Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:44:42 +0100 Subject: [PATCH 26/43] Update service.go --- server/service/download/service.go | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/server/service/download/service.go b/server/service/download/service.go index 97949e4..b0c3ef7 100644 --- a/server/service/download/service.go +++ b/server/service/download/service.go @@ -12,6 +12,7 @@ import ( "path/filepath" "strings" "time" + "regexp" ) type Service struct{} @@ -51,6 +52,28 @@ func (s *Service) ImageEnabled(c *gin.Context) { }) } +func isISO9660(path string) (bool, error) { + f, err := os.Open(path) + if err != nil { + return false, err + } + defer f.Close() + + // ISO-9660 Magic "CD001" bei Offset 32769 + _, err = f.Seek(0x8001, io.SeekStart) + if err != nil { + return false, err + } + + buf := make([]byte, 5) + _, err = io.ReadFull(f, buf) + if err != nil { + return false, err + } + + return string(buf) == "CD001", nil +} + func (s *Service) StatusImage(c *gin.Context) { var rsp proto.Response @@ -168,6 +191,19 @@ func (s *Service) DownloadImageFile(c *gin.Context) { return } + if !strings.HasSuffix(strings.ToLower(filename), ".iso") { + rsp.ErrRsp(c, -1, "only .iso files allowed") + defer os.Remove(sentinelPath) + return + } + + valid := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + if !valid.MatchString(filename) { + rsp.ErrRsp(c, -1, "invalid filename") + defer os.Remove(sentinelPath) + return + } + data, err := os.ReadFile(sentinelPath) if err != nil { log.Error("Read failed") @@ -222,6 +258,15 @@ func (s *Service) DownloadImageFile(c *gin.Context) { defer os.Remove(sentinelPath) return } + + ok, err := isISO9660(outPath) + if err != nil || !ok { + rsp.ErrRsp(c, -1, "file is not a valid ISO image") + lw.stopTicker() + defer os.Remove(outPath) + defer os.Remove(sentinelPath) + return + } } lw.stopTicker() From d9c9eae4f9b4e84744d34d08e1c1953eaa84ed1a Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:46:26 +0100 Subject: [PATCH 27/43] Create index.tsx --- .../menu/settings/offlineupdate/index.tsx | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 web/src/pages/desktop/menu/settings/offlineupdate/index.tsx diff --git a/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx b/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx new file mode 100644 index 0000000..45ab004 --- /dev/null +++ b/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx @@ -0,0 +1,211 @@ +import { useEffect, useState } from 'react'; +import { LoadingOutlined, SmileOutlined } from '@ant-design/icons'; +import { Button, Input, Result, Spin } from 'antd'; +import { useTranslation } from 'react-i18next'; +import clsx from 'clsx'; + +import * as api from '@/api/application.ts'; + +type UpdateProps = { + setIsLocked: (isClosable: boolean) => void; +}; + +type Status = '' | 'loading' | 'updating' | 'outdated' | 'latest' | 'latests' | 'failed'; + +export const OfflineUpdate = ({ setIsLocked }: UpdateProps) => { + const { t } = useTranslation(); + + const [status, setStatus] = useState(''); + const [currentVersion, setCurrentVersion] = useState(''); + const [errMsg, setErrMsg] = useState(''); + const [selectedFile, setSelectedFile] = useState(null); + const [isDragging, setIsDragging] = useState(false); + + useEffect(() => { + checkForUpdates(); + }, []); + + function checkForUpdates() { + if (status === 'loading') return; + setStatus('loading'); + + setIsLocked(true); + + api + .getCurrentVersion() + .then((rsp: any) => { + if (rsp.code !== 0 || !rsp.data) { + setStatus('failed'); + setErrMsg(t('settings.offlineupdate.queryFailed')); + return; + } + + setStatus('latest'); + setCurrentVersion(rsp.data.current); + }) + .catch(() => { + setStatus('failed'); + setErrMsg(t('settings.offlineupdate.queryFailed')); + }); + + setIsLocked(false); + } + + function handleFileChange(e: React.ChangeEvent) { + const file = e.target.files?.[0] ?? null; + if (!file || !file.name.toLowerCase().endsWith(".tar.gz")) { + setStatus('failed'); + setErrMsg(t('settings.offlineupdate.noupdatefile')); + return; + } + setSelectedFile(file); + } + + function upload(file: File | null) { + if (!file) return; + + if (!file.name.toLowerCase().endsWith(".tar.gz")) { + setStatus('failed'); + setErrMsg(t('settings.offlineupdate.noupdatefile')); + return; + } + + setStatus('updating'); + setErrMsg(''); + + const formData = new FormData(); + formData.append("file", file); + + fetch("/api/download/file", { + method: "POST", + body: formData, + }) + .then((rsp: Response) => { + // Prüfen ob HTTP OK + if (!rsp.ok) throw new Error(`HTTP error ${rsp.status}`); + return rsp.json(); // JSON-Payload parsen + }) + .then((rspj: any) => { + // Jetzt rspj ist das tatsächliche JSON + if (rspj.code !== 0 || !rspj.data) { + setStatus('failed'); + setErrMsg(rspj.msg || 'Unknown error'); + console.log(rspj); + return; + } + checkForUpdates(); + }) + .catch((err: any) => { + setStatus('failed'); + setErrMsg(err?.message || 'Failed'); + }); +} + + + return ( + <> +
{t('settings.offlineupdate.title')}
+ +
+ +
+ {status === 'loading' && ( +
+ } size="large" /> +
+ )} + + {status === 'latest' && ( +
+
{t('settings.offlineupdate.inputfile')}
+
+
{ + e.preventDefault(); + setIsDragging(false); + const file = e.dataTransfer.files?.[0] ?? null; + if (!file || !file.name.toLowerCase().endsWith(".tar.gz")) { + setStatus('failed'); + setErrMsg(t('settings.offlineupdate.noupdatefile')); + return; + } + setStatus('latest'); + setErrMsg(''); + setSelectedFile(file); + }} + onDragOver={(e) => { + e.preventDefault(); + setIsDragging(true); // Datei wird über den Bereich gezogen + }} + onDragLeave={(e) => { + e.preventDefault(); + setIsDragging(false); // Maus verlässt Bereich + }} + onClick={() => { + document.getElementById("file-upload")?.click() + }} + > + + {selectedFile ? selectedFile.name : t('download.uploadbox')} + + + +
+ +
+
+ )} + + {status === 'updating' && ( +
+ + {t('settings.offlineupdate.updating')} +
+ )} + + {status === 'latests' && ( + } + title={currentVersion} + subTitle={t('settings.offlineupdate.isLatest')} + extra={[ + + ]} + /> + )} + + {status === 'failed' && } + +
+ +
+
+ + ); +}; From bcd0b717ee14eb1404934f5c2e749c1e74629da0 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:54:03 +0100 Subject: [PATCH 28/43] Update application.go --- server/router/application.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/router/application.go b/server/router/application.go index 1748298..eee6512 100644 --- a/server/router/application.go +++ b/server/router/application.go @@ -14,6 +14,7 @@ func applicationRouter(r *gin.Engine) { api.GET("/application/version", service.GetVersion) // get application version api.GET("/application/currentversion", service.GetCurrentVersion) // get application current version api.POST("/application/update", service.Update) // update application + api.POST("/application/uploadupdate", service.Update) // upload update application api.GET("/application/preview", service.GetPreview) // get preview updates state api.POST("/application/preview", service.SetPreview) // set preview updates state From 52416dfdd75cb768c71f5bee730e298da0895a3d Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:54:20 +0100 Subject: [PATCH 29/43] Update application.go --- server/router/application.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/router/application.go b/server/router/application.go index eee6512..522e391 100644 --- a/server/router/application.go +++ b/server/router/application.go @@ -14,7 +14,7 @@ func applicationRouter(r *gin.Engine) { api.GET("/application/version", service.GetVersion) // get application version api.GET("/application/currentversion", service.GetCurrentVersion) // get application current version api.POST("/application/update", service.Update) // update application - api.POST("/application/uploadupdate", service.Update) // upload update application + api.POST("/application/uploadupdate", service.UploadUpdate) // upload update application api.GET("/application/preview", service.GetPreview) // get preview updates state api.POST("/application/preview", service.SetPreview) // set preview updates state From 56a0468a53ee762ebff80fc3c1deb1a3faf4ac6b Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 14:55:25 +0100 Subject: [PATCH 30/43] Update index.tsx --- web/src/pages/desktop/menu/settings/offlineupdate/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx b/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx index 45ab004..282d6d6 100644 --- a/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx +++ b/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx @@ -76,7 +76,7 @@ export const OfflineUpdate = ({ setIsLocked }: UpdateProps) => { const formData = new FormData(); formData.append("file", file); - fetch("/api/download/file", { + fetch("/api/application/uploadupdate", { method: "POST", body: formData, }) @@ -104,7 +104,7 @@ export const OfflineUpdate = ({ setIsLocked }: UpdateProps) => { return ( <> -
{t('settings.offlineupdate.title')}
+
{t('settings.offlineupdate.title') + ", " + t('settings.offlineupdate.version') + ": " + currentVersion}
From 3305c69e2d873b6604f8b77bc5efddbdffddd277 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:32:55 +0100 Subject: [PATCH 31/43] Update de.ts --- web/src/i18n/locales/de.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index cbb03fc..bddb4d9 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -326,7 +326,8 @@ const de = { confirm: 'Bestätigen', cancel: 'Abbrechen', inputfile: "Bitte geben Sie die Datei für das Update an", - noupdatefile: "Keine Update Datei" + noupdatefile: "Keine Update Datei", + version: "Version" }, account: { title: 'Konto', From 63c8fc8eb56e1aa10eb81f2dd8472d7a96fd92bc Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:33:07 +0100 Subject: [PATCH 32/43] Update en.ts --- web/src/i18n/locales/en.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 6c7accd..0a442fb 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -359,7 +359,8 @@ const en = { confirm: 'Confirm', cancel: 'Cancel', inputfile: "Please enter the Update File", - noupdatefile: "No update file" + noupdatefile: "No update file", + version: "Version" }, account: { title: 'Account', From 5a9cd18f94f70c264d961792494789afa65491b9 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:36:28 +0100 Subject: [PATCH 33/43] Update en.ts --- web/src/i18n/locales/en.ts | 549 +++++++++++++++++-------------------- 1 file changed, 259 insertions(+), 290 deletions(-) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 0a442fb..34f49d6 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -1,390 +1,359 @@ -const en = { +const de = { translation: { head: { - desktop: 'Remote Desktop', - login: 'Login', - changePassword: 'Change Password', + desktop: 'Entfernter Desktop', + login: 'Anmelden', + changePassword: 'Passwort ändern', terminal: 'Terminal', wifi: 'Wi-Fi' }, auth: { - login: 'Login', - placeholderUsername: 'Username', - placeholderPassword: 'Password', - placeholderPassword2: 'Please enter password again', - noEmptyUsername: 'Username required', - noEmptyPassword: 'Password required', - noAccount: 'Failed to get user information, please refresh web page or reset password', - invalidUser: 'Invalid username or password', - error: 'Unexpected error', - changePassword: 'Change Password', - changePasswordDesc: 'For the security of your device, please change the password!', - differentPassword: 'Passwords do not match', - illegalUsername: 'Username contains illegal characters', - illegalPassword: 'Password contains illegal characters', - forgetPassword: 'Forgot Password', + login: 'Anmelden', + placeholderUsername: 'Benutzername', + placeholderPassword: 'Passwort', + placeholderPassword2: 'Bitte Passwort erneut eingeben', + noEmptyUsername: 'Benutzername benötigt', + noEmptyPassword: 'Passwort benötigt', + noAccount: 'Abfragen der Benutzerdaten fehlgeschlagen, bitte die Seite neu laden oder Passwort zurücksetzen', + invalidUser: 'Falscher Benutzername oder falsches Passwort', + error: 'Unerwarteter Fehler', + changePassword: 'Passwort ändern', + changePasswordDesc: 'Für die Sicherheit Ihres Geräts ändern Sie bitte das Passwort!', + differentPassword: 'Passwörter stimmen nicht überein', + illegalUsername: 'Benutzername enthält ungültige Zeichen', + illegalPassword: 'Passwort enthält ungültige Zeichen', + forgetPassword: 'Passwort vergessen', ok: 'Ok', - cancel: 'Cancel', - loginButtonText: 'Login', + cancel: 'Abbrechen', + loginButtonText: 'Anmelden', tips: { reset1: - 'To reset the passwords, press and hold the BOOT button on the NanoKVM for 10 seconds.', - reset2: 'For detailed steps, please consult this document:', - reset3: 'Web default account:', - reset4: 'SSH default account:', - change1: 'Please note that this action will change the following passwords:', - change2: 'Web login password', - change3: 'System root password (SSH login password)', - change4: 'To reset the passwords, press and hold the BOOT button on the NanoKVM.' + 'Um das Passwort zurückzusetzen, drücken und halten Sie den BOOT Knopf auf dem NanoKVM für 10 Sekunden.', + reset2: 'Für detailliertere Anweisungen lesen Sie folgendes Dokument:', + reset3: 'Web Standard-Account:', + reset4: 'SSH Standard-Account:', + change1: 'Bitte beachten Sie, dass diese Aktion folgende Passwörter ändert:', + change2: 'Web Anmelde-Passwort', + change3: 'System root Passwort (SSH Anmelde-Passwort)', + change4: 'Um die Passwörter zurückzusetzen, drücken und halten Sie den BOOT Knopf auf dem NanoKVM.' } }, wifi: { title: 'Wi-Fi', - description: 'Configure Wi-Fi for NanoKVM', - success: 'Please check the network status of NanoKVM and visit the new IP address.', - failed: 'Operation failed, please try again.', + description: 'Wi-Fi Konfiguration für NanoKVM', + success: 'Bitte überprüfen Sie den Netzwerk-Status des NanoKVM und greifen Sie über die neue IP Adresse darauf zu.', + failed: 'Aktion fehlgeschlagen, bitte erneut versuchen.', confirmBtn: 'Ok', - finishBtn: 'Finished' + finishBtn: 'Fertig' }, screen: { - title: 'Screen', - video: 'Video Mode', - videoDirectTips: 'Enable HTTPS in "Settings > Device" to use this mode', - resolution: 'Resolution', - auto: 'Automatic', + title: 'Bildschirm', + video: 'Video Modus', + resolution: 'Auflösung', + auto: 'Automatisch', autoTips: - "Screen tearing or mouse offset may occur at specific resolutions. Consider adjusting the remote host's resolution or disable automatic mode.", + "Bildverzerrungen oder ein versetzter Mauszeiger können bei bestimmten Auflösungen auftreten. Versuchen Sie, die Auflösung des entfernten Hosts anzupassen oder den automatischen Modus zu deaktivieren.", fps: 'FPS', - customizeFps: 'Customize', - quality: 'Quality', - qualityLossless: 'Lossless', - qualityHigh: 'High', - qualityMedium: 'Medium', - qualityLow: 'Low', - frameDetect: 'Frame Detect', + customizeFps: 'Anpassen', + quality: 'Qualität', + qualityLossless: 'Verlustfrei', + qualityHigh: 'Hoch', + qualityMedium: 'Mittel', + qualityLow: 'Niedrig', + frameDetect: 'Bilderkennung', frameDetectTip: - "Calculate the difference between frames. Stop transmitting video stream when no changes are detected on the remote host's screen.", - resetHdmi: 'Reset HDMI' + 'Berechnet den Unterschied zwischen den Einzelbildern. Beendet die Liveübertragung des Videostreams wenn keine Änderungen auf dem Bildschirm des Hosts festgestellt werden kann.', + resetHdmi: 'HDMI zurücksetzen' }, keyboard: { - title: 'Keyboard', - paste: 'Paste', - tips: 'Only standard keyboard letters and symbols are supported', - placeholder: 'Please input', - submit: 'Submit', - virtual: 'Keyboard', + title: 'Tastatur', + paste: 'Einfügen', + tips: 'Server Tastaturbelegung', + placeholder: 'Bitte eingeben', + submit: 'Senden', + virtual: 'Tastatur', ctrlaltdel: 'Ctrl+Alt+Del', - readClipboard: 'Read from Clipboard', - clipboardPermissionDenied: - 'Clipboard permission denied. Please allow clipboard access in your browser.', - clipboardReadError: 'Failed to read clipboard', - dropdownEnglish: 'English', - dropdownGerman: 'German' + dropdownEnglish: 'Englisch', + dropdownGerman: 'Deutsch' }, mouse: { - title: 'Mouse', - cursor: 'Cursor style', - default: 'Default cursor', - pointer: 'Pointer cursor', - cell: 'Cell cursor', - text: 'Text cursor', - grab: 'Grab cursor', - hide: 'Hide cursor', - mode: 'Mouse mode', - absolute: 'Absolute mode', - relative: 'Relative mode', - speed: 'Wheel speed', - fast: 'Fast', - slow: 'Slow', - requestPointer: 'Using relative mode. Please click desktop to get mouse pointer.', - resetHid: 'Reset HID', + title: 'Maus', + cursor: 'Cursor', + default: 'Standard Cursor', + pointer: 'Zeiger Cursor', + cell: 'Zellen Cursor', + text: 'Text Cursor', + grab: 'Greif Cursor', + hide: 'Versteckter Cursor', + mode: 'Maus Modus', + absolute: 'Absoluter Modus', + relative: 'Relativer Modus', + requestPointer: 'Relativer Modus aktiv. Klicken Sie auf den Desktop um den Mauszeiger zu sehen.', + resetHid: 'HID zurücksetzen', hidOnly: { - title: 'HID-Only mode', - desc: "If your mouse and keyboard stop responding and resetting HID doesn't help, it could be a compatibility issue between the NanoKVM and the device. Try to enable HID-Only mode for better compatibility.", - tip1: 'Enabling HID-Only mode will unmount the virtual U-disk and virtual network', - tip2: 'In HID-Only mode, image mounting is disabled', - tip3: 'NanoKVM will automatically reboot after switching modes', - enable: 'Enable HID-Only mode', - disable: 'Disable HID-Only mode' + title: 'HID-Only Modus', + desc: 'Wenn Ihre Maus und Tastatur nicht mehr reagieren und das Zurücksetzen der HID-Verbindung nicht hilft, könnte es sich um ein Kompatibilitätsproblem zwischen dem NanoKVM und dem Gerät handeln. Versuchen Sie, den HID-Only Modus zu aktivieren, um die Kompatibilität zu verbessern.', + tip1: 'Die Aktivierung des HID-Only Modus entfernt das virtuelle U-Laufwerk und das virtuelle Netzwerk.', + tip2: 'Im HID-Only Modus ist das Einbinden von Systemabbilder deaktiviert.', + tip3: 'NanoKVM wird nach dem Wechsel in den neuen Modus automatisch neu gestartet.', + enable: 'HID-Only Modus aktivieren', + disable: 'HID-Only Modus deaktivieren' } }, image: { - title: 'Images', - loading: 'Loading...', - empty: 'Nothing Found', - mountMode: 'Mount mode', - mountFailed: 'Mount failed', - mountDesc: - 'On some systems, you need to eject the virtual disk from the remote host before mounting the image.', - unmountFailed: 'Unmount failed', - unmountDesc: - 'On some systems, you need to manually eject from the remote host before unmounting the image.', - refresh: 'Refresh the image list', - attention: 'Attention', - deleteConfirm: 'Are you sure you want to delete this image?', - okBtn: 'Yes', - cancelBtn: 'No', + title: 'Bilder', + loading: 'Lädt...', + empty: 'Nichts gefunden', + cdrom: 'Binden Sie das Abbild im CD-ROM Modus ein', + mountFailed: 'Einbinden fehlgeschlagen', + mountDesc: + "In einigen Systemen ist es notwendig, die virtuelle Festplatte auf dem entfernten Host auszuwerfen, bevor das Image eingebunden werden kann.", + refresh: 'Bilder aktualisieren', tips: { - title: 'How to upload', - usb1: 'Connect the NanoKVM to your computer via USB.', - usb2: 'Ensure that the virtual disk is mounted (Settings - Virtual Disk).', - usb3: 'Open the virtual disk on your computer and copy the image file to the root directory of the virtual disk.', - scp1: 'Make sure the NanoKVM and your computer are on the same local network.', - scp2: 'Open a terminal on your computer and use the SCP command to upload the image file to the /data directory on the NanoKVM.', - scp3: 'Example: scp your-image-path root@your-nanokvm-ip:/data', - tfCard: 'TF Card', - tf1: 'This method is supported on Linux system', - tf2: 'Get TF card from the NanoKVM (for the FULL version, disassemble the case first).', - tf3: 'Insert the TF card into a card reader and connect it to your computer.', - tf4: 'Copy the image file to the /data directory on the TF card.', - tf5: 'Insert the TF card into the NanoKVM.' + title: 'So laden Sie Dateien hoch', + usb1: 'Verbinden Sie den NanoKVM über USB mit Ihrem Computer.', + usb2: 'Stellen Sie sicher, dass die virtuelle Festplatte eingebunden ist (Einstellungen – Virtuelle Festplatte).', + usb3: 'Öffnen Sie die virtuelle Festplatte auf Ihrem Computer und kopieren Sie die Image-Datei in das Stammverzeichnis der virtuellen Festplatte.', + scp1: 'Stellen Sie sicher, dass sich der NanoKVM und Ihr Computer im selben lokalen Netzwerk befinden.', + scp2: 'Öffnen Sie ein Terminal auf Ihrem Computer und verwenden Sie den SCP-Befehl, um die Image-Datei in das Verzeichnis /data auf dem NanoKVM hochzuladen.', + scp3: 'Beispiel: scp your-image-path root@your-nanokvm-ip:/data', + tfCard: 'TF-Karte', + tf1: 'Diese Methode wird unter Linux-Systemen unterstützt.', + tf2: 'Entnehmen Sie die TF-Karte aus dem NanoKVM (bei der FULL-Version muss zuvor das Gehäuse geöffnet werden).', + tf3: 'Stecken Sie die TF-Karte in einen Kartenleser und verbinden Sie diesen mit Ihrem Computer.', + tf4: 'Kopieren Sie die Image-Datei in das Verzeichnis /data auf der TF-Karte.', + tf5: 'Setzen Sie die TF-Karte wieder in den NanoKVM ein.' } }, script: { - title: 'Scripts', - upload: 'Upload', - run: 'Run', - runBackground: 'Run Background', - runFailed: 'Run failed', - attention: 'Attention', - delDesc: 'Are you sure you want to delete this file?', - confirm: 'Yes', - cancel: 'No', - delete: 'Delete', - close: 'Close' + title: 'Skripte', + upload: 'Hochladen', + run: 'Ausführen', + runBackground: 'Im Hintergrund ausführen', + runFailed: '', + attention: 'Achtung', + delDesc: 'Möchten Sie diese Datei wirklich löschen?', + confirm: 'Ja', + cancel: 'Nein', + delete: 'Löschen', + close: 'Schliessen' }, terminal: { title: 'Terminal', nanokvm: 'NanoKVM Terminal', - serial: 'Serial Port Terminal', - serialPort: 'Serial Port', - serialPortPlaceholder: 'Please enter the serial port', - baudrate: 'Baud rate', - parity: 'Parity', - parityNone: 'None', - parityEven: 'Even', - parityOdd: 'Odd', - flowControl: 'Flow control', - flowControlNone: 'None', - flowControlSoft: 'Soft', - flowControlHard: 'Hard', - dataBits: 'Data bits', - stopBits: 'Stop bits', + serial: 'Serieller Anschluss Terminal', + serialPort: 'Serieller Anschluss', + serialPortPlaceholder: 'Bitte seriellen Anschluss angeben', + baudrate: 'Baudrate', + parity: 'Parität', + parityNone: 'Keine', + parityEven: 'Gerade', + parityOdd: 'Ungerade', + flowControl: 'Fluss-Kontrolle', + flowControlNone: 'Keine', + flowControlSoft: 'Wenig', + flowControlHard: 'Viel', + dataBits: 'Daten bits', + stopBits: 'Stopp bits', confirm: 'Ok' }, wol: { title: 'Wake-on-LAN', - sending: 'Sending command...', - sent: 'Command sent', - input: 'Please enter the MAC', + sending: 'Sende Befehl...', + sent: 'Befehl gesendet', + input: 'Bitte MAC Adresse eingeben', ok: 'Ok' }, download: { - title: 'Image Downloader', - input: 'Please enter a remote image URL', + title: 'Systemabbild Downloader', + input: 'Bitte geben Sie die URL für das Remote-Systemabbild ein', ok: 'Ok', - disabled: '/data partition is RO, so we cannot download the image', - uploadbox: "Drop file here or click to select", - inputfile: "Please enter the image File", - NoISO: "No ISO" + disabled: '/data Partition ist nur-lesbar, daher kann das Systemabbild nicht heruntergeladen werden', + uploadbox: "Datei hier ablegen oder klicken zum Auswählen", + inputfile: "Bitte geben Sie die Datei für das Systemabbild an", + NoISO: "Keine ISO" }, power: { title: 'Power', - showConfirm: 'Confirmation', - showConfirmTip: 'Power operations require an extra confirmation', - reset: 'Reset', + showConfirm: 'Bestätigung', + showConfirmTip: 'Diese Aktionen benötigen eine zusätzliche Bestätigung', + reset: 'Zurücksetzen', power: 'Power', - powerShort: 'Power (short click)', - powerLong: 'Power (long click)', - resetConfirm: 'Proceed reset operation?', - powerConfirm: 'Proceed power operation?', - okBtn: 'Yes', - cancelBtn: 'No' + powerShort: 'Power (Kurzer Klick)', + powerLong: 'Power (Langer Klick)', + resetConfirm: 'Reset-Aktion durchführen?', + powerConfirm: 'Power-Aktion durchführen?', + okBtn: 'Ja', + cancelBtn: 'Nein' }, settings: { - title: 'Settings', + title: 'Einstellungen', about: { - title: 'About NanoKVM', - information: 'Information', + title: 'Über NanoKVM', + information: 'Informationen', ip: 'IP', mdns: 'mDNS', - application: 'Application Version', - applicationTip: 'NanoKVM web application version', - image: 'Image Version', - imageTip: 'NanoKVM system image version', - deviceKey: 'Device Key', + application: 'Applikations-Version', + applicationTip: 'NanoKVM Web Applikations-Version', + image: 'Systemabbild-Version', + imageTip: 'NanoKVM Systemabbild-Version', + deviceKey: 'Geräte Schlüssel', community: 'Community', hostname: 'Hostname', - hostnameUpdated: 'Hostname updated. Reboot to apply.', + hostnameUpdated: 'Hostname aktualisiert. Neustarten um zu übernehmen.', ipType: { - Wired: 'Wired', - Wireless: 'Wireless', - Other: 'Other' + 'Wired': 'Kabel', + 'Wireless': 'Drahtlos', + 'Other': 'Andere' } }, appearance: { - title: 'Appearance', - display: 'Display', - language: 'Language', - menuBar: 'Menu Bar', - menuBarDesc: 'Display icons in the menu bar', - webTitle: 'Web Title', - webTitleDesc: 'Customize the web page title' + title: 'Erscheinungsbild', + display: 'Bildschirm', + language: 'Sprache', + menuBar: 'Menu-Leiste', + menuBarDesc: 'Symbole in der Menu-Leiste anzeigen', + webTitle: 'Web Titel', + webTitleDesc: 'Passen Sie den Web-Seite Titel an' }, device: { - title: 'Device', + title: 'Gerät', oled: { title: 'OLED', - description: 'Turn off OLED screen after', - 0: 'Never', - 15: '15 sec', - 30: '30 sec', - 60: '1 min', - 180: '3 min', - 300: '5 min', - 600: '10 min', - 1800: '30 min', - 3600: '1 hour' + description: 'Schalte OLED Bildschirm aus nach', + 0: 'Nie', + 15: '15 Sek', + 30: '30 Sek', + 60: '1 Min', + 180: '3 Min', + 300: '5 Min', + 600: '10 Min', + 1800: '30 Min', + 3600: '1 Stunde' }, wifi: { title: 'Wi-Fi', - description: 'Configure Wi-Fi', - setBtn: 'Config' + description: 'Wi-Fi konfigurieren', + setBtn: 'Konfigurieren' }, ssh: { - description: 'Enable SSH remote access', - tip: 'Set a strong password before enabling (Account - Change Password)' + description: 'Aktiviere entfernten SSH-Zugang', + tip: 'Setzten Sie ein starkes Passwort vor dem aktivieren (Konto - Passwort ändern)' }, - tls: { - description: 'Enable HTTPS protocol', - tip: 'Be aware: Using HTTPS can increase latency, especially with MJPEG video mode.' - }, - advanced: 'Advanced Settings', + advanced: 'Erweiterte Einstellungen', swap: { title: 'Swap', - disable: 'Disable', - description: 'Set the swap file size', - tip: "Enabling this feature could shorten your SD card's usable life!" + disable: 'Deaktivieren', + description: 'Grösse der Swap-Datei festlegen', + tip: "Das Aktivieren dieser Funktion kann die Lebensdauer Ihrer SD-Karte verkürzen!" }, mouseJiggler: { - title: 'Mouse Jiggler', - description: 'Prevent the remote host from sleeping', - disable: 'Disable', - absolute: 'Absolute Mode', - relative: 'Relative Mode' + title: 'Mausaktivitäts-Simulator', + description: 'Verhindert, dass der remote Host in den Energiesparmodus wechselt', + disable: 'Deaktivieren', + absolute: 'Absoluter Modus', + relative: 'Relativer Modus' }, mdns: { - description: 'Enable mDNS discovery service', - tip: "Turning it off if it's not needed" - }, - hdmi: { - description: 'Enable HDMI/monitor output' + description: 'mDNS-Erkennungsdienst aktivieren', + tip: "Deaktivieren Sie den Dienst, wenn Sie ihn nicht benötigen" }, hidOnly: 'HID-Only Mode', - disk: 'Virtual Disk', - diskDesc: 'Mount SD card on the remote host', - media: "Virtual Image", - mediaDesc: 'Attach virtual image device to the remote host', - network: 'Virtual Network', - networkDesc: 'Mount virtual network card on the remote host', - reboot: 'Reboot', - rebootDesc: 'Are you sure you want to reboot NanoKVM?', - usb: 'USB Interface', - usbDesc: 'Enable USB to host interface', - okBtn: 'Yes', - cancelBtn: 'No' + disk: 'Virtuelle Festplatte', + diskDesc: 'Binde das virtuelle U-Laufwerk an den entfernten Host', + network: 'Virtuelles Netzwerk', + networkDesc: 'Binde die virtuelle Netzwerkkarte an den entfernten Host', + reboot: 'Neustarten', + rebootDesc: 'Sind Sie sicher dass Sie NanoKVM neustarten möchten?', + okBtn: 'Ja', + cancelBtn: 'Nein' }, tailscale: { title: 'Tailscale', memory: { - title: 'Memory optimization', - tip: 'When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. A Tailscale restart is required for the change to take effect.' + title: 'Speicher Optimierung', + tip: "Wenn die Speichernutzung das Limit überschreitet, wird die Speicherbereinigung aggressiver durchgeführt, um Speicher freizugeben. Es wird empfohlen, den Wert auf 75 MB zu setzen, wenn Tailscale verwendet wird. Ein Neustart von Tailscale ist erforderlich, damit die Änderung wirksam wird.", + disable: 'Deaktivieren' }, - swap: { - title: 'Swap memory', - tip: 'If issues persist after enabling memory optimization, try enabling swap memory. This sets the swap file size to 256MB by default, which can be adjusted in "Settings > Device".' - }, - restart: 'Restart Tailscale?', - stop: 'Stop Tailscale?', - stopDesc: 'Log out Tailscale and disable automatic startup on boot.', - loading: 'Loading...', - notInstall: 'Tailscale not found! Please install.', - install: 'Install', - installing: 'Installing', - failed: 'Install failed', - retry: 'Please refresh and try again. Or try to install manually', - download: 'Download the', - package: 'installation package', - unzip: 'and unzip it', - upTailscale: 'Upload tailscale to NanoKVM directory /usr/bin/', - upTailscaled: 'Upload tailscaled to NanoKVM directory /usr/sbin/', - refresh: 'Refresh current page', - notRunning: 'Tailscale is not running. Please start it to continue.', - run: 'Start', + restart: 'Tailscale neu starten?', + stop: 'Tailscale stoppen?', + stopDesc: 'Von Tailscale abmelden und automatischen Start beim Booten deaktivieren.', + loading: 'Lädt...', + notInstall: 'Tailscale nicht gefunden! Bitte installieren.', + install: 'Installieren', + installing: 'Installiere', + failed: 'Installation fehlgeschlagen', + retry: 'Bitte Seite neu laden und erneut versuchen oder manuelle Installation versuchen.', + download: 'Laden Sie das', + package: 'Installations-Paket herunter', + unzip: 'und entpacken Sie es', + upTailscale: 'Tailscale nach /usr/bin/ auf NanoKVM hochladen', + upTailscaled: 'Tailscaled nach /usr/bin/ auf NanoKVM hochladen', + refresh: 'Aktuelle Seite neu laden', notLogin: - 'The device has not been bound yet. Please login and bind this device to your account.', - urlPeriod: 'This url is valid for 10 minutes', - login: 'Login', - loginSuccess: 'Login Success', - enable: 'Enable Tailscale', - deviceName: 'Device Name', - deviceIP: 'Device IP', - account: 'Account', - logout: 'Logout', - logoutDesc: 'Are you sure you want to logout?', - uninstall: 'Uninstall Tailscale', - uninstallDesc: 'Are you sure you want to uninstall Tailscale?', - okBtn: 'Yes', - cancelBtn: 'No' + 'Das Gerät konnte noch nicht gefunden werden. Bitte melden Sie sich an und verknüpfen Sie dieses Gerät mit Ihrem Konto.', + urlPeriod: 'Diese URL ist für 10 Minuten gültig', + login: 'Anmelden', + loginSuccess: 'Anmeldung erfolgreich', + enable: 'Tailscale einschalten', + deviceName: 'Geräte Name', + deviceIP: 'Geräte IP', + account: 'Konto', + logout: 'Abmelden', + logoutDesc: 'Möchten Sie sich wirklich abmelden?', + uninstall: 'Tailscale deinstallieren', + okBtn: 'Ja', + cancelBtn: 'Nein' }, update: { - title: 'Check for Updates', - queryFailed: 'Get version failed', - updateFailed: 'Update failed. Please retry.', - isLatest: 'You already have the latest version.', - available: 'An update is available. Are you sure you want to update now?', - updating: 'Update started. Please wait...', - confirm: 'Confirm', - cancel: 'Cancel', - preview: 'Preview Updates', - previewDesc: 'Get early access to new features and improvements', - previewTip: - 'Please be aware that preview releases may contain bugs or incomplete functionality!' + title: 'Nach Aktualisierungen suchen', + queryFailed: 'Version konnte nicht abgefragt werden', + updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.', + isLatest: 'Sie haben bereits die aktuellste Version.', + available: 'Eine Aktualisierung ist verfügbar. Möchten sie diese jetzt durchführen?', + updating: 'Aktualisierung gestartet. Bitte warten...', + confirm: 'Bestätigen', + cancel: 'Abbrechen', + preview: 'Vorab-Versionen', + previewDesc: 'Erhalten Sie vorab Zugriff auf neue Funktionen und Verbesserungen', + previewTip: 'Bitte beachten Sie, dass Vorab-Versionen womöglich noch Fehler oder unvollständige Funktionen enthalten!' }, offlineupdate: { - title: 'Offline Updates', - queryFailed: 'Get version failed', - updateFailed: 'Update failed. Please retry.', - updating: 'Update started. Please wait...', - confirm: 'Confirm', - cancel: 'Cancel', - inputfile: "Please enter the Update File", - noupdatefile: "No update file", - version: "Version" + title: 'Offline Aktualisierung', + queryFailed: 'Version konnte nicht abgefragt werden', + updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.', + updating: 'Aktualisierung gestartet. Bitte warten...', + confirm: 'Bestätigen', + cancel: 'Abbrechen', + inputfile: "Bitte geben Sie die Datei für das Update an", + noupdatefile: "Keine Update Datei", + version: "Version", + uploadbox: "Drop file here or click to select", + ok: 'Ok' }, account: { - title: 'Account', - webAccount: 'Web Account Name', - password: 'Password', - updateBtn: 'Change', - logoutBtn: 'Logout', - logoutDesc: 'Are you sure you want to logout?', - okBtn: 'Yes', - cancelBtn: 'No' + title: 'Konto', + webAccount: 'Web Konto Name', + password: 'Passwort', + updateBtn: 'Ändern', + logoutBtn: 'Abmelden', + logoutDesc: 'Möchten Sie sich wirklich abmelden?', + okBtn: 'Ja', + cancelBtn: 'Nein' } }, error: { - title: "We've ran into an issue", - refresh: 'Refresh' + title: "Wir sind auf ein Problem gestossen", + refresh: 'Neuladen' }, fullscreen: { - toggle: 'Toggle Fullscreen' + toggle: 'Vollbild ein/aus' }, menu: { - collapse: 'Collapse Menu', - expand: 'Expand Menu' + collapse: 'Menu einblenden', + expand: 'Menu verbergen' } } }; -export default en; +export default de; From b3c6b6c53d41a388928287d09f8c939374ae447c Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:36:39 +0100 Subject: [PATCH 34/43] Update de.ts --- web/src/i18n/locales/de.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index bddb4d9..34f49d6 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -327,7 +327,9 @@ const de = { cancel: 'Abbrechen', inputfile: "Bitte geben Sie die Datei für das Update an", noupdatefile: "Keine Update Datei", - version: "Version" + version: "Version", + uploadbox: "Drop file here or click to select", + ok: 'Ok' }, account: { title: 'Konto', From 551329a41d0ad2a1ad622943cf9f7f3331be810f Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:36:52 +0100 Subject: [PATCH 35/43] Update en.ts --- web/src/i18n/locales/en.ts | 545 ++++++++++++++++++++----------------- 1 file changed, 289 insertions(+), 256 deletions(-) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index 34f49d6..f6abbe8 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -1,359 +1,392 @@ -const de = { +const en = { translation: { head: { - desktop: 'Entfernter Desktop', - login: 'Anmelden', - changePassword: 'Passwort ändern', + desktop: 'Remote Desktop', + login: 'Login', + changePassword: 'Change Password', terminal: 'Terminal', wifi: 'Wi-Fi' }, auth: { - login: 'Anmelden', - placeholderUsername: 'Benutzername', - placeholderPassword: 'Passwort', - placeholderPassword2: 'Bitte Passwort erneut eingeben', - noEmptyUsername: 'Benutzername benötigt', - noEmptyPassword: 'Passwort benötigt', - noAccount: 'Abfragen der Benutzerdaten fehlgeschlagen, bitte die Seite neu laden oder Passwort zurücksetzen', - invalidUser: 'Falscher Benutzername oder falsches Passwort', - error: 'Unerwarteter Fehler', - changePassword: 'Passwort ändern', - changePasswordDesc: 'Für die Sicherheit Ihres Geräts ändern Sie bitte das Passwort!', - differentPassword: 'Passwörter stimmen nicht überein', - illegalUsername: 'Benutzername enthält ungültige Zeichen', - illegalPassword: 'Passwort enthält ungültige Zeichen', - forgetPassword: 'Passwort vergessen', + login: 'Login', + placeholderUsername: 'Username', + placeholderPassword: 'Password', + placeholderPassword2: 'Please enter password again', + noEmptyUsername: 'Username required', + noEmptyPassword: 'Password required', + noAccount: 'Failed to get user information, please refresh web page or reset password', + invalidUser: 'Invalid username or password', + error: 'Unexpected error', + changePassword: 'Change Password', + changePasswordDesc: 'For the security of your device, please change the password!', + differentPassword: 'Passwords do not match', + illegalUsername: 'Username contains illegal characters', + illegalPassword: 'Password contains illegal characters', + forgetPassword: 'Forgot Password', ok: 'Ok', - cancel: 'Abbrechen', - loginButtonText: 'Anmelden', + cancel: 'Cancel', + loginButtonText: 'Login', tips: { reset1: - 'Um das Passwort zurückzusetzen, drücken und halten Sie den BOOT Knopf auf dem NanoKVM für 10 Sekunden.', - reset2: 'Für detailliertere Anweisungen lesen Sie folgendes Dokument:', - reset3: 'Web Standard-Account:', - reset4: 'SSH Standard-Account:', - change1: 'Bitte beachten Sie, dass diese Aktion folgende Passwörter ändert:', - change2: 'Web Anmelde-Passwort', - change3: 'System root Passwort (SSH Anmelde-Passwort)', - change4: 'Um die Passwörter zurückzusetzen, drücken und halten Sie den BOOT Knopf auf dem NanoKVM.' + 'To reset the passwords, press and hold the BOOT button on the NanoKVM for 10 seconds.', + reset2: 'For detailed steps, please consult this document:', + reset3: 'Web default account:', + reset4: 'SSH default account:', + change1: 'Please note that this action will change the following passwords:', + change2: 'Web login password', + change3: 'System root password (SSH login password)', + change4: 'To reset the passwords, press and hold the BOOT button on the NanoKVM.' } }, wifi: { title: 'Wi-Fi', - description: 'Wi-Fi Konfiguration für NanoKVM', - success: 'Bitte überprüfen Sie den Netzwerk-Status des NanoKVM und greifen Sie über die neue IP Adresse darauf zu.', - failed: 'Aktion fehlgeschlagen, bitte erneut versuchen.', + description: 'Configure Wi-Fi for NanoKVM', + success: 'Please check the network status of NanoKVM and visit the new IP address.', + failed: 'Operation failed, please try again.', confirmBtn: 'Ok', - finishBtn: 'Fertig' + finishBtn: 'Finished' }, screen: { - title: 'Bildschirm', - video: 'Video Modus', - resolution: 'Auflösung', - auto: 'Automatisch', + title: 'Screen', + video: 'Video Mode', + videoDirectTips: 'Enable HTTPS in "Settings > Device" to use this mode', + resolution: 'Resolution', + auto: 'Automatic', autoTips: - "Bildverzerrungen oder ein versetzter Mauszeiger können bei bestimmten Auflösungen auftreten. Versuchen Sie, die Auflösung des entfernten Hosts anzupassen oder den automatischen Modus zu deaktivieren.", + "Screen tearing or mouse offset may occur at specific resolutions. Consider adjusting the remote host's resolution or disable automatic mode.", fps: 'FPS', - customizeFps: 'Anpassen', - quality: 'Qualität', - qualityLossless: 'Verlustfrei', - qualityHigh: 'Hoch', - qualityMedium: 'Mittel', - qualityLow: 'Niedrig', - frameDetect: 'Bilderkennung', + customizeFps: 'Customize', + quality: 'Quality', + qualityLossless: 'Lossless', + qualityHigh: 'High', + qualityMedium: 'Medium', + qualityLow: 'Low', + frameDetect: 'Frame Detect', frameDetectTip: - 'Berechnet den Unterschied zwischen den Einzelbildern. Beendet die Liveübertragung des Videostreams wenn keine Änderungen auf dem Bildschirm des Hosts festgestellt werden kann.', - resetHdmi: 'HDMI zurücksetzen' + "Calculate the difference between frames. Stop transmitting video stream when no changes are detected on the remote host's screen.", + resetHdmi: 'Reset HDMI' }, keyboard: { - title: 'Tastatur', - paste: 'Einfügen', - tips: 'Server Tastaturbelegung', - placeholder: 'Bitte eingeben', - submit: 'Senden', - virtual: 'Tastatur', + title: 'Keyboard', + paste: 'Paste', + tips: 'Only standard keyboard letters and symbols are supported', + placeholder: 'Please input', + submit: 'Submit', + virtual: 'Keyboard', ctrlaltdel: 'Ctrl+Alt+Del', - dropdownEnglish: 'Englisch', - dropdownGerman: 'Deutsch' + readClipboard: 'Read from Clipboard', + clipboardPermissionDenied: + 'Clipboard permission denied. Please allow clipboard access in your browser.', + clipboardReadError: 'Failed to read clipboard', + dropdownEnglish: 'English', + dropdownGerman: 'German' }, mouse: { - title: 'Maus', - cursor: 'Cursor', - default: 'Standard Cursor', - pointer: 'Zeiger Cursor', - cell: 'Zellen Cursor', - text: 'Text Cursor', - grab: 'Greif Cursor', - hide: 'Versteckter Cursor', - mode: 'Maus Modus', - absolute: 'Absoluter Modus', - relative: 'Relativer Modus', - requestPointer: 'Relativer Modus aktiv. Klicken Sie auf den Desktop um den Mauszeiger zu sehen.', - resetHid: 'HID zurücksetzen', + title: 'Mouse', + cursor: 'Cursor style', + default: 'Default cursor', + pointer: 'Pointer cursor', + cell: 'Cell cursor', + text: 'Text cursor', + grab: 'Grab cursor', + hide: 'Hide cursor', + mode: 'Mouse mode', + absolute: 'Absolute mode', + relative: 'Relative mode', + speed: 'Wheel speed', + fast: 'Fast', + slow: 'Slow', + requestPointer: 'Using relative mode. Please click desktop to get mouse pointer.', + resetHid: 'Reset HID', hidOnly: { - title: 'HID-Only Modus', - desc: 'Wenn Ihre Maus und Tastatur nicht mehr reagieren und das Zurücksetzen der HID-Verbindung nicht hilft, könnte es sich um ein Kompatibilitätsproblem zwischen dem NanoKVM und dem Gerät handeln. Versuchen Sie, den HID-Only Modus zu aktivieren, um die Kompatibilität zu verbessern.', - tip1: 'Die Aktivierung des HID-Only Modus entfernt das virtuelle U-Laufwerk und das virtuelle Netzwerk.', - tip2: 'Im HID-Only Modus ist das Einbinden von Systemabbilder deaktiviert.', - tip3: 'NanoKVM wird nach dem Wechsel in den neuen Modus automatisch neu gestartet.', - enable: 'HID-Only Modus aktivieren', - disable: 'HID-Only Modus deaktivieren' + title: 'HID-Only mode', + desc: "If your mouse and keyboard stop responding and resetting HID doesn't help, it could be a compatibility issue between the NanoKVM and the device. Try to enable HID-Only mode for better compatibility.", + tip1: 'Enabling HID-Only mode will unmount the virtual U-disk and virtual network', + tip2: 'In HID-Only mode, image mounting is disabled', + tip3: 'NanoKVM will automatically reboot after switching modes', + enable: 'Enable HID-Only mode', + disable: 'Disable HID-Only mode' } }, image: { - title: 'Bilder', - loading: 'Lädt...', - empty: 'Nichts gefunden', - cdrom: 'Binden Sie das Abbild im CD-ROM Modus ein', - mountFailed: 'Einbinden fehlgeschlagen', - mountDesc: - "In einigen Systemen ist es notwendig, die virtuelle Festplatte auf dem entfernten Host auszuwerfen, bevor das Image eingebunden werden kann.", - refresh: 'Bilder aktualisieren', + title: 'Images', + loading: 'Loading...', + empty: 'Nothing Found', + mountMode: 'Mount mode', + mountFailed: 'Mount failed', + mountDesc: + 'On some systems, you need to eject the virtual disk from the remote host before mounting the image.', + unmountFailed: 'Unmount failed', + unmountDesc: + 'On some systems, you need to manually eject from the remote host before unmounting the image.', + refresh: 'Refresh the image list', + attention: 'Attention', + deleteConfirm: 'Are you sure you want to delete this image?', + okBtn: 'Yes', + cancelBtn: 'No', tips: { - title: 'So laden Sie Dateien hoch', - usb1: 'Verbinden Sie den NanoKVM über USB mit Ihrem Computer.', - usb2: 'Stellen Sie sicher, dass die virtuelle Festplatte eingebunden ist (Einstellungen – Virtuelle Festplatte).', - usb3: 'Öffnen Sie die virtuelle Festplatte auf Ihrem Computer und kopieren Sie die Image-Datei in das Stammverzeichnis der virtuellen Festplatte.', - scp1: 'Stellen Sie sicher, dass sich der NanoKVM und Ihr Computer im selben lokalen Netzwerk befinden.', - scp2: 'Öffnen Sie ein Terminal auf Ihrem Computer und verwenden Sie den SCP-Befehl, um die Image-Datei in das Verzeichnis /data auf dem NanoKVM hochzuladen.', - scp3: 'Beispiel: scp your-image-path root@your-nanokvm-ip:/data', - tfCard: 'TF-Karte', - tf1: 'Diese Methode wird unter Linux-Systemen unterstützt.', - tf2: 'Entnehmen Sie die TF-Karte aus dem NanoKVM (bei der FULL-Version muss zuvor das Gehäuse geöffnet werden).', - tf3: 'Stecken Sie die TF-Karte in einen Kartenleser und verbinden Sie diesen mit Ihrem Computer.', - tf4: 'Kopieren Sie die Image-Datei in das Verzeichnis /data auf der TF-Karte.', - tf5: 'Setzen Sie die TF-Karte wieder in den NanoKVM ein.' + title: 'How to upload', + usb1: 'Connect the NanoKVM to your computer via USB.', + usb2: 'Ensure that the virtual disk is mounted (Settings - Virtual Disk).', + usb3: 'Open the virtual disk on your computer and copy the image file to the root directory of the virtual disk.', + scp1: 'Make sure the NanoKVM and your computer are on the same local network.', + scp2: 'Open a terminal on your computer and use the SCP command to upload the image file to the /data directory on the NanoKVM.', + scp3: 'Example: scp your-image-path root@your-nanokvm-ip:/data', + tfCard: 'TF Card', + tf1: 'This method is supported on Linux system', + tf2: 'Get TF card from the NanoKVM (for the FULL version, disassemble the case first).', + tf3: 'Insert the TF card into a card reader and connect it to your computer.', + tf4: 'Copy the image file to the /data directory on the TF card.', + tf5: 'Insert the TF card into the NanoKVM.' } }, script: { - title: 'Skripte', - upload: 'Hochladen', - run: 'Ausführen', - runBackground: 'Im Hintergrund ausführen', - runFailed: '', - attention: 'Achtung', - delDesc: 'Möchten Sie diese Datei wirklich löschen?', - confirm: 'Ja', - cancel: 'Nein', - delete: 'Löschen', - close: 'Schliessen' + title: 'Scripts', + upload: 'Upload', + run: 'Run', + runBackground: 'Run Background', + runFailed: 'Run failed', + attention: 'Attention', + delDesc: 'Are you sure you want to delete this file?', + confirm: 'Yes', + cancel: 'No', + delete: 'Delete', + close: 'Close' }, terminal: { title: 'Terminal', nanokvm: 'NanoKVM Terminal', - serial: 'Serieller Anschluss Terminal', - serialPort: 'Serieller Anschluss', - serialPortPlaceholder: 'Bitte seriellen Anschluss angeben', - baudrate: 'Baudrate', - parity: 'Parität', - parityNone: 'Keine', - parityEven: 'Gerade', - parityOdd: 'Ungerade', - flowControl: 'Fluss-Kontrolle', - flowControlNone: 'Keine', - flowControlSoft: 'Wenig', - flowControlHard: 'Viel', - dataBits: 'Daten bits', - stopBits: 'Stopp bits', + serial: 'Serial Port Terminal', + serialPort: 'Serial Port', + serialPortPlaceholder: 'Please enter the serial port', + baudrate: 'Baud rate', + parity: 'Parity', + parityNone: 'None', + parityEven: 'Even', + parityOdd: 'Odd', + flowControl: 'Flow control', + flowControlNone: 'None', + flowControlSoft: 'Soft', + flowControlHard: 'Hard', + dataBits: 'Data bits', + stopBits: 'Stop bits', confirm: 'Ok' }, wol: { title: 'Wake-on-LAN', - sending: 'Sende Befehl...', - sent: 'Befehl gesendet', - input: 'Bitte MAC Adresse eingeben', + sending: 'Sending command...', + sent: 'Command sent', + input: 'Please enter the MAC', ok: 'Ok' }, download: { - title: 'Systemabbild Downloader', - input: 'Bitte geben Sie die URL für das Remote-Systemabbild ein', + title: 'Image Downloader', + input: 'Please enter a remote image URL', ok: 'Ok', - disabled: '/data Partition ist nur-lesbar, daher kann das Systemabbild nicht heruntergeladen werden', - uploadbox: "Datei hier ablegen oder klicken zum Auswählen", - inputfile: "Bitte geben Sie die Datei für das Systemabbild an", - NoISO: "Keine ISO" + disabled: '/data partition is RO, so we cannot download the image', + uploadbox: "Drop file here or click to select", + inputfile: "Please enter the image File", + NoISO: "No ISO" }, power: { title: 'Power', - showConfirm: 'Bestätigung', - showConfirmTip: 'Diese Aktionen benötigen eine zusätzliche Bestätigung', - reset: 'Zurücksetzen', + showConfirm: 'Confirmation', + showConfirmTip: 'Power operations require an extra confirmation', + reset: 'Reset', power: 'Power', - powerShort: 'Power (Kurzer Klick)', - powerLong: 'Power (Langer Klick)', - resetConfirm: 'Reset-Aktion durchführen?', - powerConfirm: 'Power-Aktion durchführen?', - okBtn: 'Ja', - cancelBtn: 'Nein' + powerShort: 'Power (short click)', + powerLong: 'Power (long click)', + resetConfirm: 'Proceed reset operation?', + powerConfirm: 'Proceed power operation?', + okBtn: 'Yes', + cancelBtn: 'No' }, settings: { - title: 'Einstellungen', + title: 'Settings', about: { - title: 'Über NanoKVM', - information: 'Informationen', + title: 'About NanoKVM', + information: 'Information', ip: 'IP', mdns: 'mDNS', - application: 'Applikations-Version', - applicationTip: 'NanoKVM Web Applikations-Version', - image: 'Systemabbild-Version', - imageTip: 'NanoKVM Systemabbild-Version', - deviceKey: 'Geräte Schlüssel', + application: 'Application Version', + applicationTip: 'NanoKVM web application version', + image: 'Image Version', + imageTip: 'NanoKVM system image version', + deviceKey: 'Device Key', community: 'Community', hostname: 'Hostname', - hostnameUpdated: 'Hostname aktualisiert. Neustarten um zu übernehmen.', + hostnameUpdated: 'Hostname updated. Reboot to apply.', ipType: { - 'Wired': 'Kabel', - 'Wireless': 'Drahtlos', - 'Other': 'Andere' + Wired: 'Wired', + Wireless: 'Wireless', + Other: 'Other' } }, appearance: { - title: 'Erscheinungsbild', - display: 'Bildschirm', - language: 'Sprache', - menuBar: 'Menu-Leiste', - menuBarDesc: 'Symbole in der Menu-Leiste anzeigen', - webTitle: 'Web Titel', - webTitleDesc: 'Passen Sie den Web-Seite Titel an' + title: 'Appearance', + display: 'Display', + language: 'Language', + menuBar: 'Menu Bar', + menuBarDesc: 'Display icons in the menu bar', + webTitle: 'Web Title', + webTitleDesc: 'Customize the web page title' }, device: { - title: 'Gerät', + title: 'Device', oled: { title: 'OLED', - description: 'Schalte OLED Bildschirm aus nach', - 0: 'Nie', - 15: '15 Sek', - 30: '30 Sek', - 60: '1 Min', - 180: '3 Min', - 300: '5 Min', - 600: '10 Min', - 1800: '30 Min', - 3600: '1 Stunde' + description: 'Turn off OLED screen after', + 0: 'Never', + 15: '15 sec', + 30: '30 sec', + 60: '1 min', + 180: '3 min', + 300: '5 min', + 600: '10 min', + 1800: '30 min', + 3600: '1 hour' }, wifi: { title: 'Wi-Fi', - description: 'Wi-Fi konfigurieren', - setBtn: 'Konfigurieren' + description: 'Configure Wi-Fi', + setBtn: 'Config' }, ssh: { - description: 'Aktiviere entfernten SSH-Zugang', - tip: 'Setzten Sie ein starkes Passwort vor dem aktivieren (Konto - Passwort ändern)' + description: 'Enable SSH remote access', + tip: 'Set a strong password before enabling (Account - Change Password)' }, - advanced: 'Erweiterte Einstellungen', + tls: { + description: 'Enable HTTPS protocol', + tip: 'Be aware: Using HTTPS can increase latency, especially with MJPEG video mode.' + }, + advanced: 'Advanced Settings', swap: { title: 'Swap', - disable: 'Deaktivieren', - description: 'Grösse der Swap-Datei festlegen', - tip: "Das Aktivieren dieser Funktion kann die Lebensdauer Ihrer SD-Karte verkürzen!" + disable: 'Disable', + description: 'Set the swap file size', + tip: "Enabling this feature could shorten your SD card's usable life!" }, mouseJiggler: { - title: 'Mausaktivitäts-Simulator', - description: 'Verhindert, dass der remote Host in den Energiesparmodus wechselt', - disable: 'Deaktivieren', - absolute: 'Absoluter Modus', - relative: 'Relativer Modus' + title: 'Mouse Jiggler', + description: 'Prevent the remote host from sleeping', + disable: 'Disable', + absolute: 'Absolute Mode', + relative: 'Relative Mode' }, mdns: { - description: 'mDNS-Erkennungsdienst aktivieren', - tip: "Deaktivieren Sie den Dienst, wenn Sie ihn nicht benötigen" + description: 'Enable mDNS discovery service', + tip: "Turning it off if it's not needed" + }, + hdmi: { + description: 'Enable HDMI/monitor output' }, hidOnly: 'HID-Only Mode', - disk: 'Virtuelle Festplatte', - diskDesc: 'Binde das virtuelle U-Laufwerk an den entfernten Host', - network: 'Virtuelles Netzwerk', - networkDesc: 'Binde die virtuelle Netzwerkkarte an den entfernten Host', - reboot: 'Neustarten', - rebootDesc: 'Sind Sie sicher dass Sie NanoKVM neustarten möchten?', - okBtn: 'Ja', - cancelBtn: 'Nein' + disk: 'Virtual Disk', + diskDesc: 'Mount SD card on the remote host', + media: "Virtual Image", + mediaDesc: 'Attach virtual image device to the remote host', + network: 'Virtual Network', + networkDesc: 'Mount virtual network card on the remote host', + reboot: 'Reboot', + rebootDesc: 'Are you sure you want to reboot NanoKVM?', + usb: 'USB Interface', + usbDesc: 'Enable USB to host interface', + okBtn: 'Yes', + cancelBtn: 'No' }, tailscale: { title: 'Tailscale', memory: { - title: 'Speicher Optimierung', - tip: "Wenn die Speichernutzung das Limit überschreitet, wird die Speicherbereinigung aggressiver durchgeführt, um Speicher freizugeben. Es wird empfohlen, den Wert auf 75 MB zu setzen, wenn Tailscale verwendet wird. Ein Neustart von Tailscale ist erforderlich, damit die Änderung wirksam wird.", - disable: 'Deaktivieren' + title: 'Memory optimization', + tip: 'When memory usage exceeds the limit, garbage collection is performed more aggressively to attempt to free up memory. A Tailscale restart is required for the change to take effect.' }, - restart: 'Tailscale neu starten?', - stop: 'Tailscale stoppen?', - stopDesc: 'Von Tailscale abmelden und automatischen Start beim Booten deaktivieren.', - loading: 'Lädt...', - notInstall: 'Tailscale nicht gefunden! Bitte installieren.', - install: 'Installieren', - installing: 'Installiere', - failed: 'Installation fehlgeschlagen', - retry: 'Bitte Seite neu laden und erneut versuchen oder manuelle Installation versuchen.', - download: 'Laden Sie das', - package: 'Installations-Paket herunter', - unzip: 'und entpacken Sie es', - upTailscale: 'Tailscale nach /usr/bin/ auf NanoKVM hochladen', - upTailscaled: 'Tailscaled nach /usr/bin/ auf NanoKVM hochladen', - refresh: 'Aktuelle Seite neu laden', + swap: { + title: 'Swap memory', + tip: 'If issues persist after enabling memory optimization, try enabling swap memory. This sets the swap file size to 256MB by default, which can be adjusted in "Settings > Device".' + }, + restart: 'Restart Tailscale?', + stop: 'Stop Tailscale?', + stopDesc: 'Log out Tailscale and disable automatic startup on boot.', + loading: 'Loading...', + notInstall: 'Tailscale not found! Please install.', + install: 'Install', + installing: 'Installing', + failed: 'Install failed', + retry: 'Please refresh and try again. Or try to install manually', + download: 'Download the', + package: 'installation package', + unzip: 'and unzip it', + upTailscale: 'Upload tailscale to NanoKVM directory /usr/bin/', + upTailscaled: 'Upload tailscaled to NanoKVM directory /usr/sbin/', + refresh: 'Refresh current page', + notRunning: 'Tailscale is not running. Please start it to continue.', + run: 'Start', notLogin: - 'Das Gerät konnte noch nicht gefunden werden. Bitte melden Sie sich an und verknüpfen Sie dieses Gerät mit Ihrem Konto.', - urlPeriod: 'Diese URL ist für 10 Minuten gültig', - login: 'Anmelden', - loginSuccess: 'Anmeldung erfolgreich', - enable: 'Tailscale einschalten', - deviceName: 'Geräte Name', - deviceIP: 'Geräte IP', - account: 'Konto', - logout: 'Abmelden', - logoutDesc: 'Möchten Sie sich wirklich abmelden?', - uninstall: 'Tailscale deinstallieren', - okBtn: 'Ja', - cancelBtn: 'Nein' + 'The device has not been bound yet. Please login and bind this device to your account.', + urlPeriod: 'This url is valid for 10 minutes', + login: 'Login', + loginSuccess: 'Login Success', + enable: 'Enable Tailscale', + deviceName: 'Device Name', + deviceIP: 'Device IP', + account: 'Account', + logout: 'Logout', + logoutDesc: 'Are you sure you want to logout?', + uninstall: 'Uninstall Tailscale', + uninstallDesc: 'Are you sure you want to uninstall Tailscale?', + okBtn: 'Yes', + cancelBtn: 'No' }, update: { - title: 'Nach Aktualisierungen suchen', - queryFailed: 'Version konnte nicht abgefragt werden', - updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.', - isLatest: 'Sie haben bereits die aktuellste Version.', - available: 'Eine Aktualisierung ist verfügbar. Möchten sie diese jetzt durchführen?', - updating: 'Aktualisierung gestartet. Bitte warten...', - confirm: 'Bestätigen', - cancel: 'Abbrechen', - preview: 'Vorab-Versionen', - previewDesc: 'Erhalten Sie vorab Zugriff auf neue Funktionen und Verbesserungen', - previewTip: 'Bitte beachten Sie, dass Vorab-Versionen womöglich noch Fehler oder unvollständige Funktionen enthalten!' + title: 'Check for Updates', + queryFailed: 'Get version failed', + updateFailed: 'Update failed. Please retry.', + isLatest: 'You already have the latest version.', + available: 'An update is available. Are you sure you want to update now?', + updating: 'Update started. Please wait...', + confirm: 'Confirm', + cancel: 'Cancel', + preview: 'Preview Updates', + previewDesc: 'Get early access to new features and improvements', + previewTip: + 'Please be aware that preview releases may contain bugs or incomplete functionality!' }, offlineupdate: { - title: 'Offline Aktualisierung', - queryFailed: 'Version konnte nicht abgefragt werden', - updateFailed: 'Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut.', - updating: 'Aktualisierung gestartet. Bitte warten...', - confirm: 'Bestätigen', - cancel: 'Abbrechen', - inputfile: "Bitte geben Sie die Datei für das Update an", - noupdatefile: "Keine Update Datei", + title: 'Offline Updates', + queryFailed: 'Get version failed', + updateFailed: 'Update failed. Please retry.', + updating: 'Update started. Please wait...', + confirm: 'Confirm', + cancel: 'Cancel', + inputfile: "Please enter the Update File", + noupdatefile: "No update file", version: "Version", uploadbox: "Drop file here or click to select", ok: 'Ok' }, account: { - title: 'Konto', - webAccount: 'Web Konto Name', - password: 'Passwort', - updateBtn: 'Ändern', - logoutBtn: 'Abmelden', - logoutDesc: 'Möchten Sie sich wirklich abmelden?', - okBtn: 'Ja', - cancelBtn: 'Nein' + title: 'Account', + webAccount: 'Web Account Name', + password: 'Password', + updateBtn: 'Change', + logoutBtn: 'Logout', + logoutDesc: 'Are you sure you want to logout?', + okBtn: 'Yes', + cancelBtn: 'No' } }, error: { - title: "Wir sind auf ein Problem gestossen", - refresh: 'Neuladen' + title: "We've ran into an issue", + refresh: 'Refresh' }, fullscreen: { - toggle: 'Vollbild ein/aus' + toggle: 'Toggle Fullscreen' }, menu: { - collapse: 'Menu einblenden', - expand: 'Menu verbergen' + collapse: 'Collapse Menu', + expand: 'Expand Menu' } } }; -export default de; +export default en; From ed9b7c5126d57546f0bec3575a9338a62e7c70f2 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:37:17 +0100 Subject: [PATCH 36/43] Update index.tsx --- web/src/pages/desktop/menu/settings/offlineupdate/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx b/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx index 282d6d6..769e5a2 100644 --- a/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx +++ b/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx @@ -150,7 +150,7 @@ export const OfflineUpdate = ({ setIsLocked }: UpdateProps) => { }} > - {selectedFile ? selectedFile.name : t('download.uploadbox')} + {selectedFile ? selectedFile.name : t('settings.offlineupdate.uploadbox')} { onClick={() => upload(selectedFile)} disabled={!selectedFile} > - {t('download.ok')} + {t('settings.offlineupdate.ok')}
From afdbb179adb384ead8ce9ad571962eb17accca02 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:37:44 +0100 Subject: [PATCH 37/43] Update update.go --- server/service/application/update.go | 278 +++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) diff --git a/server/service/application/update.go b/server/service/application/update.go index 9993397..0481fc4 100644 --- a/server/service/application/update.go +++ b/server/service/application/update.go @@ -10,6 +10,9 @@ import ( "os/exec" "sync" "time" + "path/filepath" + "strings" + "regexp" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" @@ -18,6 +21,8 @@ import ( "NanoKVM-Server/utils" ) +var sentinelPath = "/tmp/.download_in_progress" + const ( maxTries = 3 ) @@ -27,6 +32,228 @@ var ( isUpdating bool ) +func (s *Service) UploadUpdate(c *gin.Context) { + var rsp proto.Response + + updateMutex.Lock() + if isUpdating { + updateMutex.Unlock() + rsp.ErrRsp(c, -1, "update already in progress") + return + } + isUpdating = true + updateMutex.Unlock() + + defer func() { + updateMutex.Lock() + isUpdating = false + updateMutex.Unlock() + }() + + if err := uploadupdate(rsp, c); err != nil { + rsp.ErrRsp(c, -1, fmt.Sprintf("update failed: %s", err)) + return + } + + rsp.OkRsp(c) + log.Debugf("update application success") + + // Sleep for a second before restarting the device + time.Sleep(1 * time.Second) + + _ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run() +} + +func uploadupdate(rsp proto.Response, c *gin.Context) error { + _ = os.RemoveAll(CacheDir) + _ = os.MkdirAll(CacheDir, 0o755) + defer func() { + _ = os.RemoveAll(CacheDir) + }() + + // Set a sentinel file to mark that there is a download in progress + // This is to prevent multiple downloads at the same time + if _, err := os.Stat(sentinelPath); err == nil { + log.Debug("Download in progress") + rsp.ErrRsp(c, -1, "download in progress") + return err + } + + // Create the sentinel file + err := os.WriteFile(sentinelPath, []byte("start"), 0644) + if err != nil { + log.Error("Failed to create sentinel file") + rsp.ErrRsp(c, -1, "failed to create sentinel file") + return err + } + + // Multipart Reader direkt nutzen (keine FormFile!) + reader, err := c.Request.MultipartReader() + if err != nil { + log.Error("invalid multipart data") + rsp.ErrRsp(c, -1, "invalid multipart data") + defer os.Remove(sentinelPath) + return err + } + + + var lw *loggingWriter + var outPath = "" + + for { + part, err := reader.NextPart() + if err == io.EOF { + break + } + if err != nil { + log.Error("failed to read part") + rsp.ErrRsp(c, -1, "failed to read part") + lw.stopTicker() + defer os.Remove(sentinelPath) + return err + } + + if part.FormName() != "file" { + continue + } + + filename := part.FileName() + if filename == "" { + log.Error("no filename") + rsp.ErrRsp(c, -1, "no filename") + lw.stopTicker() + defer os.Remove(sentinelPath) + return fmt.Errorf("err0") + } + + filename = filepath.Base(filename) + + if filename != part.FileName() { + log.Warn("path detected in filename") + rsp.ErrRsp(c, -1, "invalid filename") + defer os.Remove(sentinelPath) + return fmt.Errorf("err1") + } + + if strings.Contains(filename, "..") { + log.Warn("path traversal attempt") + rsp.ErrRsp(c, -1, "invalid filename") + defer os.Remove(sentinelPath) + return fmt.Errorf("err2") + } + + if !strings.HasSuffix(strings.ToLower(filename), ".iso") { + rsp.ErrRsp(c, -1, "only .iso files allowed") + defer os.Remove(sentinelPath) + return fmt.Errorf("err3") + } + + valid := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) + if !valid.MatchString(filename) { + rsp.ErrRsp(c, -1, "invalid filename") + defer os.Remove(sentinelPath) + return fmt.Errorf("err4") + } + + data, err := os.ReadFile(sentinelPath) + if err != nil { + log.Error("Read failed") + rsp.ErrRsp(c, -1, "Read failed") + lw.stopTicker() + defer os.Remove(sentinelPath) + return err + } + + outPath = "/data/" + filename + out, err := os.Create(outPath) + if err != nil { + log.Error("cannot create file") + rsp.ErrRsp(c, -1, "cannot create file") + lw.stopTicker() + defer os.Remove(sentinelPath) + return err + } + defer out.Close() + + if strings.Contains(string(data), "start") { + err = os.WriteFile(sentinelPath, []byte(filename), 0644) + if err != nil { + log.Error("Failed to create sentinel file") + rsp.ErrRsp(c, -1, "failed to create sentinel file") + lw.stopTicker() + defer os.Remove(outPath) + defer os.Remove(sentinelPath) + return err + } + + lw = &loggingWriter{writer: out, totalSize: c.Request.ContentLength} + lw.startTicker() + } else { + if !strings.Contains(string(data), filename) { + log.Error("failed") + rsp.ErrRsp(c, -1, "failed") + lw.stopTicker() + defer os.Remove(outPath) + defer os.Remove(sentinelPath) + return fmt.Errorf("err5") + } + } + + // Direkt streamen → kein RAM-Bedarf außer kleinem Buffer + _, err = io.Copy(lw, part) + if err != nil { + log.Error("write failed") + rsp.ErrRsp(c, -1, "write failed") + lw.stopTicker() + defer os.Remove(outPath) + defer os.Remove(sentinelPath) + return err + } + } + lw.stopTicker() + + rsp.OkRspWithData(c, &proto.StatusImageRsp{ + Status: "idle", + File: "", + Percentage: "", + }) + + defer os.Remove(sentinelPath) + + // decompress + dir, err := utils.UnTarGz(outPath, CacheDir) + log.Debugf("untar: %s", dir) + if err != nil { + log.Errorf("decompress app failed: %s", err) + return err + } + + // backup old version + if err := os.RemoveAll(BackupDir); err != nil { + log.Errorf("remove backup failed: %s", err) + return err + } + + if err := utils.MoveFilesRecursively(AppDir, BackupDir); err != nil { + log.Errorf("backup app failed: %s", err) + return err + } + + // update + if err := utils.MoveFilesRecursively(dir, AppDir); err != nil { + log.Errorf("failed to move update back in place: %s", err) + return err + } + + // modify permissions + if err := utils.ChmodRecursively(AppDir, 0o755); err != nil { + log.Errorf("chmod failed: %s", err) + return err + } + + return nil +} + func (s *Service) Update(c *gin.Context) { var rsp proto.Response @@ -172,3 +399,54 @@ func checksum(filePath string, expectedHash string) error { return nil } + +type loggingWriter struct { + writer io.Writer + total int64 + totalSize int64 + ticker *time.Ticker + done chan bool +} + +func (lw *loggingWriter) startTicker() { + lw.ticker = time.NewTicker(2500 * time.Millisecond) + lw.done = make(chan bool) + go func() { + for { + select { + case <-lw.done: + return + case <-lw.ticker.C: + lw.updateSentinel() + } + } + }() +} + +func (lw *loggingWriter) stopTicker() { + lw.ticker.Stop() + lw.done <- true +} + +func (lw *loggingWriter) updateSentinel() { + percentage := float64(lw.total) / float64(lw.totalSize) * 100 + content, err := os.ReadFile(sentinelPath) + if err != nil { + log.Error("Failed to read sentinel file") + return + } + splitted := strings.Split(string(content), ";") + if len(splitted) == 0 { + return + } + err = os.WriteFile(sentinelPath, []byte(fmt.Sprintf("%s;%.2f%%", splitted[0], percentage)), 0644) + if err != nil { + log.Error("Failed to update sentinel file") + } +} + +func (lw *loggingWriter) Write(p []byte) (int, error) { + n, err := lw.writer.Write(p) + lw.total += int64(n) + return n, err +} From c0cfcfa31a5b559a7fdb3f28ab953ae42220a820 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:44:30 +0100 Subject: [PATCH 38/43] Update de.ts --- web/src/i18n/locales/de.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/i18n/locales/de.ts b/web/src/i18n/locales/de.ts index 34f49d6..bf358f1 100644 --- a/web/src/i18n/locales/de.ts +++ b/web/src/i18n/locales/de.ts @@ -327,8 +327,7 @@ const de = { cancel: 'Abbrechen', inputfile: "Bitte geben Sie die Datei für das Update an", noupdatefile: "Keine Update Datei", - version: "Version", - uploadbox: "Drop file here or click to select", + uploadbox: "Datei hier ablegen oder zum Auswählen klicken", ok: 'Ok' }, account: { From d6b03a345b7b1e0bc9352c9e86e049acd529ca74 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:44:46 +0100 Subject: [PATCH 39/43] Update en.ts --- web/src/i18n/locales/en.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index f6abbe8..be03d53 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -360,7 +360,6 @@ const en = { cancel: 'Cancel', inputfile: "Please enter the Update File", noupdatefile: "No update file", - version: "Version", uploadbox: "Drop file here or click to select", ok: 'Ok' }, From aa80e1d7cc6755d50299e2b565af4f58f0f4af89 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:45:23 +0100 Subject: [PATCH 40/43] Update index.tsx --- web/src/pages/desktop/menu/settings/offlineupdate/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx b/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx index 769e5a2..9005ea0 100644 --- a/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx +++ b/web/src/pages/desktop/menu/settings/offlineupdate/index.tsx @@ -104,7 +104,7 @@ export const OfflineUpdate = ({ setIsLocked }: UpdateProps) => { return ( <> -
{t('settings.offlineupdate.title') + ", " + t('settings.offlineupdate.version') + ": " + currentVersion}
+
{t('settings.offlineupdate.title') + ", " + t('settings.about.application') + ": " + currentVersion}
From f0e7dc069c78262d89cf5f6fc83f2363378d101b Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 15:55:10 +0100 Subject: [PATCH 42/43] Update update.go --- server/service/application/update.go | 41 ++++++---------------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/server/service/application/update.go b/server/service/application/update.go index 0481fc4..01d5a19 100644 --- a/server/service/application/update.go +++ b/server/service/application/update.go @@ -106,8 +106,6 @@ func uploadupdate(rsp proto.Response, c *gin.Context) error { break } if err != nil { - log.Error("failed to read part") - rsp.ErrRsp(c, -1, "failed to read part") lw.stopTicker() defer os.Remove(sentinelPath) return err @@ -119,33 +117,23 @@ func uploadupdate(rsp proto.Response, c *gin.Context) error { filename := part.FileName() if filename == "" { - log.Error("no filename") - rsp.ErrRsp(c, -1, "no filename") lw.stopTicker() defer os.Remove(sentinelPath) - return fmt.Errorf("err0") + return fmt.Errorf("no filename") } filename = filepath.Base(filename) if filename != part.FileName() { - log.Warn("path detected in filename") - rsp.ErrRsp(c, -1, "invalid filename") defer os.Remove(sentinelPath) - return fmt.Errorf("err1") + return fmt.Errorf("path detected in filename") } if strings.Contains(filename, "..") { log.Warn("path traversal attempt") rsp.ErrRsp(c, -1, "invalid filename") defer os.Remove(sentinelPath) - return fmt.Errorf("err2") - } - - if !strings.HasSuffix(strings.ToLower(filename), ".iso") { - rsp.ErrRsp(c, -1, "only .iso files allowed") - defer os.Remove(sentinelPath) - return fmt.Errorf("err3") + return fmt.Errorf("path traversal attempt") } valid := regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) @@ -157,8 +145,6 @@ func uploadupdate(rsp proto.Response, c *gin.Context) error { data, err := os.ReadFile(sentinelPath) if err != nil { - log.Error("Read failed") - rsp.ErrRsp(c, -1, "Read failed") lw.stopTicker() defer os.Remove(sentinelPath) return err @@ -167,8 +153,6 @@ func uploadupdate(rsp proto.Response, c *gin.Context) error { outPath = "/data/" + filename out, err := os.Create(outPath) if err != nil { - log.Error("cannot create file") - rsp.ErrRsp(c, -1, "cannot create file") lw.stopTicker() defer os.Remove(sentinelPath) return err @@ -178,8 +162,6 @@ func uploadupdate(rsp proto.Response, c *gin.Context) error { if strings.Contains(string(data), "start") { err = os.WriteFile(sentinelPath, []byte(filename), 0644) if err != nil { - log.Error("Failed to create sentinel file") - rsp.ErrRsp(c, -1, "failed to create sentinel file") lw.stopTicker() defer os.Remove(outPath) defer os.Remove(sentinelPath) @@ -190,20 +172,16 @@ func uploadupdate(rsp proto.Response, c *gin.Context) error { lw.startTicker() } else { if !strings.Contains(string(data), filename) { - log.Error("failed") - rsp.ErrRsp(c, -1, "failed") lw.stopTicker() defer os.Remove(outPath) defer os.Remove(sentinelPath) - return fmt.Errorf("err5") + return fmt.Errorf("failed") } } // Direkt streamen → kein RAM-Bedarf außer kleinem Buffer _, err = io.Copy(lw, part) if err != nil { - log.Error("write failed") - rsp.ErrRsp(c, -1, "write failed") lw.stopTicker() defer os.Remove(outPath) defer os.Remove(sentinelPath) @@ -222,32 +200,31 @@ func uploadupdate(rsp proto.Response, c *gin.Context) error { // decompress dir, err := utils.UnTarGz(outPath, CacheDir) - log.Debugf("untar: %s", dir) if err != nil { - log.Errorf("decompress app failed: %s", err) + fmt.Errorf("decompress app failed: %s", err) return err } // backup old version if err := os.RemoveAll(BackupDir); err != nil { - log.Errorf("remove backup failed: %s", err) + fmt.Errorf("remove backup failed: %s", err) return err } if err := utils.MoveFilesRecursively(AppDir, BackupDir); err != nil { - log.Errorf("backup app failed: %s", err) + fmt.Errorf("backup app failed: %s", err) return err } // update if err := utils.MoveFilesRecursively(dir, AppDir); err != nil { - log.Errorf("failed to move update back in place: %s", err) + fmt.Errorf("failed to move update back in place: %s", err) return err } // modify permissions if err := utils.ChmodRecursively(AppDir, 0o755); err != nil { - log.Errorf("chmod failed: %s", err) + fmt.Errorf("chmod failed: %s", err) return err } From 969e3a5e6a68d54e02c9b54382843cac59c50433 Mon Sep 17 00:00:00 2001 From: Alexander-Ger-Reich <50119493+Alexander-Ger-Reich@users.noreply.github.com> Date: Sat, 13 Dec 2025 16:00:09 +0100 Subject: [PATCH 43/43] Update update.go --- server/service/application/update.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/service/application/update.go b/server/service/application/update.go index 01d5a19..0b6b0c9 100644 --- a/server/service/application/update.go +++ b/server/service/application/update.go @@ -228,6 +228,8 @@ func uploadupdate(rsp proto.Response, c *gin.Context) error { return err } + defer os.Remove(outPath) + return nil }