Refactor: Rename NanoKVM to BatchuKVM and update server URL

This commit is contained in:
2025-12-09 20:35:38 +09:00
commit 8cf674c9e5
396 changed files with 54380 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
package application
import (
"NanoKVM-Server/proto"
"os"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
PreviewUpdatesFlag = "/etc/kvm/preview_updates"
)
func (s *Service) GetPreview(c *gin.Context) {
var rsp proto.Response
isEnabled := isPreviewEnabled()
rsp.OkRspWithData(c, &proto.GetPreviewRsp{
Enabled: isEnabled,
})
}
func (s *Service) SetPreview(c *gin.Context) {
var req proto.SetPreviewReq
var rsp proto.Response
if err := proto.ParseFormRequest(c, &req); err != nil {
rsp.ErrRsp(c, -1, "invalid arguments")
return
}
if req.Enable == isPreviewEnabled() {
rsp.OkRsp(c)
return
}
if req.Enable {
if err := os.WriteFile(PreviewUpdatesFlag, []byte("1"), 0o644); err != nil {
log.Errorf("failed to write %s: %s", PreviewUpdatesFlag, err)
rsp.ErrRsp(c, -2, "enable failed")
return
}
} else {
if err := os.Remove(PreviewUpdatesFlag); err != nil {
log.Errorf("failed to remove %s: %s", PreviewUpdatesFlag, err)
rsp.ErrRsp(c, -3, "disable failed")
return
}
}
rsp.OkRsp(c)
log.Debugf("set preview updates state: %t", req.Enable)
}
func isPreviewEnabled() bool {
_, err := os.Stat(PreviewUpdatesFlag)
return err == nil
}

View File

@@ -0,0 +1,16 @@
package application
const (
StableURL = "https://update.tindevil.com/batchukvm"
PreviewURL = "https://update.tindevil.com/batchukvm/preview"
AppDir = "/kvmapp"
BackupDir = "/root/old"
CacheDir = "/root/.kvmcache"
)
type Service struct{}
func NewService() *Service {
return &Service{}
}

View File

@@ -0,0 +1,174 @@
package application
import (
"crypto/sha512"
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"sync"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"NanoKVM-Server/proto"
"NanoKVM-Server/utils"
)
const (
maxTries = 3
)
var (
updateMutex sync.Mutex
isUpdating bool
)
func (s *Service) Update(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 := update(); 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 update() error {
_ = os.RemoveAll(CacheDir)
_ = os.MkdirAll(CacheDir, 0o755)
defer func() {
_ = os.RemoveAll(CacheDir)
}()
// get latest information
latest, err := getLatest()
if err != nil {
return err
}
// download
target := fmt.Sprintf("%s/%s", CacheDir, latest.Name)
if err := download(latest.Url, target); err != nil {
log.Errorf("download app failed: %s", err)
return err
}
// check sha512
if err := checksum(target, latest.Sha512); err != nil {
log.Errorf("check sha512 failed: %s", err)
return err
}
// decompress
dir, err := utils.UnTarGz(target, 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 download(url string, target string) (err error) {
for i := range maxTries {
log.Debugf("attempt #%d/%d", i+1, maxTries)
if i > 0 {
time.Sleep(time.Second * 3) // wait for 3 seconds before retrying the download attempt
}
var req *http.Request
req, err = http.NewRequest("GET", url, nil)
if err != nil {
log.Errorf("new request err: %s", err)
continue
}
log.Debugf("update will be saved to: %s", target)
err = utils.Download(req, target)
if err != nil {
log.Errorf("downloading latest application failed, try again...")
continue
}
return nil
}
return err
}
func checksum(filePath string, expectedHash string) error {
file, err := os.Open(filePath)
if err != nil {
log.Errorf("failed to open file %s: %v", filePath, err)
return err
}
defer func() {
_ = file.Close()
}()
hasher := sha512.New()
_, err = io.Copy(hasher, file)
if err != nil {
log.Errorf("failed to copy file contents to hasher: %v", err)
return err
}
hash := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
if hash != expectedHash {
log.Errorf("invalid sha512 %s", hash)
return fmt.Errorf("invalid sha512 %s", hash)
}
return nil
}

View File

@@ -0,0 +1,89 @@
package application
import (
"NanoKVM-Server/proto"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
type Latest struct {
Version string `json:"version"`
Name string `json:"name"`
Sha512 string `json:"sha512"`
Size uint `json:"size"`
Url string `json:"url"`
}
func (s *Service) GetVersion(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)
// latest version
latest, err := getLatest()
if err != nil {
rsp.ErrRsp(c, -1, "get latest version failed")
return
}
rsp.OkRspWithData(c, &proto.GetVersionRsp{
Current: currentVersion,
Latest: latest.Version,
})
}
func getLatest() (*Latest, error) {
baseURL := StableURL
if isPreviewEnabled() {
baseURL = PreviewURL
}
url := fmt.Sprintf("%s/latest.json?now=%d", baseURL, time.Now().Unix())
resp, err := http.Get(url)
if err != nil {
log.Debugf("failed to request version: %v", err)
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf("failed to read response: %v", err)
return nil, err
}
if resp.StatusCode != http.StatusOK {
log.Errorf("server responded with status code: %d", resp.StatusCode)
return nil, fmt.Errorf("status code %d", resp.StatusCode)
}
var latest Latest
if err := json.Unmarshal(body, &latest); err != nil {
log.Errorf("failed to unmarshal response: %s", err)
return nil, err
}
latest.Url = fmt.Sprintf("%s/%s", baseURL, latest.Name)
log.Debugf("get application latest version: %s", latest.Version)
return &latest, nil
}