Project structure refactor:

- Change package name general_server to gateway
- Changing the structure of directories and packages
- Adding vendor to the project
This commit is contained in:
2025-07-28 20:16:40 +03:00
parent 19b699d92b
commit ec94df5f4a
786 changed files with 357010 additions and 357 deletions

View File

@@ -0,0 +1,22 @@
package corestate
type Stage string
const (
StageNotReady Stage = "init"
StagePreInit Stage = "pre-init"
StagePostInit Stage = "post-init"
StageReady Stage = "event"
)
const (
StringsNone string = "none"
)
func NewCorestate(o *CoreState) *CoreState {
// TODO: create a convenient interface for creating a state
// if !utils.IsFullyInitialized(o) {
// return nil, fmt.Errorf("CoreState is not fully initialized")
// }
return o
}

View File

@@ -0,0 +1,80 @@
package corestate
import (
"encoding/hex"
"errors"
"os"
"path/filepath"
"strings"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
)
// GetNodeUUID outputs the correct uuid from the file at the path specified in the arguments.
// If the uuid is not correct or is not exist, an empty string and an error will be returned.
// The path to the identifier must contain the path to the "uuid" directory,
// not the file with the identifier itself, for example: "uuid/data"
func GetNodeUUID(metaInfPath string) (string, error) {
uuid, err := readNodeUUIDRaw(filepath.Join(metaInfPath, "data"))
if err != nil {
return "", err
}
return hex.EncodeToString(uuid[:]), nil
}
func readNodeUUIDRaw(p string) ([]byte, error) {
data, err := os.ReadFile(p)
if err != nil {
return data, err
}
if len(data) != config.UUIDLength {
return data, errors.New("decoded UUID length mismatch")
}
return data, nil
}
// SetNodeUUID sets the identifier to the given path.
// The function replaces the identifier's associated directory with all its contents.
func SetNodeUUID(metaInfPath string) error {
if !strings.HasSuffix(metaInfPath, "uuid") {
return errors.New("invalid meta/uuid path")
}
info, err := os.Stat(metaInfPath)
if err == nil && info.IsDir() {
err = os.RemoveAll(metaInfPath)
if err != nil {
return err
}
} else if err != nil && !os.IsNotExist(err) {
return err
}
err = os.MkdirAll(metaInfPath, 0755)
if err != nil {
return err
}
dataPath := filepath.Join(metaInfPath, "data")
uuidStr, err := utils.NewUUID32Raw()
if err != nil {
return err
}
err = os.WriteFile(dataPath, uuidStr[:], 0644)
if err != nil {
return err
}
readmePath := filepath.Join(metaInfPath, "README.txt")
readmeContent := ` - - - - ! STRICTLY FORBIDDEN TO MODIFY THIS DIRECTORY ! - - - -
This directory contains the unique node identifier stored in the file named data.
This identifier is critical for correct node recognition both locally and across the network.
Any modification, deletion, or tampering with this directory may lead to permanent loss of identity, data corruption, or network conflicts.
Proceed at your own risk. You have been warned.`
err = os.WriteFile(readmePath, []byte(readmeContent), 0644)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,24 @@
package corestate
// CoreStateContract is interface for CoreState.
// CoreState is a structure that contains the basic meta-information vital to the node.
// The interface contains functionality for working with the Runtime directory and its files,
// and access to low-level logging in stdout
type CoreStateContract interface {
}
type CoreState struct {
UUID32 string
UUID32DirName string
StartTimestampUnix int64
NodeBinName string
NodeVersion string
Stage Stage
NodePath string
MetaDir string
RunDir string
}

View File

@@ -0,0 +1,95 @@
package run_manager
import (
"context"
"fmt"
"os"
"syscall"
"time"
)
func File(index string) RunFileManagerContract {
value, ok := indexedPaths[index]
if !ok {
err := indexPaths()
if err != nil {
return &RunFileManager{
err: err,
}
}
value, ok = indexedPaths[index]
if !ok {
return &RunFileManager{
err: fmt.Errorf("cannot detect file under index %s", index),
}
}
}
return &RunFileManager{
indexedPath: value,
}
}
func (r *RunFileManager) Open() (*os.File, error) {
if r.err != nil {
return nil, r.err
}
file, err := os.OpenFile(r.indexedPath, os.O_RDWR, 0)
if err != nil {
return nil, err
}
r.file = file
return file, nil
}
func (r *RunFileManager) Close() error {
return r.file.Close()
}
func (r *RunFileManager) Watch(parentCtx context.Context, callback func()) (context.CancelFunc, error) {
if r.err != nil {
return nil, r.err
}
if r.file == nil {
return nil, fmt.Errorf("file is not opened")
}
info, err := r.file.Stat()
if err != nil {
return nil, err
}
origStat := info.Sys().(*syscall.Stat_t)
origIno := origStat.Ino
origModTime := info.ModTime()
ctx, cancel := context.WithCancel(parentCtx)
go func() {
for {
select {
case <-ctx.Done():
return
default:
newInfo, err := os.Stat(r.indexedPath)
if err != nil {
if os.IsNotExist(err) {
callback()
return
}
} else {
newStat := newInfo.Sys().(*syscall.Stat_t)
if newStat.Ino != origIno {
callback()
return
}
if !newInfo.ModTime().Equal(origModTime) {
callback()
return
}
}
time.Sleep(1 * time.Second)
}
}
}()
return cancel, nil
}

View File

@@ -0,0 +1,158 @@
package run_manager
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
)
type RunManagerContract interface {
Get(index string) (string, error)
// Set recursively creates a file in runDir
Set(index string) error
File(index string) RunFileManagerContract
indexPaths() error
}
var (
created bool
runDir string
indexedPaths = make(map[string]string)
)
type RunFileManagerContract interface {
Open() (*os.File, error)
Close() error
Watch(parentCtx context.Context, callback func()) (context.CancelFunc, error)
}
type RunFileManager struct {
err error
indexedPath string
file *os.File
}
// func (c *CoreState) RuntimeDir() RunManagerContract {
// return c.RM
// }
// Create creates a temp directory
func Create(uuid32 string) (string, error) {
if created {
return runDir, fmt.Errorf("runtime directory is already created")
}
path, err := os.MkdirTemp("", fmt.Sprintf("*-%s-%s", uuid32, "gosally-runtime"))
if err != nil {
return "", err
}
runDir = path
created = true
return path, nil
}
func Clean() error {
created = false
indexedPaths = nil
return utils.CleanTempRuntimes(runDir)
}
// Quite dangerous and goofy.
// TODO: implement a better variant of runDir indexing on the second stage of initialization
func Toggle() string {
runDir = filepath.Dir(os.Args[0])
created = true
return runDir
}
func Get(index string) (string, error) {
if !created {
return "", fmt.Errorf("runtime directory is not created")
}
if indexedPaths == nil {
err := indexPaths()
if err != nil {
return "", nil
}
}
if indexedPaths == nil {
return "", fmt.Errorf("indexedPaths is nil")
}
value, ok := indexedPaths[index]
if !ok {
err := indexPaths()
if err != nil {
return "", err
}
value, ok = indexedPaths[index]
if !ok {
return "", fmt.Errorf("cannot detect file under index %s", index)
}
}
return value, nil
}
func Set(index string) error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
fullPath := filepath.Join(runDir, index)
dir := filepath.Dir(fullPath)
err := os.MkdirAll(dir, 0755)
if err != nil {
return err
}
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if indexedPaths == nil {
err = indexPaths()
if err != nil {
return err
}
} else {
indexedPaths[index] = fullPath
}
return nil
}
func SetDir(index string) error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
fullPath := filepath.Join(runDir, index)
err := os.MkdirAll(fullPath, 0755)
if err != nil {
return err
}
return nil
}
func indexPaths() error {
if !created {
return fmt.Errorf("runtime directory is not created")
}
i, err := utils.IndexPaths(runDir)
if err != nil {
return err
}
indexedPaths = i
return nil
}
func RuntimeDir() string {
return runDir
}

