506 lines
8.4 KiB
Go
506 lines
8.4 KiB
Go
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
|
|
}
|
|
|
|
// CloseAllStores saves and closes all open stores
|
|
func CloseAllStores() {
|
|
mutex.Lock()
|
|
defer mutex.Unlock()
|
|
|
|
for name, store := range stores {
|
|
if store.filename != "" {
|
|
store.save()
|
|
}
|
|
delete(stores, name)
|
|
}
|
|
}
|