Feat: add models/cache/config
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,2 @@ | |||||||
| .aider* | .aider* | ||||||
|  | golias | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | .PHONY: all init deps install test lint run stop | ||||||
|  |  | ||||||
|  | run: | ||||||
|  | 	go build | ||||||
|  | 	./golias start | ||||||
|  |  | ||||||
|  | init: | ||||||
|  | 	go mod init | ||||||
|  |  | ||||||
|  | # install all dependencies used by the application | ||||||
|  | deps: | ||||||
|  | 	go clean -modcache | ||||||
|  | 	go mod download | ||||||
|  |  | ||||||
|  | # install the application in the Go bin/ folder | ||||||
|  | install: | ||||||
|  | 	go install ./... | ||||||
|  |  | ||||||
|  | test: | ||||||
|  | 	go test ./... | ||||||
|  |  | ||||||
|  | lint: | ||||||
|  | 	golangci-lint run --config .golangci.yml | ||||||
|  |  | ||||||
|  | gen: | ||||||
|  | 	go generate ./... | ||||||
|  |  | ||||||
|  | build-container: | ||||||
|  | 	docker build -t golias:master . | ||||||
|  |  | ||||||
|  | stop-container: | ||||||
|  | 	docker rm -f golias 2>/dev/null && echo "old container removed" | ||||||
|  |  | ||||||
|  | run-container: stop-container | ||||||
|  | 	docker run --name=golias -v $(CURDIR)/store.json:/root/store.json -p 0.0.0.0:9000:9000 -d golias:master | ||||||
| @@ -4,7 +4,7 @@ | |||||||
| 	<meta charset="UTF-8"> | 	<meta charset="UTF-8"> | ||||||
| 	<meta name="viewport" content="width=device-width, initial-scale=1.0"> | 	<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
| 	<title>Word Colors</title> | 	<title>Word Colors</title> | ||||||
| 	<script src="https://unpkg.com/htmx.org@1.9.10"></script> | 	<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
| 	<h1>Word Color Cards</h1> | 	<h1>Word Color Cards</h1> | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								config/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | package config | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"log/slog" | ||||||
|  |  | ||||||
|  | 	"github.com/BurntSushi/toml" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Config struct { | ||||||
|  | 	ServerConfig    ServerConfig `toml:"SERVICE"` | ||||||
|  | 	BaseURL         string       `toml:"BASE_URL"` | ||||||
|  | 	SessionLifetime int          `toml:"SESSION_LIFETIME_SECONDS"` | ||||||
|  | 	DBURI           string       `toml:"DBURI"` | ||||||
|  | 	CookieSecret    string       `toml:"COOKIE_SECRET"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ServerConfig struct { | ||||||
|  | 	Host string `toml:"HOST"` | ||||||
|  | 	Port string `toml:"PORT"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func LoadConfigOrDefault(fn string) *Config { | ||||||
|  | 	if fn == "" { | ||||||
|  | 		fn = "config.toml" | ||||||
|  | 	} | ||||||
|  | 	config := &Config{} | ||||||
|  | 	_, err := toml.DecodeFile(fn, &config) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Warn("failed to read config from file, loading default", "error", err) | ||||||
|  | 		config.BaseURL = "https://localhost:3000" | ||||||
|  | 		config.SessionLifetime = 300 | ||||||
|  | 		config.CookieSecret = "test" | ||||||
|  | 	} | ||||||
|  | 	return config | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								go.mod
									
									
									
									
									
								
							| @@ -1,3 +1,10 @@ | |||||||
| module golias | module golias | ||||||
|  |  | ||||||
| go 1.24 | go 1.24 | ||||||
|  |  | ||||||
|  | require ( | ||||||
|  | 	github.com/BurntSushi/toml v1.5.0 | ||||||
|  | 	honnef.co/go/tools v0.6.1 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | require golang.org/x/tools v0.30.0 // indirect | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= | ||||||
|  | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= | ||||||
|  | golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= | ||||||
|  | golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= | ||||||
|  | honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= | ||||||
|  | honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= | ||||||
							
								
								
									
										115
									
								
								handlers/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								handlers/auth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  | package handlers | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"crypto/hmac" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"golias/models" | ||||||
|  | 	"golias/utils" | ||||||
|  | 	"html/template" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func abortWithError(w http.ResponseWriter, msg string) { | ||||||
|  | 	tmpl := template.Must(template.ParseGlob("components/*.html")) | ||||||
|  | 	tmpl.ExecuteTemplate(w, "error", msg) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func HandleFrontLogin(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	r.ParseForm() | ||||||
|  | 	username := r.PostFormValue("username") | ||||||
|  | 	if username == "" { | ||||||
|  | 		msg := "username not provided" | ||||||
|  | 		slog.Error(msg) | ||||||
|  | 		abortWithError(w, msg) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	// make sure username does not exists | ||||||
|  | 	cleanName := utils.RemoveSpacesFromStr(username) | ||||||
|  | 	// TODO: create user in db | ||||||
|  | 	// login user | ||||||
|  | 	cookie, err := makeCookie(cleanName, r.RemoteAddr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("failed to login", "error", err) | ||||||
|  | 		abortWithError(w, err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	http.SetCookie(w, cookie) | ||||||
|  | 	tmpl, err := template.ParseGlob("components/*.html") | ||||||
|  | 	if err != nil { | ||||||
|  | 		abortWithError(w, err.Error()) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	tmpl.ExecuteTemplate(w, "main", nil) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func makeCookie(username string, remote string) (*http.Cookie, error) { | ||||||
|  | 	// secret | ||||||
|  | 	// Create a new random session token | ||||||
|  | 	// sessionToken := xid.New().String() | ||||||
|  | 	sessionToken := "token" | ||||||
|  | 	expiresAt := time.Now().Add(time.Duration(cfg.SessionLifetime) * time.Second) | ||||||
|  | 	// Set the token in the session map, along with the session information | ||||||
|  | 	session := &models.Session{ | ||||||
|  | 		Username: username, | ||||||
|  | 		Expiry:   expiresAt, | ||||||
|  | 	} | ||||||
|  | 	cookieName := "session_token" | ||||||
|  | 	// hmac to protect cookies | ||||||
|  | 	hm := hmac.New(sha256.New, []byte(cfg.CookieSecret)) | ||||||
|  | 	hm.Write([]byte(cookieName)) | ||||||
|  | 	hm.Write([]byte(sessionToken)) | ||||||
|  | 	signature := hm.Sum(nil) | ||||||
|  | 	// b64 enc to avoid non-ascii | ||||||
|  | 	cookieValue := base64.URLEncoding.EncodeToString([]byte( | ||||||
|  | 		string(signature) + sessionToken)) | ||||||
|  | 	cookie := &http.Cookie{ | ||||||
|  | 		Name:     cookieName, | ||||||
|  | 		Value:    cookieValue, | ||||||
|  | 		Secure:   true, | ||||||
|  | 		HttpOnly: true, | ||||||
|  | 		SameSite: http.SameSiteNoneMode, | ||||||
|  | 		Domain:   cfg.ServerConfig.Host, | ||||||
|  | 	} | ||||||
|  | 	slog.Info("check remote addr for cookie set", | ||||||
|  | 		"remote", remote, "session", session) | ||||||
|  | 	if strings.Contains(remote, "192.168.0") { | ||||||
|  | 		// no idea what is going on | ||||||
|  | 		// cookie.Domain = "192.168.0.15" | ||||||
|  | 		cookie.Domain = "home.host" | ||||||
|  | 		slog.Info("changing cookie domain", "domain", cookie.Domain) | ||||||
|  | 	} | ||||||
|  | 	// set ctx? | ||||||
|  | 	// set user in session | ||||||
|  | 	if err := cacheSetSession(sessionToken, session); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return cookie, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func cacheGetSession(key string) (*models.Session, error) { | ||||||
|  | 	userSessionB, err := memcache.Get(key) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	var us *models.Session | ||||||
|  | 	if err := json.Unmarshal(userSessionB, &us); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return us, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func cacheSetSession(key string, session *models.Session) error { | ||||||
|  | 	sesb, err := json.Marshal(session) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	memcache.Set(key, sesb) | ||||||
|  | 	// expire in 10 min | ||||||
|  | 	memcache.Expire(key, 10*60) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										83
									
								
								handlers/middleware.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								handlers/middleware.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | package handlers | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/hmac" | ||||||
|  | 	"crypto/sha256" | ||||||
|  | 	"encoding/base64" | ||||||
|  | 	"errors" | ||||||
|  | 	"golias/config" | ||||||
|  | 	"golias/pkg/cache" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"net/http" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	cfg      config.Config | ||||||
|  | 	memcache cache.Cache | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func GetSession(next http.Handler) http.Handler { | ||||||
|  | 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		cookieName := "session_token" | ||||||
|  | 		sessionCookie, err := r.Cookie(cookieName) | ||||||
|  | 		if err != nil { | ||||||
|  | 			msg := "auth failed; failed to get session token from cookies" | ||||||
|  | 			slog.Debug(msg, "error", err) | ||||||
|  | 			next.ServeHTTP(w, r) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		cookieValueB, err := base64.URLEncoding. | ||||||
|  | 			DecodeString(sessionCookie.Value) | ||||||
|  | 		if err != nil { | ||||||
|  | 			msg := "auth failed; failed to decode b64 cookie" | ||||||
|  | 			slog.Debug(msg, "error", err) | ||||||
|  | 			next.ServeHTTP(w, r) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		cookieValue := string(cookieValueB) | ||||||
|  | 		if len(cookieValue) < sha256.Size { | ||||||
|  | 			slog.Warn("small cookie", "size", len(cookieValue)) | ||||||
|  | 			next.ServeHTTP(w, r) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// Split apart the signature and original cookie value. | ||||||
|  | 		signature := cookieValue[:sha256.Size] | ||||||
|  | 		sessionToken := cookieValue[sha256.Size:] | ||||||
|  | 		//verify signature | ||||||
|  | 		mac := hmac.New(sha256.New, []byte(cfg.CookieSecret)) | ||||||
|  | 		mac.Write([]byte(cookieName)) | ||||||
|  | 		mac.Write([]byte(sessionToken)) | ||||||
|  | 		expectedSignature := mac.Sum(nil) | ||||||
|  | 		if !hmac.Equal([]byte(signature), expectedSignature) { | ||||||
|  | 			slog.Debug("cookie with an invalid sign") | ||||||
|  | 			next.ServeHTTP(w, r) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		userSession, err := cacheGetSession(sessionToken) | ||||||
|  | 		if err != nil { | ||||||
|  | 			msg := "auth failed; session does not exists" | ||||||
|  | 			err = errors.New(msg) | ||||||
|  | 			slog.Debug(msg, "error", err) | ||||||
|  | 			next.ServeHTTP(w, r) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		if userSession.IsExpired() { | ||||||
|  | 			memcache.RemoveKey(sessionToken) | ||||||
|  | 			msg := "session is expired" | ||||||
|  | 			slog.Debug(msg, "error", err, "token", sessionToken) | ||||||
|  | 			next.ServeHTTP(w, r) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx := context.WithValue(r.Context(), | ||||||
|  | 			"username", userSession.Username) | ||||||
|  | 		if err := cacheSetSession(sessionToken, | ||||||
|  | 			userSession); err != nil { | ||||||
|  | 			msg := "failed to marshal user session" | ||||||
|  | 			slog.Warn(msg, "error", err) | ||||||
|  | 			next.ServeHTTP(w, r) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		next.ServeHTTP(w, r.WithContext(ctx)) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										25
									
								
								models/auth.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								models/auth.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | package models | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // each session contains the username of the user and the time at which it expires | ||||||
|  | type Session struct { | ||||||
|  | 	Username    string | ||||||
|  | 	CurrentRoom string | ||||||
|  | 	Expiry      time.Time | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // we'll use this method later to determine if the session has expired | ||||||
|  | func (s Session) IsExpired() bool { | ||||||
|  | 	return s.Expiry.Before(time.Now()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ListUsernames(ss map[string]*Session) []string { | ||||||
|  | 	resp := make([]string, 0, len(ss)) | ||||||
|  | 	for _, s := range ss { | ||||||
|  | 		resp = append(resp, s.Username) | ||||||
|  | 	} | ||||||
|  | 	return resp | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								models/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								models/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | package models | ||||||
|  |  | ||||||
|  | import "time" | ||||||
|  |  | ||||||
|  | type WordColor string | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	WordColorWhite = "white" | ||||||
|  | 	WordColorBlue  = "blue" | ||||||
|  | 	WordColorRed   = "red" | ||||||
|  | 	WordColorBlack = "black" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | type Room struct { | ||||||
|  | 	ID           string    `json:"id" db:"id"` | ||||||
|  | 	CreatedAt    time.Time `json:"created_at" db:"created_at"` | ||||||
|  | 	RoomName     string    `json:"room_name"` | ||||||
|  | 	RoomPass     string    `json:"room_pass"` | ||||||
|  | 	RoomLink     string | ||||||
|  | 	CreatorName  string   `json:"creator_name"` | ||||||
|  | 	PlayerList   []string `json:"player_list"` | ||||||
|  | 	RedMime      string | ||||||
|  | 	BlueMime     string | ||||||
|  | 	RedGuessers  []string | ||||||
|  | 	BlueGuessers []string | ||||||
|  | 	Cards        []WordCard | ||||||
|  | 	GameSettings *GameSettings `json:"settings"` | ||||||
|  | 	Result       uint8         // 0 for unknown; 1 is win for red; 2 if for blue; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type WordCard struct { | ||||||
|  | 	Word     string | ||||||
|  | 	Color    WordColor | ||||||
|  | 	Revealed bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type RoomPublic struct { | ||||||
|  | 	ID           string        `json:"id" db:"id"` | ||||||
|  | 	CreatedAt    time.Time     `json:"created_at" db:"created_at"` | ||||||
|  | 	PlayerList   []string      `json:"player_list"` | ||||||
|  | 	CreatorName  string        `json:"creator_name"` | ||||||
|  | 	GameSettings *GameSettings `json:"settings"` | ||||||
|  | 	RedMime      string | ||||||
|  | 	BlueMime     string | ||||||
|  | 	RedGuessers  []string | ||||||
|  | 	BlueGuessers []string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (r Room) ToPublic() RoomPublic { | ||||||
|  | 	return RoomPublic{ | ||||||
|  | 		ID:           r.ID, | ||||||
|  | 		CreatedAt:    r.CreatedAt, | ||||||
|  | 		PlayerList:   r.PlayerList, | ||||||
|  | 		GameSettings: r.GameSettings, | ||||||
|  | 		CreatorName:  r.CreatorName, | ||||||
|  | 		RedMime:      r.RedMime, | ||||||
|  | 		BlueMime:     r.BlueMime, | ||||||
|  | 		RedGuessers:  r.RedGuessers, | ||||||
|  | 		BlueGuessers: r.BlueGuessers, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type GameSettings struct { | ||||||
|  | 	IsRunning   bool   `json:"is_running"` | ||||||
|  | 	Language    string `json:"language" example:"en" form:"language"` | ||||||
|  | 	RoundTime   int32  `json:"round_time"` | ||||||
|  | 	ProgressPct uint32 `json:"progress_pct"` | ||||||
|  | 	IsOver      bool | ||||||
|  | } | ||||||
							
								
								
									
										144
									
								
								pkg/cache/impl.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								pkg/cache/impl.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | |||||||
|  | package cache | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"log/slog" | ||||||
|  | 	"os" | ||||||
|  | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const storeFileName = "store.json" | ||||||
|  |  | ||||||
|  | // var MemCache Cache | ||||||
|  | var ( | ||||||
|  | 	MemCache *MemoryCache | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func readJSON(fileName string) (map[string][]byte, error) { | ||||||
|  | 	data := make(map[string][]byte) | ||||||
|  | 	file, err := os.Open(fileName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return data, err | ||||||
|  | 	} | ||||||
|  | 	defer file.Close() | ||||||
|  | 	decoder := json.NewDecoder(file) | ||||||
|  | 	if err := decoder.Decode(&data); err != nil { | ||||||
|  | 		return data, err | ||||||
|  | 	} | ||||||
|  | 	return data, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	data, err := readJSON(storeFileName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		slog.Error("failed to load store from file") | ||||||
|  | 	} | ||||||
|  | 	MemCache = &MemoryCache{ | ||||||
|  | 		data:    data, | ||||||
|  | 		timeMap: make(map[string]time.Time), | ||||||
|  | 		lock:    &sync.RWMutex{}, | ||||||
|  | 	} | ||||||
|  | 	MemCache.StartExpiryRoutine(time.Minute) | ||||||
|  | 	MemCache.StartBackupRoutine(time.Minute) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type MemoryCache struct { | ||||||
|  | 	data    map[string][]byte | ||||||
|  | 	timeMap map[string]time.Time | ||||||
|  | 	lock    *sync.RWMutex | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Get a value by key from the cache | ||||||
|  | func (mc *MemoryCache) Get(key string) (value []byte, err error) { | ||||||
|  | 	var ok bool | ||||||
|  | 	mc.lock.RLock() | ||||||
|  | 	if value, ok = mc.data[key]; !ok { | ||||||
|  | 		err = fmt.Errorf("not found data in mc for the key: %v", key) | ||||||
|  | 	} | ||||||
|  | 	mc.lock.RUnlock() | ||||||
|  | 	return value, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Update a single value in the cache | ||||||
|  | func (mc *MemoryCache) Set(key string, value []byte) { | ||||||
|  | 	// no async writing | ||||||
|  | 	mc.lock.Lock() | ||||||
|  | 	mc.data[key] = value | ||||||
|  | 	mc.lock.Unlock() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (mc *MemoryCache) Expire(key string, exp int64) { | ||||||
|  | 	mc.lock.RLock() | ||||||
|  | 	mc.timeMap[key] = time.Now().Add(time.Duration(exp) * time.Second) | ||||||
|  | 	mc.lock.RUnlock() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (mc *MemoryCache) GetAll() (resp map[string][]byte) { | ||||||
|  | 	resp = make(map[string][]byte) | ||||||
|  | 	mc.lock.RLock() | ||||||
|  | 	for k, v := range mc.data { | ||||||
|  | 		resp[k] = v | ||||||
|  | 	} | ||||||
|  | 	mc.lock.RUnlock() | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (mc *MemoryCache) GetAllTime() (resp map[string]time.Time) { | ||||||
|  | 	resp = make(map[string]time.Time) | ||||||
|  | 	mc.lock.RLock() | ||||||
|  | 	for k, v := range mc.timeMap { | ||||||
|  | 		resp[k] = v | ||||||
|  | 	} | ||||||
|  | 	mc.lock.RUnlock() | ||||||
|  | 	return | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (mc *MemoryCache) RemoveKey(key string) { | ||||||
|  | 	mc.lock.RLock() | ||||||
|  | 	delete(mc.data, key) | ||||||
|  | 	delete(mc.timeMap, key) | ||||||
|  | 	mc.lock.RUnlock() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (mc *MemoryCache) StartExpiryRoutine(n time.Duration) { | ||||||
|  | 	ticker := time.NewTicker(n) | ||||||
|  | 	go func() { | ||||||
|  | 		for { | ||||||
|  | 			<-ticker.C | ||||||
|  | 			// get all | ||||||
|  | 			timeData := mc.GetAllTime() | ||||||
|  | 			// check time | ||||||
|  | 			currentTS := time.Now() | ||||||
|  | 			for k, ts := range timeData { | ||||||
|  | 				if ts.Before(currentTS) { | ||||||
|  | 					// delete exp keys | ||||||
|  | 					mc.RemoveKey(k) | ||||||
|  | 					slog.Debug("remove by expiry", "key", k) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (mc *MemoryCache) StartBackupRoutine(n time.Duration) { | ||||||
|  | 	ticker := time.NewTicker(n) | ||||||
|  | 	go func() { | ||||||
|  | 		for { | ||||||
|  | 			<-ticker.C | ||||||
|  | 			// get all | ||||||
|  | 			data := mc.GetAll() | ||||||
|  | 			jsonString, err := json.Marshal(data) | ||||||
|  | 			if err != nil { | ||||||
|  | 				slog.Warn("failed to marshal", "err", err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			err = os.WriteFile(storeFileName, jsonString, os.ModePerm) | ||||||
|  | 			if err != nil { | ||||||
|  | 				slog.Warn("failed to write", "err", err) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								pkg/cache/main.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								pkg/cache/main.go
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | package cache | ||||||
|  |  | ||||||
|  | type Cache interface { | ||||||
|  | 	Get(key string) ([]byte, error) | ||||||
|  | 	Set(key string, value []byte) | ||||||
|  | 	Expire(key string, exp int64) | ||||||
|  | 	GetAll() (resp map[string][]byte) | ||||||
|  | 	RemoveKey(key string) | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								utils/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								utils/main.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | package utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  | 	"unicode" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func RemoveSpacesFromStr(origin string) string { | ||||||
|  | 	return strings.Map(func(r rune) rune { | ||||||
|  | 		if unicode.IsSpace(r) { | ||||||
|  | 			return -1 | ||||||
|  | 		} | ||||||
|  | 		return r | ||||||
|  | 	}, origin) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func StrInSlice(key string, sl []string) bool { | ||||||
|  | 	for _, i := range sl { | ||||||
|  | 		if key == i { | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Grail Finder
					Grail Finder