View File

@@ -0,0 +1,335 @@
package update
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/akyaiy/GoSally-mvp/internal/core/run_manager"
"github.com/akyaiy/GoSally-mvp/internal/core/utils"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
"golang.org/x/net/context"
)
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 *log.Logger
config *config.Conf
env *config.Env
ctx context.Context
cancel context.CancelFunc
}
func NewUpdater(ctx context.Context, log *log.Logger, cfg *config.Conf, env *config.Env) *Updater {
return &Updater{
log: log,
config: cfg,
env: env,
ctx: ctx,
}
}
func splitVersionString(versionStr string) (Version, Branch, error) {
versionStr = strings.TrimSpace(versionStr)
if !strings.HasPrefix(versionStr, "v") {
return "", "unknown", errors.New("version string does not start with 'v'")
}
parts := strings.SplitN(versionStr[len("v"):], "-", 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
}
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
}
// 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.NodeVersion))
if err != nil {
u.log.Printf("Failed to parse version string: %s", 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.Printf("Failed to get latest version: %s", "RepositoryURL is empty in config")
return "", "", errors.New("repository URL is empty")
}
if !strings.HasPrefix(repoURL, "http://") && !strings.HasPrefix(repoURL, "https://") {
u.log.Printf("Failed to get latest version: %s: %s", "RepositoryURL does not start with http:// or https:/", repoURL)
return "", "", errors.New("repository URL must start with http:// or https://")
}
response, err := http.Get(repoURL + "/" + config.ActualFileName)
if err != nil {
u.log.Printf("Failed to fetch latest version: %s", err.Error())
return "", "", err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
u.log.Printf("Failed to fetch latest version: HTTP status %d", 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.Printf("Failed to read latest version response: %s", 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.Printf("Failed to parse version string: %s", err.Error())
return "", "", err
}
if branch == updateBranch {
return Version(version), Branch(branch), nil
}
}
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
}
return true, nil
}
func (u *Updater) Update() error {
if !u.config.Updates.UpdatesEnabled {
return errors.New("updates are disabled in config, skipping update")
}
if err := run_manager.SetDir("update"); err != nil {
return fmt.Errorf("failed to create update dir: %w", err)
}
downloadPath := filepath.Join(run_manager.RuntimeDir(), "update")
_, currentBranch, err := u.GetCurrentVersion()
if err != nil {
return fmt.Errorf("failed to get current version: %w", err)
}
latestVersion, latestBranch, err := u.GetLatestVersion(currentBranch)
if err != nil {
return fmt.Errorf("failed to get latest version: %w", err)
}
updateArchiveName := fmt.Sprintf("%s.v%s-%s", config.UpdateArchiveName, latestVersion, latestBranch)
updateDest := fmt.Sprintf("%s/%s.%s", u.config.Updates.RepositoryURL, updateArchiveName, "tar.gz")
resp, err := http.Get(updateDest)
if err != nil {
return fmt.Errorf("failed to fetch archive: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("unexpected HTTP status: %s, body: %s", resp.Status, body)
}
gzReader, err := gzip.NewReader(resp.Body)
if err != nil {
return fmt.Errorf("gzip reader error: %w", err)
}
defer gzReader.Close()
tarReader := tar.NewReader(gzReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("tar read error: %w", err)
}
relativeParts := strings.SplitN(header.Name, string(os.PathSeparator), 2)
if len(relativeParts) < 2 {
// It's either a top level directory or garbage.
continue
}
cleanName := relativeParts[1]
targetPath := filepath.Join(downloadPath, cleanName)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
return fmt.Errorf("mkdir error: %w", err)
}
case tar.TypeReg:
if err := run_manager.Set(filepath.Join("update", cleanName)); err != nil {
return fmt.Errorf("set file error: %w", err)
}
f := run_manager.File(filepath.Join("update", cleanName))
outFile, err := f.Open()
if err != nil {
return fmt.Errorf("open file error: %w", err)
}
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
return fmt.Errorf("copy file error: %w", err)
}
outFile.Close()
default:
return fmt.Errorf("unsupported tar type: %v", header.Typeflag)
}
}
return u.InstallAndRestart()
}
func (u *Updater) InstallAndRestart() error {
nodePath := u.env.NodePath
if nodePath == "" {
return errors.New("GS_NODE_PATH environment variable is not set")
}
installDir := filepath.Join(nodePath, "bin")
targetPath := filepath.Join(installDir, "node")
f := run_manager.File("update/node")
input, err := f.Open()
if err != nil {
return fmt.Errorf("cannot open new binary: %w", err)
}
defer f.Close()
output, err := os.Create(targetPath)
if err != nil {
return fmt.Errorf("cannot create target binary: %w", err)
}
if _, err := io.Copy(output, input); err != nil {
output.Close()
return fmt.Errorf("copy failed: %w", err)
}
output.Close()
if err := os.Chmod(targetPath, 0755); err != nil {
return fmt.Errorf("failed to chmod: %w", err)
}
u.log.Printf("Launching new version: path is %s", targetPath)
// cmd := exec.Command(targetPath, os.Args[1:]...)
// cmd.Env = os.Environ()
// cmd.Stdout = os.Stdout
// cmd.Stderr = os.Stderr
// cmd.Stdin = os.Stdin
args := os.Args
args[0] = targetPath
env := utils.SetEviron(os.Environ(), "GS_PARENT_PID=-1")
if err := run_manager.Clean(); err != nil {
return err
}
return syscall.Exec(targetPath, args, env)
//u.cancel()
// TODO: fix this crap and find a better way to update without errors
// for {
// _, err := run_manager.Get("run.lock")
// if err != nil {
// break
// }
// }
// return cmd.Start()
}
func (u *Updater) Shutdownfunc(f context.CancelFunc) {
u.cancel = f
}

