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 +}