package kv import ( "bufio" "fmt" "os" "strconv" "strings" "sync" "time" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" "github.com/goccy/go-json" ) var ( stores = make(map[string]*Store) mutex sync.RWMutex ) type Store struct { data map[string]string expires map[string]int64 filename string mutex sync.RWMutex } func GetFunctionList() map[string]luajit.GoFunction { return map[string]luajit.GoFunction{ "kv_open": kv_open, "kv_get": kv_get, "kv_set": kv_set, "kv_delete": kv_delete, "kv_clear": kv_clear, "kv_has": kv_has, "kv_size": kv_size, "kv_keys": kv_keys, "kv_values": kv_values, "kv_save": kv_save, "kv_close": kv_close, "kv_increment": kv_increment, "kv_append": kv_append, "kv_expire": kv_expire, "kv_cleanup_expired": kv_cleanup_expired, } } // kv_open(name, filename) -> boolean func kv_open(s *luajit.State) int { name := s.ToString(1) filename := s.ToString(2) if name == "" { s.PushBoolean(false) return 1 } mutex.Lock() defer mutex.Unlock() if store, exists := stores[name]; exists { if filename != "" && store.filename != filename { store.filename = filename } s.PushBoolean(true) return 1 } store := &Store{ data: make(map[string]string), expires: make(map[string]int64), filename: filename, } if filename != "" { store.load() } stores[name] = store s.PushBoolean(true) return 1 } // kv_get(name, key, default) -> value or default func kv_get(s *luajit.State) int { name := s.ToString(1) key := s.ToString(2) hasDefault := s.GetTop() >= 3 mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { if hasDefault { s.PushCopy(3) } else { s.PushNil() } return 1 } store.mutex.RLock() value, found := store.data[key] store.mutex.RUnlock() if found { s.PushString(value) } else if hasDefault { s.PushCopy(3) } else { s.PushNil() } return 1 } // kv_set(name, key, value) -> boolean func kv_set(s *luajit.State) int { name := s.ToString(1) key := s.ToString(2) value := s.ToString(3) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushBoolean(false) return 1 } store.mutex.Lock() store.data[key] = value store.mutex.Unlock() s.PushBoolean(true) return 1 } // kv_delete(name, key) -> boolean func kv_delete(s *luajit.State) int { name := s.ToString(1) key := s.ToString(2) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushBoolean(false) return 1 } store.mutex.Lock() _, existed := store.data[key] delete(store.data, key) delete(store.expires, key) store.mutex.Unlock() s.PushBoolean(existed) return 1 } // kv_clear(name) -> boolean func kv_clear(s *luajit.State) int { name := s.ToString(1) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushBoolean(false) return 1 } store.mutex.Lock() store.data = make(map[string]string) store.expires = make(map[string]int64) store.mutex.Unlock() s.PushBoolean(true) return 1 } // kv_has(name, key) -> boolean func kv_has(s *luajit.State) int { name := s.ToString(1) key := s.ToString(2) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushBoolean(false) return 1 } store.mutex.RLock() _, found := store.data[key] store.mutex.RUnlock() s.PushBoolean(found) return 1 } // kv_size(name) -> number func kv_size(s *luajit.State) int { name := s.ToString(1) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushNumber(0) return 1 } store.mutex.RLock() size := len(store.data) store.mutex.RUnlock() s.PushNumber(float64(size)) return 1 } // kv_keys(name) -> table func kv_keys(s *luajit.State) int { name := s.ToString(1) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.NewTable() return 1 } store.mutex.RLock() keys := make([]string, 0, len(store.data)) for k := range store.data { keys = append(keys, k) } store.mutex.RUnlock() s.PushValue(keys) return 1 } // kv_values(name) -> table func kv_values(s *luajit.State) int { name := s.ToString(1) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.NewTable() return 1 } store.mutex.RLock() values := make([]string, 0, len(store.data)) for _, v := range store.data { values = append(values, v) } store.mutex.RUnlock() s.PushValue(values) return 1 } // kv_save(name) -> boolean func kv_save(s *luajit.State) int { name := s.ToString(1) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists || store.filename == "" { s.PushBoolean(false) return 1 } err := store.save() s.PushBoolean(err == nil) return 1 } // kv_close(name) -> boolean func kv_close(s *luajit.State) int { name := s.ToString(1) mutex.Lock() defer mutex.Unlock() store, exists := stores[name] if !exists { s.PushBoolean(false) return 1 } if store.filename != "" { store.save() } delete(stores, name) s.PushBoolean(true) return 1 } // kv_increment(name, key, delta) -> number func kv_increment(s *luajit.State) int { name := s.ToString(1) key := s.ToString(2) delta := 1.0 if s.GetTop() >= 3 { delta = s.ToNumber(3) } mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushNumber(0) return 1 } store.mutex.Lock() current, _ := strconv.ParseFloat(store.data[key], 64) newValue := current + delta store.data[key] = strconv.FormatFloat(newValue, 'g', -1, 64) store.mutex.Unlock() s.PushNumber(newValue) return 1 } // kv_append(name, key, value, separator) -> boolean func kv_append(s *luajit.State) int { name := s.ToString(1) key := s.ToString(2) value := s.ToString(3) separator := "" if s.GetTop() >= 4 { separator = s.ToString(4) } mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushBoolean(false) return 1 } store.mutex.Lock() current := store.data[key] if current == "" { store.data[key] = value } else { store.data[key] = current + separator + value } store.mutex.Unlock() s.PushBoolean(true) return 1 } // kv_expire(name, key, ttl) -> boolean func kv_expire(s *luajit.State) int { name := s.ToString(1) key := s.ToString(2) ttl := s.ToNumber(3) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushBoolean(false) return 1 } store.mutex.Lock() store.expires[key] = time.Now().Unix() + int64(ttl) store.mutex.Unlock() s.PushBoolean(true) return 1 } // kv_cleanup_expired(name) -> number func kv_cleanup_expired(s *luajit.State) int { name := s.ToString(1) mutex.RLock() store, exists := stores[name] mutex.RUnlock() if !exists { s.PushNumber(0) return 1 } currentTime := time.Now().Unix() deleted := 0 store.mutex.Lock() for key, expireTime := range store.expires { if currentTime >= expireTime { delete(store.data, key) delete(store.expires, key) deleted++ } } store.mutex.Unlock() s.PushNumber(float64(deleted)) return 1 } func (store *Store) load() error { if store.filename == "" { return nil } file, err := os.Open(store.filename) if err != nil { if os.IsNotExist(err) { return nil } return err } defer file.Close() if strings.HasSuffix(store.filename, ".json") { decoder := json.NewDecoder(file) return decoder.Decode(&store.data) } scanner := bufio.NewScanner(file) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { store.data[parts[0]] = parts[1] } } return scanner.Err() } func (store *Store) save() error { if store.filename == "" { return fmt.Errorf("no filename specified") } file, err := os.Create(store.filename) if err != nil { return err } defer file.Close() store.mutex.RLock() defer store.mutex.RUnlock() if strings.HasSuffix(store.filename, ".json") { encoder := json.NewEncoder(file) encoder.SetIndent("", "\t") return encoder.Encode(store.data) } for key, value := range store.data { if _, err := fmt.Fprintf(file, "%s=%s\n", key, value); err != nil { return err } } return nil }