View File

@@ -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)
}
})
}
}

View File

@@ -0,0 +1,22 @@
package utils
import (
"encoding/json"
"net/http"
)
// writeJSONError writes a JSON error response to the HTTP response writer.
// It sets the Content-Type to application/json, writes the specified HTTP status code
func WriteJSONError(w http.ResponseWriter, status int, msg string) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
resp := map[string]any{
"status": "error",
"error": msg,
"code": status,
}
if err := json.NewEncoder(w).Encode(resp); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,78 @@
package utils
import (
"fmt"
lua "github.com/yuin/gopher-lua"
)
func ConvertLuaTypesToGolang(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:
tbl := value.(*lua.LTable)
// Попробуем как массив
var arr []any
isArray := true
tbl.ForEach(func(key, val lua.LValue) {
if key.Type() != lua.LTNumber {
isArray = false
}
arr = append(arr, ConvertLuaTypesToGolang(val))
})
if isArray {
return arr
}
result := make(map[string]any)
tbl.ForEach(func(key, val lua.LValue) {
result[key.String()] = ConvertLuaTypesToGolang(val)
})
return result
case lua.LTNil:
return nil
default:
return value.String()
}
}
func ConvertGolangTypesToLua(L *lua.LState, val any) lua.LValue {
switch v := val.(type) {
case string:
return lua.LString(v)
case bool:
return lua.LBool(v)
case int:
return lua.LNumber(float64(v))
case int64:
return lua.LNumber(float64(v))
case float32:
return lua.LNumber(float64(v))
case float64:
return lua.LNumber(v)
case []any:
tbl := L.NewTable()
for i, item := range v {
tbl.RawSetInt(i+1, ConvertGolangTypesToLua(L, item))
}
return tbl
case map[string]any:
tbl := L.NewTable()
for key, value := range v {
tbl.RawSetString(key, ConvertGolangTypesToLua(L, value))
}
return tbl
case nil:
return lua.LNil
default:
return lua.LString(fmt.Sprintf("%v", v))
}
}

