diff --git a/.luarc.json b/.luarc.json index eebfd24..e5e2b0f 100644 --- a/.luarc.json +++ b/.luarc.json @@ -2,6 +2,5 @@ "runtime.version": "Lua 5.1", "workspace.library": [ "./scripts/_globals.lua" - ], - "diagnostics.globals": ["Params", "Result"] + ] } \ No newline at end of file diff --git a/Makefile b/Makefile index dd66151..234eb65 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ APP_NAME := node BIN_DIR := bin GOPATH := $(shell go env GOPATH) export CONFIG_PATH := ./config.yaml +LDFLAGS := -X 'github.com/akyaiy/GoSally-mvp/core/config.NodeVersion=version0.0.1-dev' CGO_CFLAGS := -I/usr/local/include CGO_LDFLAGS := -L/usr/local/lib -llua5.1 -lm -ldl .PHONY: all build run runq test fmt vet lint check clean @@ -24,9 +25,10 @@ setup: lint-setup goimports-setup golicenses-setup build: @echo "Building..." - @echo "CGO_CFLAGS is: '$(CGO_CFLAGS)'" - @echo "CGO_LDFLAGS is: '$(CGO_LDFLAGS)'" - CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" go build -o $(BIN_DIR)/$(APP_NAME) ./cmd/$(APP_NAME) + @# @echo "CGO_CFLAGS is: '$(CGO_CFLAGS)'" + @# @echo "CGO_LDFLAGS is: '$(CGO_LDFLAGS)'" + @# CGO_CFLAGS="$(CGO_CFLAGS)" CGO_LDFLAGS="$(CGO_LDFLAGS)" + go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/$(APP_NAME) ./cmd/$(APP_NAME) run: build @echo "Running!" @@ -56,4 +58,7 @@ licenses: @echo "Licenses have been exported to third_party/licenses" clean: - @rm -rf bin \ No newline at end of file + @rm -rf bin + +help: + @echo "Available commands: $$(cat Makefile | grep -E '^[a-zA-Z_-]+:.*?' | grep -v -- '-setup:' | sed 's/:.*//g' | sort | uniq | tr '\n' ' ')" diff --git a/cmd/node/node.go b/cmd/node/node.go index 239ae1d..a2488e3 100644 --- a/cmd/node/node.go +++ b/cmd/node/node.go @@ -1,14 +1,19 @@ package main import ( + "fmt" "log/slog" + "net" "net/http" "regexp" + "golang.org/x/net/netutil" + "github.com/akyaiy/GoSally-mvp/core/config" gs "github.com/akyaiy/GoSally-mvp/core/general_server" "github.com/akyaiy/GoSally-mvp/core/logs" "github.com/akyaiy/GoSally-mvp/core/sv1" + "github.com/akyaiy/GoSally-mvp/core/update" "github.com/go-chi/chi/v5" ) @@ -27,34 +32,63 @@ func init() { } func main() { + updater := update.NewUpdater(*log, cfg) + versuion, versionType, _ := updater.GetCurrentVersion() + fmt.Printf("Current version: %s (%s)\n", versuion, versionType) + ver, vert, _ := updater.GetLatestVersion(versionType) + fmt.Printf("Latest version: %s (%s)\n", ver, vert) + + fmt.Println("Checking for updates...") + isNewUpdate, _ := updater.CkeckUpdates() + fmt.Println("Update check result:", isNewUpdate) serverv1 := sv1.InitV1Server(&sv1.HandlerV1InitStruct{ - Log: *logs.SetupLogger(cfg.Mode), + Log: *log, Config: cfg, AllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9]+$`), ListAllowedCmd: regexp.MustCompile(`^[a-zA-Z0-9_-]+$`), Ver: "v1", }) s := gs.InitGeneral(&gs.GeneralServerInit{ - Log: *logs.SetupLogger(cfg.Mode), + Log: *log, Config: cfg, }, serverv1) r := chi.NewRouter() - r.Route("/api/{ver}/com", func(r chi.Router) { + r.Route(config.GetServerConsts().GetApiRoute()+config.GetServerConsts().GetComDirRoute(), func(r chi.Router) { r.Get("/", s.HandleList) r.Get("/{cmd}", s.Handle) }) + r.Route("/favicon.ico", func(r chi.Router) { + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + }) r.NotFound(serverv1.ErrNotFound) - if cfg.TlsEnabled == "true" { - log.Info("Server started with TLS", slog.String("address", cfg.Address)) - err := http.ListenAndServeTLS(cfg.Address, cfg.CertFile, cfg.KeyFile, r) + + address := cfg.Address + if cfg.TlsEnabled { + log.Info("HTTPS server started with TLS", slog.String("address", address)) + listener, err := net.Listen("tcp", address) + if err != nil { + log.Error("Failed to start TLS listener", slog.String("error", err.Error())) + return + } + limitedListener := netutil.LimitListener(listener, 100) + err = http.ServeTLS(limitedListener, r, cfg.CertFile, cfg.KeyFile) if err != nil { log.Error("Failed to start HTTPS server", slog.String("error", err.Error())) } - } - log.Info("Server started", slog.String("address", cfg.Address)) - err := http.ListenAndServe(cfg.Address, r) - if err != nil { - log.Error("Failed to start HTTP server", slog.String("error", err.Error())) + } else { + log.Info("HTTP server started", slog.String("address", address)) + listener, err := net.Listen("tcp", address) + if err != nil { + log.Error("Failed to start listener", slog.String("error", err.Error())) + return + } + limitedListener := netutil.LimitListener(listener, 100) + err = http.Serve(limitedListener, r) + if err != nil { + log.Error("Failed to start HTTP server", slog.String("error", err.Error())) + } } } diff --git a/com/_globals.lua b/com/_globals.lua index 611f429..d7442e5 100644 --- a/com/_globals.lua +++ b/com/_globals.lua @@ -1,7 +1,11 @@ ---@alias AnyTable table ---@type AnyTable -Params = {} +In = { + Params = {}, +} ---@type AnyTable -Result = {} \ No newline at end of file +Out = { + Result = {}, +} \ No newline at end of file diff --git a/com/_prepare.lua b/com/_prepare.lua index f083f30..6952840 100644 --- a/com/_prepare.lua +++ b/com/_prepare.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: duplicate-set-field package.path = package.path .. ";/usr/lib64/lua/5.1/?.lua;/usr/local/share/lua/5.1/?.lua" .. ";./com/?.lua;" package.cpath = package.cpath .. ";/usr/lib64/lua/5.1/?.so;/usr/local/lib/lua/5.1/?.so" @@ -5,4 +6,11 @@ print = function() end io.write = function(...) end io.stdout = function() return nil end io.stderr = function() return nil end -io.read = function(...) return nil end \ No newline at end of file +io.read = function(...) return nil end + +---@type table +Status = { + ok = "ok", + error = "error", + invalid = "invalid", +} diff --git a/com/echo.lua b/com/echo.lua index 715beab..04603e6 100644 --- a/com/echo.lua +++ b/com/echo.lua @@ -2,12 +2,12 @@ --- #args --- msg = the message -if not Params.msg then - Result.status = "error" - Result.error = "Missing parameter: msg" +if not In.Params.msg or In.Params.msg == "" then + Out.Result.status = Status.error + Out.Result.error = "Missing parameter: msg" return end -Result.status = "ok" -Result.answer = Params.msg +Out.Result.status = Status.ok +Out.Result.answer = In.Params.msg return \ No newline at end of file diff --git a/com/http.lua b/com/http.lua deleted file mode 100644 index 9054695..0000000 --- a/com/http.lua +++ /dev/null @@ -1,15 +0,0 @@ -package.path = package.path .. ";/usr/lib64/lua/5.1/?.lua;/usr/local/share/lua/5.1/?.lua;" .. ";./com/?.lua;" -package.cpath = package.cpath .. ";/usr/lib64/lua/5.1/?.so;/usr/local/lib/lua/5.1/?.so;" - -local https = require("ssl.https") -local ltn12 = require("ltn12") - -local response = {} -local res, code, headers = https.request{ - url = "https://localhost:8080/api/v1/echo?msg=sigma", - sink = ltn12.sink.table(response) -} - - -Result.msg = table.concat(response) -Result.status = "ok" diff --git a/config.yaml b/config.yaml index e40cdce..66e2a7e 100644 --- a/config.yaml +++ b/config.yaml @@ -1,7 +1,7 @@ mode: "dev" http_server: - address: "localhost:8080" + address: "0.0.0.0:8080" timeout: 3s idle_timeout: 30s api: @@ -11,8 +11,22 @@ http_server: - s2 tls: - enabled: "true" - cert_file: "./cert/server.crt" - key_file: "./cert/server.key" + enabled: true + cert_file: "./cert/fullchain.pem" + key_file: "./cert/privkey.pem" -com_dir: "com/" \ No newline at end of file +internal: + meta-dir: "./.meta/" + +com_dir: "com/" + + +updates: + enabled: true + allow-auto-updates: true + allow-updates: true + allow-downgrades: false + + check-interval: 1h + repository_url: "https://repo.serve.lv/raw/go-sally" + wanted-version: "latest-stable" \ No newline at end of file diff --git a/core/config/config.go b/core/config/config.go index 020c888..b3d9e10 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -13,10 +13,26 @@ type ConfigConf struct { ComDir string `yaml:"com_dir" env-default:"./com/"` HTTPServer `yaml:"http_server"` TLS `yaml:"tls"` + Internal `yaml:"internal"` + Updates `yaml:"updates"` +} + +type Updates struct { + UpdatesEnabled bool `yaml:"enabled" env-default:"false"` + AllowAutoUpdates bool `yaml:"allow_auto_updates" env-default:"false"` + AllowUpdates bool `yaml:"allow_updates" env-default:"false"` + AllowDowngrades bool `yaml:"allow_downgrades" env-default:"false"` + CheckInterval time.Duration `yaml:"check_interval" env-default:"2h"` + RepositoryURL string `yaml:"repository_url" env-default:""` + WantedVersion string `yaml:"wanted_version" env-default:"latest-stable"` +} + +type Internal struct { + MetaDir string `yaml:"meta_dir" env-default:"./.meta/"` } type TLS struct { - TlsEnabled string `yaml:"enabled" env-default:"false"` + TlsEnabled bool `yaml:"enabled" env-default:"false"` CertFile string `yaml:"cert_file" env-default:"./cert/server.crt"` KeyFile string `yaml:"key_file" env-default:"./cert/server.key"` } diff --git a/core/config/consts.go b/core/config/consts.go new file mode 100644 index 0000000..b0f72a4 --- /dev/null +++ b/core/config/consts.go @@ -0,0 +1,29 @@ +package config + +var UUIDLength int = 4 + +var ApiRoute string = "/api/{ver}" +var ComDirRoute string = "/com" + +var NodeVersion string +var ActualFileNanme string = "actual.txt" + +type _internalConsts struct{} +type _serverConsts struct{} +type _updateConsts struct{} + +func GetUpdateConsts() _updateConsts { return _updateConsts{} } +func (_ _updateConsts) GetNodeVersion() string { + if NodeVersion == "" { + return "version0.0.0-none" + } + return NodeVersion +} +func (_ _updateConsts) GetActualFileName() string { return ActualFileNanme } + +func GetInternalConsts() _internalConsts { return _internalConsts{} } +func (_ _internalConsts) GetUUIDLength() int { return UUIDLength } + +func GetServerConsts() _serverConsts { return _serverConsts{} } +func (_ _serverConsts) GetApiRoute() string { return ApiRoute } +func (_ _serverConsts) GetComDirRoute() string { return ComDirRoute } diff --git a/core/general_server/hanle_multi.go b/core/general_server/hanle_multi.go index 288833c..aac1417 100644 --- a/core/general_server/hanle_multi.go +++ b/core/general_server/hanle_multi.go @@ -160,5 +160,10 @@ func (s *GeneralServer) writeJSONError(status int, msg string) { "error": msg, "code": status, } - json.NewEncoder(s.w).Encode(resp) + if err := json.NewEncoder(s.w).Encode(resp); err != nil { + s.log.Error("Failed to write JSON error response", + slog.String("error", err.Error()), + slog.Int("status", status)) + return + } } diff --git a/core/logs/mock.go b/core/logs/mock.go new file mode 100644 index 0000000..f0cd8a2 --- /dev/null +++ b/core/logs/mock.go @@ -0,0 +1,35 @@ +package logs + +import ( + "context" + "log/slog" + "sync" +) + +type MockHandler struct { + mu sync.Mutex + Logs []slog.Record +} + +func NewMockHandler() *MockHandler { + return &MockHandler{} +} + +func (h *MockHandler) Enabled(_ context.Context, _ slog.Level) bool { + return true +} + +func (h *MockHandler) Handle(_ context.Context, r slog.Record) error { + h.mu.Lock() + defer h.mu.Unlock() + h.Logs = append(h.Logs, r.Clone()) + return nil +} + +func (h *MockHandler) WithAttrs(_ []slog.Attr) slog.Handler { + return h +} + +func (h *MockHandler) WithGroup(_ string) slog.Handler { + return h +} diff --git a/core/sv1/handle_com.go b/core/sv1/handle_com.go index e8a9bd6..3b8a4dc 100644 --- a/core/sv1/handle_com.go +++ b/core/sv1/handle_com.go @@ -7,9 +7,8 @@ import ( "os" "path/filepath" - lua "github.com/aarzilli/golua/lua" - "github.com/go-chi/chi/v5" + lua "github.com/yuin/gopher-lua" ) func (h *HandlerV1) _handle() { @@ -28,8 +27,7 @@ func (h *HandlerV1) _handle() { log.Info("Received request") cmd := chi.URLParam(h.r, "cmd") - var scriptPath string - if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) { + if !h.allowedCmd.MatchString(string([]rune(cmd)[0])) || !h.listAllowedCmd.MatchString(cmd) { log.Error("HTTP request error", slog.String("error", "invalid command"), slog.String("cmd", cmd), @@ -37,15 +35,9 @@ func (h *HandlerV1) _handle() { h.writeJSONError(http.StatusBadRequest, "invalid command") return } - if !h.listAllowedCmd.MatchString(cmd) { - log.Error("HTTP request error", - slog.String("error", "invalid command"), - slog.String("cmd", cmd), - slog.Int("status", http.StatusBadRequest)) - h.writeJSONError(http.StatusBadRequest, "invalid command") - return - } - if scriptPath = h.comMatch(chi.URLParam(h.r, "ver"), cmd); scriptPath == "" { + + scriptPath := h.comMatch(chi.URLParam(h.r, "ver"), cmd) + if scriptPath == "" { log.Error("HTTP request error", slog.String("error", "command not found"), slog.String("cmd", cmd), @@ -66,29 +58,27 @@ func (h *HandlerV1) _handle() { L := lua.NewState() defer L.Close() - L.OpenLibs() // Создаем таблицу Params - L.NewTable() - paramsTableIndex := L.GetTop() // Индекс таблицы в стеке - - // Заполняем таблицу из query параметров + // Создаем таблицу In с Params + paramsTable := L.NewTable() qt := h.r.URL.Query() for k, v := range qt { if len(v) > 0 { - L.PushString(v[0]) // Значение - L.SetField(paramsTableIndex, k) // paramsTable[k] = v[0] + L.SetField(paramsTable, k, lua.LString(v[0])) } } + inTable := L.NewTable() + L.SetField(inTable, "Params", paramsTable) + L.SetGlobal("In", inTable) - // Помещаем Params в глобальные переменные - L.SetGlobal("Params") + // Создаем таблицу Out с Result + resultTable := L.NewTable() + outTable := L.NewTable() + L.SetField(outTable, "Result", resultTable) + L.SetGlobal("Out", outTable) - // Создаем пустую таблицу Result - L.NewTable() - L.SetGlobal("Result") - - // Загружаем и выполняем скрипт подготовки окружения, если есть + // Скрипт подготовки окружения prepareLuaEnv := filepath.Join(h.cfg.ComDir, "_prepare.lua") if _, err := os.Stat(prepareLuaEnv); err == nil { if err := L.DoFile(prepareLuaEnv); err != nil { @@ -98,77 +88,85 @@ func (h *HandlerV1) _handle() { return } } else { - log.Error("No environment preparation script found, skipping preparation", - slog.String("error", err.Error())) + log.Warn("No environment preparation script found, skipping preparation") } - // Выполняем основной Lua скрипт + // Основной Lua скрипт if err := L.DoFile(scriptPath); err != nil { log.Error("Failed to execute lua script", - slog.Group("lua-status", - slog.String("error", err.Error()), - slog.String("lua-version", lua.LUA_VERSION))) + slog.String("error", err.Error())) h.writeJSONError(http.StatusInternalServerError, "lua error: "+err.Error()) return } - // Получаем глобальную переменную Result (таблица) - L.GetGlobal("Result") - if L.IsTable(-1) { - out := make(map[string]interface{}) + // Получаем Out + lv := L.GetGlobal("Out") + tbl, ok := lv.(*lua.LTable) + if !ok { + log.Error("Lua global 'Out' is not a table") + h.writeJSONError(http.StatusInternalServerError, "'Out' is not a table") + return + } - L.PushNil() // Первый ключ - for { - if L.Next(-2) == 0 { - break - } - // На стеке: -1 = value, -2 = key - key := L.ToString(-2) - var val interface{} - - switch L.Type(-1) { - case lua.LUA_TSTRING: - val = L.ToString(-1) - case lua.LUA_TNUMBER: - val = L.ToNumber(-1) - case lua.LUA_TBOOLEAN: - val = L.ToBoolean(-1) - default: - // fallback - val = L.ToString(-1) - } - out[key] = val - L.Pop(1) // Удаляем value, key остаётся для следующего L.Next - } - L.Pop(1) // Удаляем таблицу Result со стека - - h.w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(h.w).Encode(out); err != nil { - log.Error("Failed to encode JSON response", - slog.String("error", err.Error())) - } - - switch out["status"] { - case "error": - log.Info("Command executed with error", - slog.String("cmd", cmd), - slog.Any("result", out)) - case "ok": - log.Info("Command executed successfully", - slog.String("cmd", cmd), slog.Any("result", out)) - default: - log.Info("Command executed and returned an unknown status", - slog.String("cmd", cmd), - slog.Any("result", out)) - } - } else { - L.Pop(1) // убираем не таблицу из стека + // Получаем Result из Out + resultVal := tbl.RawGetString("Result") + resultTbl, ok := resultVal.(*lua.LTable) + if !ok { log.Error("Lua global 'Result' is not a table") h.writeJSONError(http.StatusInternalServerError, "'Result' is not a table") return } - log.Info("Session completed", - slog.Group("lua-status", - slog.String("lua-version", lua.LUA_VERSION))) + // Перебираем таблицу Result + out := make(map[string]interface{}) + resultTbl.ForEach(func(key lua.LValue, value lua.LValue) { + out[key.String()] = convertTypes(value) + }) + + h.w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(h.w).Encode(out); err != nil { + log.Error("Failed to encode JSON response", + slog.String("error", err.Error())) + } + + status, _ := out["status"].(string) + switch status { + case "error": + log.Info("Command executed with error", + slog.String("cmd", cmd), + slog.Any("result", out)) + case "ok": + log.Info("Command executed successfully", + slog.String("cmd", cmd), + slog.Any("result", out)) + default: + log.Info("Command executed and returned an unknown status", + slog.String("cmd", cmd), + slog.Any("result", out)) + } + + log.Info("Session completed") +} + +func convertTypes(value lua.LValue) any { + switch value.Type() { + case lua.LTString: + return value.String() + case lua.LTNumber: + return float64(value.(lua.LNumber)) + case lua.LTBool: + return bool(value.(lua.LBool)) + case lua.LTTable: + result := make(map[string]interface{}) + if tbl, ok := value.(*lua.LTable); ok { + tbl.ForEach(func(key lua.LValue, value lua.LValue) { + result[key.String()] = convertTypes(value) + }) + } + return result + case lua.LTNil: + return nil + default: + return value.String() + } } diff --git a/core/sv1/handle_list.go b/core/sv1/handle_list.go index 67f3c2d..1008e35 100644 --- a/core/sv1/handle_list.go +++ b/core/sv1/handle_list.go @@ -104,5 +104,8 @@ func (h *HandlerV1) _handleList() { log.Info("Session completed") h.w.Header().Set("Content-Type", "application/json") - json.NewEncoder(h.w).Encode(commands) + if err := json.NewEncoder(h.w).Encode(commands); err != nil { + h.log.Error("Failed to write JSON error response", + slog.String("error", err.Error())) + } } diff --git a/core/sv1/utils.go b/core/sv1/utils.go index ba4d67a..043732c 100644 --- a/core/sv1/utils.go +++ b/core/sv1/utils.go @@ -8,6 +8,8 @@ import ( "net/http" "os" "regexp" + + "github.com/akyaiy/GoSally-mvp/core/config" ) func (h *HandlerV1) ErrNotFound(w http.ResponseWriter, r *http.Request) { @@ -17,7 +19,7 @@ func (h *HandlerV1) ErrNotFound(w http.ResponseWriter, r *http.Request) { } func (h *HandlerV1) newUUID() string { - bytes := make([]byte, 16) + bytes := make([]byte, int(config.GetInternalConsts().GetUUIDLength()/2)) _, err := rand.Read(bytes) if err != nil { h.log.Error("Failed to generate UUID", slog.String("error", err.Error())) @@ -43,7 +45,12 @@ func (h *HandlerV1) writeJSONError(status int, msg string) { "error": msg, "code": status, } - json.NewEncoder(h.w).Encode(resp) + if err := json.NewEncoder(h.w).Encode(resp); err != nil { + h.log.Error("Failed to write JSON error response", + slog.String("error", err.Error()), + slog.Int("status", status)) + return + } } func (h *HandlerV1) extractDescriptionStatic(path string) (string, error) { diff --git a/core/update/update.go b/core/update/update.go new file mode 100644 index 0000000..aa82d6b --- /dev/null +++ b/core/update/update.go @@ -0,0 +1,216 @@ +package update + +import ( + "errors" + "io" + "log/slog" + "net/http" + "strconv" + "strings" + + "github.com/akyaiy/GoSally-mvp/core/config" +) + +const ( + UpdateBranchStable = "stable" + UpdateBranchDev = "dev" + UpdateBranchTesting = "testing" +) + +type Version string +type Branch string +type IsNewUpdate bool + +type UpdaterContract interface { + CkeckUpdates() (IsNewUpdate, error) + Update() error + GetCurrentVersion() (Version, Branch, error) + GetLatestVersion(updateBranch Branch) (Version, Branch, error) +} + +type Updater struct { + Log slog.Logger + Config *config.ConfigConf +} + +func NewUpdater(log slog.Logger, cfg *config.ConfigConf) *Updater { + return &Updater{ + Log: log, + Config: cfg, + } +} + +func splitVersionString(versionStr string) (Version, Branch, error) { + versionStr = strings.TrimSpace(versionStr) + if !strings.HasPrefix(versionStr, "version") { + return "", "unknown", errors.New("version string does not start with 'version'") + } + parts := strings.SplitN(versionStr[len("version"):], "-", 2) + parts[0] = strings.TrimPrefix(parts[0], "version") + if len(parts) != 2 { + return Version(parts[0]), Branch("unknown"), errors.New("version string format invalid") + } + return Version(parts[0]), Branch(parts[1]), nil +} + +// isVersionNewer compares two version strings and returns true if the current version is newer than the latest version. +func isVersionNewer(current, latest Version) bool { + if current == latest { + return false + } + + currentParts := strings.Split(string(current), ".") + latestParts := strings.Split(string(latest), ".") + + maxLen := len(currentParts) + if len(latestParts) > maxLen { + maxLen = len(latestParts) + } + + for i := 0; i < maxLen; i++ { + var curPart, latPart int + + if i < len(currentParts) { + cur, err := strconv.Atoi(currentParts[i]) + if err != nil { + cur = 0 // или можно обработать ошибку иначе + } + curPart = cur + } else { + curPart = 0 // Если части в current меньше, считаем недостающие нулями + } + + if i < len(latestParts) { + lat, err := strconv.Atoi(latestParts[i]) + if err != nil { + lat = 0 + } + latPart = lat + } else { + latPart = 0 + } + + if curPart < latPart { + return true + } + if curPart > latPart { + return false + } + // если равны — идём дальше + } + return false // все части равны, значит не новее +} + +// if len(currentParts) >= 1 && len(latestParts) >= 1 { +// if currentParts[0] < latestParts[0] { +// if len(currentParts) < 2 || len(latestParts) < 2 { +// if currentParts[1] < latestParts[1] { +// return true +// } +// if currentParts[1] > latestParts[1] { +// return false +// } +// } +// if currentParts[0] > latestParts[0] { +// return false +// } +// } + +// GetCurrentVersion reads the current version from the version file and returns it along with the branch. +func (u *Updater) GetCurrentVersion() (Version, Branch, error) { + version, branch, err := splitVersionString(string(config.GetUpdateConsts().GetNodeVersion())) + if err != nil { + u.Log.Error("Failed to parse version string", slog.String("version", string(config.GetUpdateConsts().GetNodeVersion())), slog.String("error", err.Error())) + return "", "", err + } + switch branch { + case UpdateBranchDev, UpdateBranchStable, UpdateBranchTesting: + return Version(version), Branch(branch), nil + default: + return Version(version), Branch("unknown"), nil + } +} + +func (u *Updater) GetLatestVersion(updateBranch Branch) (Version, Branch, error) { + repoURL := u.Config.Updates.RepositoryURL + if repoURL == "" { + u.Log.Error("RepositoryURL is empty in config") + return "", "", errors.New("repository URL is empty") + } + if !strings.HasPrefix(repoURL, "http://") && !strings.HasPrefix(repoURL, "https://") { + u.Log.Error("RepositoryURL does not start with http:// or https://", slog.String("RepositoryURL", repoURL)) + return "", "", errors.New("repository URL must start with http:// or https://") + } + response, err := http.Get(repoURL + "/" + config.GetUpdateConsts().GetActualFileName()) + if err != nil { + u.Log.Error("Failed to fetch latest version", slog.String("error", err.Error())) + return "", "", err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + u.Log.Error("Failed to fetch latest version", slog.Int("status", response.StatusCode)) + return "", "", errors.New("failed to fetch latest version, status code: " + http.StatusText(response.StatusCode)) + } + data, err := io.ReadAll(response.Body) + if err != nil { + u.Log.Error("Failed to read latest version response", slog.String("error", err.Error())) + return "", "", err + } + lines := strings.Split(string(data), "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + version, branch, err := splitVersionString(string(line)) + if err != nil { + u.Log.Error("Failed to parse version string", slog.String("version", string(line)), slog.String("error", err.Error())) + return "", "", err + } + if branch == updateBranch { + return Version(version), Branch(branch), nil + } + } + u.Log.Warn("No version found for branch", slog.String("branch", string(updateBranch))) + return "", "", errors.New("no version found for branch: " + string(updateBranch)) +} + +func (u *Updater) CkeckUpdates() (IsNewUpdate, error) { + currentVersion, currentBranch, err := u.GetCurrentVersion() + if err != nil { + return false, err + } + latestVersion, latestBranch, err := u.GetLatestVersion(currentBranch) + if err != nil { + return false, err + } + if currentVersion == latestVersion && currentBranch == latestBranch { + return false, nil + } + u.Log.Info("New update available", + slog.String("current_version", string(currentVersion)), + slog.String("current_branch", string(currentBranch)), + slog.String("latest_version", string(latestVersion)), + slog.String("latest_branch", string(latestBranch)), + ) + return true, nil +} + +// func (u *Updater) Update() error { +// if !(u.Config.UpdatesEnabled && u.Config.Updates.AllowUpdates && u.Config.Updates.AllowDowngrades) { +// u.Log.Info("Updates are disabled in config, skipping update") +// return nil +// } +// wantedVersion := u.Config.Updates.WantedVersion +// _, wantedBranch, _ := splitVersionString(wantedVersion) +// newVersion, newBranch, err := u.GetLatestVersion(wantedBranch) +// if err != nil { +// return err +// } +// if wantedBranch != newBranch { +// u.Log.Info("Wanted version branch does not match latest version branch: updating wanted branch", +// slog.String("wanted_branch", string(wantedBranch)), +// slog.String("latest_branch", string(newBranch)), +// ) +// } +// } diff --git a/core/update/update_test.go b/core/update/update_test.go new file mode 100644 index 0000000..195b03a --- /dev/null +++ b/core/update/update_test.go @@ -0,0 +1,30 @@ +package update + +import ( + "testing" +) + +func TestFunc_isVersionNewer(t *testing.T) { + tests := []struct { + current string + latest string + want bool + }{ + {"1.0.0", "1.0.0", false}, + {"1.0.0", "1.0.1", true}, + {"1.0.1", "1.0.0", false}, + {"2.0.0", "1.9.9", false}, + {"2.2.3", "1.9.9", false}, + {"22.2.3", "1.9.9", false}, + {"1.2.3", "1.99.9", true}, + {"1.10", "1.5.99999", false}, + } + + for _, tt := range tests { + t.Run(tt.current+" vs "+tt.latest, func(t *testing.T) { + if got := isVersionNewer(Version(tt.current), Version(tt.latest)); got != tt.want { + t.Errorf("isVersionNewer(%q, %q) = %v; want %v", tt.current, tt.latest, got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod index 8cfedc4..8141e05 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,11 @@ require ( github.com/go-chi/chi/v5 v5.2.2 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/yuin/gopher-lua v1.1.1 + golang.org/x/net v0.41.0 ) require ( github.com/BurntSushi/toml v1.5.0 // indirect - github.com/aarzilli/golua v0.0.0-20250217091409-248753f411c4 // indirect github.com/joho/godotenv v1.5.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect diff --git a/go.sum b/go.sum index 84c7419..1e409b1 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,6 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/aarzilli/golua v0.0.0-20250217091409-248753f411c4 h1:gW5i3FQAMcbkNgo/A87gCKAbBMalAO8BlPIMo9Gk2Ow= -github.com/aarzilli/golua v0.0.0-20250217091409-248753f411c4/go.mod h1:hMjfaJVSqVnxenMlsxrq3Ni+vrm9Hs64tU4M7dhUoO4= -github.com/akyaiy/GoSally-mvp/config v0.0.0-20250622141207-5326dd45b694 h1:SJfxaud4HMVg9roTMMJaTQ+Odoz1LIw60TiS97EtWCE= -github.com/akyaiy/GoSally-mvp/config v0.0.0-20250622141207-5326dd45b694/go.mod h1:2eaoBiPQmvZoC9DAzn11zHzWmscBI4dMTi4HeGO96XQ= -github.com/akyaiy/GoSally-mvp/logs v0.0.0-20250622141207-5326dd45b694 h1:qYZIzX3NczqozwCnlLQ5M1vTLoCqIIF1qxxweub4zQo= -github.com/akyaiy/GoSally-mvp/logs v0.0.0-20250622141207-5326dd45b694/go.mod h1:o5ysbqTH4qQTlpx8cu2XaXbTI2blpDgaUI4CEvi0VGo= github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= @@ -15,6 +9,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=