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