View File

@@ -0,0 +1,122 @@
package utils
import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
)
func SetEviron(eviron []string, envs ...string) []string {
envMap := make(map[string]string)
for _, e := range eviron {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
for _, e := range envs {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
envMap[parts[0]] = parts[1]
}
}
newEviron := make([]string, 0, len(envMap))
for k, v := range envMap {
newEviron = append(newEviron, fmt.Sprintf("%s=%s", k, v))
}
return newEviron
}
func CleanTempRuntimes(pattern string) error {
matches, err := filepath.Glob(pattern)
if err != nil {
return err
}
for _, path := range matches {
info, err := os.Stat(path)
if err != nil {
continue
}
if info.IsDir() {
os.RemoveAll(path)
}
}
return nil
}
func ExistsMatchingDirs(pattern, exclude string) (bool, error) {
matches, err := filepath.Glob(pattern)
if err != nil {
return false, err
}
for _, path := range matches {
if filepath.Clean(path) == filepath.Clean(exclude) {
continue
}
info, err := os.Stat(path)
if err == nil && info.IsDir() {
return true, nil
}
}
return false, nil
}
func IndexPaths(runDir string) (map[string]string, error) {
indexed := make(map[string]string)
err := filepath.Walk(runDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
relPath, err := filepath.Rel(runDir, path)
if err != nil {
return err
}
indexed[relPath] = path
return nil
})
if err != nil {
return nil, err
}
return indexed, nil
}
func IsFullyInitialized(i any) bool {
v := reflect.ValueOf(i).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
switch field.Kind() {
case reflect.Ptr, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
if field.IsNil() {
return false
}
case reflect.String:
if field.String() == "" {
return false
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if field.Int() == 0 {
return false
}
case reflect.Bool:
if !field.Bool() {
return false
}
}
}
return true
}

