initial commit

This commit is contained in:
2025-12-10 22:50:35 +09:00
parent 8cf674c9e5
commit 6b76389942
12 changed files with 393 additions and 493 deletions

View File

@@ -3,6 +3,7 @@ package proto
type GetVersionRsp struct {
Current string `json:"current"`
Latest string `json:"latest"`
UpdateUrl string `json:"update_url"`
}
type GetPreviewRsp struct {

View File

@@ -13,6 +13,8 @@ func applicationRouter(r *gin.Engine) {
api.GET("/application/version", service.GetVersion) // get application version
api.POST("/application/update", service.Update) // update application
api.POST("/application/update/server", service.UpdateServer)
api.POST("/application/update/web", service.UpdateWeb)
api.GET("/application/preview", service.GetPreview) // get preview updates state
api.POST("/application/preview", service.SetPreview) // set preview updates state

View File

@@ -0,0 +1,161 @@
package application
import (
"NanoKVM-Server/proto"
"NanoKVM-Server/utils"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
func (s *Service) UpdateServer(c *gin.Context) {
var rsp proto.Response
file, err := c.FormFile("file")
if err != nil {
rsp.ErrRsp(c, -1, "invalid file")
return
}
// Get executable path
execPath, err := os.Executable()
if err != nil {
rsp.ErrRsp(c, -2, "get executable path failed")
return
}
execDir := filepath.Dir(execPath)
// Save to temp file in SAME directory to ensure atomic rename
tmpPath := filepath.Join(execDir, "NanoKVM-Server.new")
if err := c.SaveUploadedFile(file, tmpPath); err != nil {
rsp.ErrRsp(c, -3, "save file failed")
return
}
// Verify file size
info, err := os.Stat(tmpPath)
if err != nil || info.Size() == 0 {
_ = os.Remove(tmpPath)
rsp.ErrRsp(c, -4, "invalid file size")
return
}
// Verify it's executable
if err := os.Chmod(tmpPath, 0755); err != nil {
_ = os.Remove(tmpPath)
rsp.ErrRsp(c, -5, "chmod failed")
return
}
// Backup current binary
backupPath := execPath + ".bak"
// Try to remove old backup first
_ = os.Remove(backupPath)
if err := os.Rename(execPath, backupPath); err != nil {
// If rename fails (e.g. running binary locked?), try copy
if err := utils.CopyFile(execPath, backupPath); err != nil {
log.Warnf("backup failed: %v", err)
// Proceed with caution or fail?
// If we can't backup, maybe we shouldn't proceed.
// But for embedded systems, sometimes we just overwrite.
}
}
// Replace binary
if err := os.Rename(tmpPath, execPath); err != nil {
// Attempt rollback
_ = os.Rename(backupPath, execPath)
rsp.ErrRsp(c, -6, "replace binary failed")
return
}
rsp.OkRsp(c)
// Restart service in background
go func() {
time.Sleep(1 * time.Second)
_ = exec.Command("sh", "-c", "/etc/init.d/S95nanokvm restart").Run()
}()
}
func (s *Service) UpdateWeb(c *gin.Context) {
var rsp proto.Response
file, err := c.FormFile("file")
if err != nil {
rsp.ErrRsp(c, -1, "invalid file")
return
}
// Get executable path
execPath, err := os.Executable()
if err != nil {
rsp.ErrRsp(c, -2, "get executable path failed")
return
}
execDir := filepath.Dir(execPath)
// Save to temp file in SAME directory
tmpPath := filepath.Join(execDir, "web.tar.gz.new")
if err := c.SaveUploadedFile(file, tmpPath); err != nil {
rsp.ErrRsp(c, -3, "save file failed")
return
}
// Verify file size
info, err := os.Stat(tmpPath)
if err != nil || info.Size() == 0 {
_ = os.Remove(tmpPath)
rsp.ErrRsp(c, -4, "invalid file size")
return
}
// Define web directory
webDir := filepath.Join(execDir, "web")
// Extract to temp dir first
extractDir := filepath.Join(execDir, "web_extract_tmp")
os.RemoveAll(extractDir)
os.MkdirAll(extractDir, 0755)
if _, err := utils.UnTarGz(tmpPath, extractDir); err != nil {
os.RemoveAll(extractDir)
_ = os.Remove(tmpPath)
rsp.ErrRsp(c, -5, "extract failed")
return
}
// Check if there is a single directory inside extractDir (e.g. 'dist')
entries, _ := os.ReadDir(extractDir)
sourceDir := extractDir
if len(entries) == 1 && entries[0].IsDir() {
sourceDir = filepath.Join(extractDir, entries[0].Name())
}
// Backup old web dir
backupWebDir := filepath.Join(execDir, "web.bak")
os.RemoveAll(backupWebDir)
_ = os.Rename(webDir, backupWebDir)
// Move new web dir
if err := os.Rename(sourceDir, webDir); err != nil {
// Rollback
_ = os.Rename(backupWebDir, webDir)
os.RemoveAll(extractDir)
_ = os.Remove(tmpPath)
rsp.ErrRsp(c, -6, "replace web dir failed")
return
}
// Cleanup
os.RemoveAll(extractDir)
os.RemoveAll(backupWebDir)
_ = os.Remove(tmpPath)
rsp.OkRsp(c)
}

View File

@@ -26,7 +26,7 @@ func (s *Service) GetVersion(c *gin.Context) {
var rsp proto.Response
// current version
currentVersion := "1.0.0"
currentVersion := "2.3.0.b1"
versionFile := fmt.Sprintf("%s/version", AppDir)
if version, err := os.ReadFile(versionFile); err == nil {
@@ -45,6 +45,7 @@ func (s *Service) GetVersion(c *gin.Context) {
rsp.OkRspWithData(c, &proto.GetVersionRsp{
Current: currentVersion,
Latest: latest.Version,
UpdateUrl: StableURL,
})
}

View File

@@ -73,7 +73,7 @@ func (c *Cli) Stop() error {
}
func (c *Cli) Up() error {
command := "tailscale up --accept-dns=false"
command := "tailscale up --accept-dns=false --login-server=https://headscale.tindevil.com --hostname=batchuKVM"
return exec.Command("sh", "-c", command).Run()
}
@@ -111,7 +111,7 @@ func (c *Cli) Status() (*TsStatus, error) {
}
func (c *Cli) Login() (string, error) {
command := "tailscale login --accept-dns=false --timeout=10m"
command := "tailscale login --accept-dns=false --timeout=10m --login-server=https://headscale.tindevil.com --hostname=batchuKVM"
cmd := exec.Command("sh", "-c", command)
stderr, err := cmd.StderrPipe()

39
server/utils/copy_file.go Normal file
View File

@@ -0,0 +1,39 @@
package utils
import (
"io"
"os"
)
func CopyFile(src, dst string) error {
sourceFileStat, err := os.Stat(src)
if err != nil {
return err
}
if !sourceFileStat.Mode().IsRegular() {
return os.ErrInvalid
}
source, err := os.Open(src)
if err != nil {
return err
}
defer func() {
_ = source.Close()
}()
destination, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
_ = destination.Close()
}()
if _, err := io.Copy(destination, source); err != nil {
return err
}
return os.Chmod(dst, sourceFileStat.Mode())
}