View File

@@ -0,0 +1,48 @@
package utils
import (
"fmt"
"reflect"
"sort"
"testing"
)
func TestFunc_SetEviron(t *testing.T) {
tests := []struct {
eviron []string
envs []string
want []string
}{
{
[]string{"ENV1=1", "ENV2=2", "ENV3=4"},
[]string{"ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
{
[]string{"ENV1=1", "ENV2=5", "ENV3=4"},
[]string{"ENV2=2", "ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
{
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
[]string{"ENV4=4"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3", "ENV4=4"},
},
{
[]string{"ENV1=1", "ENV2=2", "ENV3=4"},
[]string{"ENV3=2", "ENV3=3"},
[]string{"ENV1=1", "ENV2=2", "ENV3=3"},
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("in %q set new %q", tt.eviron, tt.envs), func(t *testing.T) {
got := SetEviron(tt.eviron, tt.envs...)
sort.Strings(got)
sort.Strings(tt.want)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("SetEviron(%q, %q) = got %v; want %v", tt.eviron, tt.envs, got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,41 @@
package utils
import (
"crypto/rand"
"encoding/hex"
"errors"
"github.com/akyaiy/GoSally-mvp/internal/engine/config"
)
func NewUUIDRaw(length int) ([]byte, error) {
bytes := make([]byte, int(length))
_, err := rand.Read(bytes)
if err != nil {
return bytes, errors.New("failed to generate UUID: " + err.Error())
}
return bytes, nil
}
func NewUUID(length int) (string, error) {
data, err := NewUUIDRaw(length)
if err != nil {
return "", err
}
return hex.EncodeToString(data), nil
}
func NewUUID32() (string, error) {
return NewUUID(config.UUIDLength)
}
func NewUUID32Raw() ([]byte, error) {
data, err := NewUUIDRaw(config.UUIDLength)
if err != nil {
return data, err
}
if len(data) != config.UUIDLength {
return data, errors.New("unexpected UUID length")
}
return data, nil
}