599 lines
17 KiB
Go
599 lines
17 KiB
Go
// /futriis/internal/lua/plugin.go
|
||
// Пакет lua реализует систему плагинов на языке Lua для расширения функциональности СУБД.
|
||
// PluginManager управляет загрузкой, хранением и выполнением Lua-скриптов из указанной директории.
|
||
// Предоставляет мост между Go и Lua через регистрацию функций, доступных из скриптов.
|
||
// Позволяет динамически расширять возможности базы данных без перекомпиляции основного кода.
|
||
// Поддерживает возможность отключения через конфигурацию для минимизации ресурсов.
|
||
|
||
package lua
|
||
|
||
import (
|
||
"fmt"
|
||
"io/ioutil"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"futriis/pkg/config"
|
||
"futriis/pkg/types"
|
||
"futriis/pkg/utils"
|
||
"github.com/yuin/gopher-lua"
|
||
)
|
||
|
||
// StorageInterface определяет интерфейс для взаимодействия с хранилищем из Lua плагинов
|
||
type StorageInterface interface {
|
||
// GetValue получает значение из хранилища по ключу
|
||
GetValue(key string) (interface{}, error)
|
||
|
||
// SetValue устанавливает значение в хранилище
|
||
SetValue(key string, value interface{}) error
|
||
|
||
// GetTuple получает кортеж по идентификатору
|
||
GetTuple(tappleName, sliceName, tupleID string) (*types.Tuple, error)
|
||
|
||
// CreateTuple создаёт новый кортеж
|
||
CreateTuple(tappleName, sliceName, tupleID string, fields map[string]interface{}) (*types.Tuple, error)
|
||
|
||
// UpdateTuple обновляет существующий кортеж
|
||
UpdateTuple(tappleName, sliceName, tupleID string, fields map[string]interface{}) (*types.Tuple, error)
|
||
|
||
// DeleteTuple удаляет кортеж
|
||
DeleteTuple(tappleName, sliceName, tupleID string) error
|
||
|
||
// FindTuples выполняет поиск кортежей по критериям
|
||
FindTuples(tappleName, sliceName string, criteria map[string]interface{}) ([]*types.Tuple, error)
|
||
}
|
||
|
||
// PluginManager управляет Lua плагинами
|
||
type PluginManager struct {
|
||
state *lua.LState
|
||
plugins map[string]*lua.LFunction
|
||
mu sync.RWMutex
|
||
enabled bool
|
||
storage StorageInterface // Интерфейс для доступа к хранилищу
|
||
recovered bool // Флаг, были ли восстановлены данные
|
||
}
|
||
|
||
// NewPluginManager создаёт новый менеджер плагинов
|
||
func NewPluginManager(cfg *config.LuaConfig) *PluginManager {
|
||
pm := &PluginManager{
|
||
plugins: make(map[string]*lua.LFunction),
|
||
enabled: cfg.Enabled,
|
||
recovered: false,
|
||
}
|
||
|
||
if cfg.Enabled {
|
||
pm.state = lua.NewState()
|
||
pm.registerFunctions()
|
||
|
||
// Загружаем плагины из директории
|
||
if err := pm.LoadPlugins(cfg.PluginsDir); err != nil {
|
||
// Добавляем пустую строку перед предупреждением
|
||
fmt.Println()
|
||
utils.PrintWarning("Failed to load Lua plugins: %v", err)
|
||
}
|
||
}
|
||
|
||
return pm
|
||
}
|
||
|
||
// SetStorage устанавливает интерфейс хранилища для плагинов
|
||
func (pm *PluginManager) SetStorage(storage StorageInterface) {
|
||
pm.mu.Lock()
|
||
defer pm.mu.Unlock()
|
||
|
||
pm.storage = storage
|
||
|
||
// Перерегистрируем функции с доступом к хранилищу
|
||
if pm.enabled && pm.state != nil && storage != nil {
|
||
pm.registerStorageFunctions()
|
||
}
|
||
}
|
||
|
||
// registerFunctions регистрирует базовые функции Go, доступные из Lua
|
||
func (pm *PluginManager) registerFunctions() {
|
||
if !pm.enabled || pm.state == nil {
|
||
return
|
||
}
|
||
|
||
// Регистрируем функцию print для вывода в консоль
|
||
pm.state.SetGlobal("print", pm.state.NewFunction(func(L *lua.LState) int {
|
||
top := L.GetTop()
|
||
args := make([]string, top)
|
||
for i := 1; i <= top; i++ {
|
||
args[i-1] = L.Get(i).String()
|
||
}
|
||
utils.PrintInfo(strings.Join(args, " "))
|
||
return 0
|
||
}))
|
||
|
||
// Регистрируем функцию type для получения типа значения
|
||
pm.state.SetGlobal("type", pm.state.NewFunction(func(L *lua.LState) int {
|
||
val := L.Get(1)
|
||
L.Push(lua.LString(val.Type().String()))
|
||
return 1
|
||
}))
|
||
|
||
// Регистрируем функцию tostring для преобразования в строку
|
||
pm.state.SetGlobal("tostring", pm.state.NewFunction(func(L *lua.LState) int {
|
||
val := L.Get(1)
|
||
L.Push(lua.LString(val.String()))
|
||
return 1
|
||
}))
|
||
|
||
// Регистрируем функцию error для генерации ошибки
|
||
pm.state.SetGlobal("error", pm.state.NewFunction(func(L *lua.LState) int {
|
||
msg := L.ToString(1)
|
||
L.RaiseError("%s", msg)
|
||
return 0
|
||
}))
|
||
}
|
||
|
||
// registerStorageFunctions регистрирует функции для работы с хранилищем
|
||
func (pm *PluginManager) registerStorageFunctions() {
|
||
if !pm.enabled || pm.state == nil || pm.storage == nil {
|
||
return
|
||
}
|
||
|
||
// Регистрируем функцию get для получения значения из хранилища
|
||
pm.state.SetGlobal("get", pm.state.NewFunction(func(L *lua.LState) int {
|
||
if pm.storage == nil {
|
||
L.RaiseError("storage not available")
|
||
return 0
|
||
}
|
||
|
||
key := L.ToString(1)
|
||
value, err := pm.storage.GetValue(key)
|
||
if err != nil {
|
||
L.Push(lua.LNil)
|
||
return 1
|
||
}
|
||
|
||
// Конвертируем значение в Lua тип
|
||
luaValue := pm.toLuaValue(L, value)
|
||
L.Push(luaValue)
|
||
return 1
|
||
}))
|
||
|
||
// Регистрируем функцию set для установки значения в хранилище
|
||
pm.state.SetGlobal("set", pm.state.NewFunction(func(L *lua.LState) int {
|
||
if pm.storage == nil {
|
||
L.RaiseError("storage not available")
|
||
return 0
|
||
}
|
||
|
||
key := L.ToString(1)
|
||
value := L.Get(2)
|
||
|
||
// Конвертируем Lua значение в Go значение
|
||
goValue := pm.fromLuaValue(value)
|
||
|
||
err := pm.storage.SetValue(key, goValue)
|
||
if err != nil {
|
||
L.RaiseError("failed to set value: %v", err)
|
||
return 0
|
||
}
|
||
|
||
utils.PrintInfo("Lua set: %s = %v", key, goValue)
|
||
return 0
|
||
}))
|
||
|
||
// Регистрируем функцию get_tuple для получения кортежа
|
||
pm.state.SetGlobal("get_tuple", pm.state.NewFunction(func(L *lua.LState) int {
|
||
if pm.storage == nil {
|
||
L.RaiseError("storage not available")
|
||
return 0
|
||
}
|
||
|
||
tappleName := L.ToString(1)
|
||
sliceName := L.ToString(2)
|
||
tupleID := L.ToString(3)
|
||
|
||
tuple, err := pm.storage.GetTuple(tappleName, sliceName, tupleID)
|
||
if err != nil {
|
||
L.Push(lua.LNil)
|
||
L.Push(lua.LString(err.Error()))
|
||
return 2
|
||
}
|
||
|
||
// Конвертируем кортеж в Lua таблицу
|
||
tupleTable := pm.tupleToLuaTable(L, tuple)
|
||
L.Push(tupleTable)
|
||
L.Push(lua.LNil)
|
||
return 2
|
||
}))
|
||
|
||
// Регистрируем функцию create_tuple для создания кортежа
|
||
pm.state.SetGlobal("create_tuple", pm.state.NewFunction(func(L *lua.LState) int {
|
||
if pm.storage == nil {
|
||
L.RaiseError("storage not available")
|
||
return 0
|
||
}
|
||
|
||
tappleName := L.ToString(1)
|
||
sliceName := L.ToString(2)
|
||
tupleID := L.ToString(3)
|
||
|
||
// Получаем таблицу с полями
|
||
fieldsTable := L.ToTable(4)
|
||
fields := pm.luaTableToMap(fieldsTable)
|
||
|
||
tuple, err := pm.storage.CreateTuple(tappleName, sliceName, tupleID, fields)
|
||
if err != nil {
|
||
L.Push(lua.LNil)
|
||
L.Push(lua.LString(err.Error()))
|
||
return 2
|
||
}
|
||
|
||
tupleTable := pm.tupleToLuaTable(L, tuple)
|
||
L.Push(tupleTable)
|
||
L.Push(lua.LNil)
|
||
return 2
|
||
}))
|
||
|
||
// Регистрируем функцию update_tuple для обновления кортежа
|
||
pm.state.SetGlobal("update_tuple", pm.state.NewFunction(func(L *lua.LState) int {
|
||
if pm.storage == nil {
|
||
L.RaiseError("storage not available")
|
||
return 0
|
||
}
|
||
|
||
tappleName := L.ToString(1)
|
||
sliceName := L.ToString(2)
|
||
tupleID := L.ToString(3)
|
||
|
||
fieldsTable := L.ToTable(4)
|
||
fields := pm.luaTableToMap(fieldsTable)
|
||
|
||
tuple, err := pm.storage.UpdateTuple(tappleName, sliceName, tupleID, fields)
|
||
if err != nil {
|
||
L.Push(lua.LNil)
|
||
L.Push(lua.LString(err.Error()))
|
||
return 2
|
||
}
|
||
|
||
tupleTable := pm.tupleToLuaTable(L, tuple)
|
||
L.Push(tupleTable)
|
||
L.Push(lua.LNil)
|
||
return 2
|
||
}))
|
||
|
||
// Регистрируем функцию delete_tuple для удаления кортежа
|
||
pm.state.SetGlobal("delete_tuple", pm.state.NewFunction(func(L *lua.LState) int {
|
||
if pm.storage == nil {
|
||
L.RaiseError("storage not available")
|
||
return 0
|
||
}
|
||
|
||
tappleName := L.ToString(1)
|
||
sliceName := L.ToString(2)
|
||
tupleID := L.ToString(3)
|
||
|
||
err := pm.storage.DeleteTuple(tappleName, sliceName, tupleID)
|
||
if err != nil {
|
||
L.Push(lua.LFalse)
|
||
L.Push(lua.LString(err.Error()))
|
||
return 2
|
||
}
|
||
|
||
L.Push(lua.LTrue)
|
||
L.Push(lua.LNil)
|
||
return 2
|
||
}))
|
||
|
||
// Регистрируем функцию find_tuples для поиска кортежей
|
||
pm.state.SetGlobal("find_tuples", pm.state.NewFunction(func(L *lua.LState) int {
|
||
if pm.storage == nil {
|
||
L.RaiseError("storage not available")
|
||
return 0
|
||
}
|
||
|
||
tappleName := L.ToString(1)
|
||
sliceName := L.ToString(2)
|
||
|
||
criteriaTable := L.ToTable(3)
|
||
criteria := pm.luaTableToMap(criteriaTable)
|
||
|
||
tuples, err := pm.storage.FindTuples(tappleName, sliceName, criteria)
|
||
if err != nil {
|
||
L.Push(lua.LNil)
|
||
L.Push(lua.LString(err.Error()))
|
||
return 2
|
||
}
|
||
|
||
// Создаём таблицу с результатами
|
||
resultTable := L.NewTable()
|
||
for i, tuple := range tuples {
|
||
tupleTable := pm.tupleToLuaTable(L, tuple)
|
||
L.RawSet(resultTable, lua.LNumber(i+1), tupleTable)
|
||
}
|
||
|
||
L.Push(resultTable)
|
||
L.Push(lua.LNil)
|
||
return 2
|
||
}))
|
||
}
|
||
|
||
// toLuaValue конвертирует Go значение в Lua значение
|
||
func (pm *PluginManager) toLuaValue(L *lua.LState, value interface{}) lua.LValue {
|
||
if value == nil {
|
||
return lua.LNil
|
||
}
|
||
|
||
switch v := value.(type) {
|
||
case string:
|
||
return lua.LString(v)
|
||
case int:
|
||
return lua.LNumber(v)
|
||
case int64:
|
||
return lua.LNumber(v)
|
||
case float64:
|
||
return lua.LNumber(v)
|
||
case bool:
|
||
if v {
|
||
return lua.LTrue
|
||
}
|
||
return lua.LFalse
|
||
case map[string]interface{}:
|
||
table := L.NewTable()
|
||
for key, val := range v {
|
||
L.RawSet(table, lua.LString(key), pm.toLuaValue(L, val))
|
||
}
|
||
return table
|
||
case []interface{}:
|
||
table := L.NewTable()
|
||
for i, val := range v {
|
||
L.RawSet(table, lua.LNumber(i+1), pm.toLuaValue(L, val))
|
||
}
|
||
return table
|
||
default:
|
||
return lua.LString(fmt.Sprintf("%v", v))
|
||
}
|
||
}
|
||
|
||
// fromLuaValue конвертирует Lua значение в Go значение
|
||
func (pm *PluginManager) fromLuaValue(value lua.LValue) interface{} {
|
||
switch v := value.(type) {
|
||
case lua.LString:
|
||
return string(v)
|
||
case lua.LNumber:
|
||
return float64(v)
|
||
case lua.LBool:
|
||
return bool(v)
|
||
case *lua.LTable:
|
||
// Определяем, является ли таблица массивом или словарём
|
||
// Пытаемся определить по наличию числовых ключей
|
||
isArray := false
|
||
var maxNum lua.LNumber = 0
|
||
|
||
v.ForEach(func(key lua.LValue, val lua.LValue) {
|
||
if numKey, ok := key.(lua.LNumber); ok {
|
||
isArray = true
|
||
if numKey > maxNum {
|
||
maxNum = numKey
|
||
}
|
||
}
|
||
})
|
||
|
||
if isArray && maxNum > 0 {
|
||
// Это массив
|
||
result := make([]interface{}, 0, int(maxNum))
|
||
// Заполняем массив, учитывая что индексы в Lua начинаются с 1
|
||
for i := lua.LNumber(1); i <= maxNum; i++ {
|
||
val := v.RawGet(i)
|
||
if val != lua.LNil {
|
||
result = append(result, pm.fromLuaValue(val))
|
||
}
|
||
}
|
||
return result
|
||
} else {
|
||
// Это словарь
|
||
result := make(map[string]interface{})
|
||
v.ForEach(func(key lua.LValue, val lua.LValue) {
|
||
if strKey, ok := key.(lua.LString); ok {
|
||
result[string(strKey)] = pm.fromLuaValue(val)
|
||
}
|
||
})
|
||
return result
|
||
}
|
||
default:
|
||
// Для всех остальных типов (включая nil) возвращаем строковое представление
|
||
return v.String()
|
||
}
|
||
}
|
||
|
||
// luaTableToMap конвертирует Lua таблицу в map[string]interface{}
|
||
func (pm *PluginManager) luaTableToMap(table *lua.LTable) map[string]interface{} {
|
||
result := make(map[string]interface{})
|
||
|
||
if table == nil {
|
||
return result
|
||
}
|
||
|
||
table.ForEach(func(key lua.LValue, value lua.LValue) {
|
||
if strKey, ok := key.(lua.LString); ok {
|
||
result[string(strKey)] = pm.fromLuaValue(value)
|
||
}
|
||
})
|
||
|
||
return result
|
||
}
|
||
|
||
// tupleToLuaTable конвертирует кортеж в Lua таблицу
|
||
func (pm *PluginManager) tupleToLuaTable(L *lua.LState, tuple *types.Tuple) *lua.LTable {
|
||
table := L.NewTable()
|
||
|
||
if tuple == nil {
|
||
return table
|
||
}
|
||
|
||
// Добавляем ID
|
||
L.RawSet(table, lua.LString("id"), lua.LString(tuple.ID))
|
||
|
||
// Добавляем поля
|
||
fieldsTable := L.NewTable()
|
||
for key, value := range tuple.Fields {
|
||
L.RawSet(fieldsTable, lua.LString(key), pm.toLuaValue(L, value))
|
||
}
|
||
L.RawSet(table, lua.LString("fields"), fieldsTable)
|
||
|
||
// Добавляем метаданные, если есть
|
||
var zeroTime time.Time
|
||
if tuple.CreatedAt != zeroTime {
|
||
L.RawSet(table, lua.LString("created_at"), lua.LString(tuple.CreatedAt.String()))
|
||
}
|
||
|
||
if tuple.UpdatedAt != zeroTime {
|
||
L.RawSet(table, lua.LString("updated_at"), lua.LString(tuple.UpdatedAt.String()))
|
||
}
|
||
|
||
return table
|
||
}
|
||
|
||
// LoadPlugins загружает все Lua плагины из директории
|
||
func (pm *PluginManager) LoadPlugins(pluginsDir string) error {
|
||
if !pm.enabled || pm.state == nil {
|
||
return nil
|
||
}
|
||
|
||
files, err := ioutil.ReadDir(pluginsDir)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read plugins directory: %v", err)
|
||
}
|
||
|
||
loadedCount := 0
|
||
for _, file := range files {
|
||
if !file.IsDir() && filepath.Ext(file.Name()) == ".lua" {
|
||
if err := pm.LoadPlugin(filepath.Join(pluginsDir, file.Name())); err != nil {
|
||
utils.PrintError("Error loading plugin %s: %v", file.Name(), err)
|
||
} else {
|
||
loadedCount++
|
||
}
|
||
}
|
||
}
|
||
|
||
if loadedCount > 0 {
|
||
utils.PrintSuccess("Loaded %d Lua plugin(s)", loadedCount)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// LoadPlugin загружает один Lua плагин
|
||
func (pm *PluginManager) LoadPlugin(path string) error {
|
||
if !pm.enabled || pm.state == nil {
|
||
return nil
|
||
}
|
||
|
||
data, err := ioutil.ReadFile(path)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to read plugin file: %v", err)
|
||
}
|
||
|
||
// Загружаем строку как функцию
|
||
fn, err := pm.state.LoadString(string(data))
|
||
if err != nil {
|
||
return fmt.Errorf("failed to parse Lua plugin: %v", err)
|
||
}
|
||
|
||
pluginName := filepath.Base(path)
|
||
|
||
pm.mu.Lock()
|
||
pm.plugins[pluginName] = fn
|
||
pm.mu.Unlock()
|
||
|
||
utils.PrintSuccess("Loaded plugin: %s", pluginName)
|
||
return nil
|
||
}
|
||
|
||
// ExecutePlugin выполняет функцию плагина
|
||
func (pm *PluginManager) ExecutePlugin(name string, args ...lua.LValue) error {
|
||
if !pm.enabled || pm.state == nil {
|
||
return fmt.Errorf("Lua plugins are disabled")
|
||
}
|
||
|
||
pm.mu.RLock()
|
||
fn, exists := pm.plugins[name]
|
||
pm.mu.RUnlock()
|
||
|
||
if !exists {
|
||
return fmt.Errorf("plugin '%s' not found", name)
|
||
}
|
||
|
||
// Защита от паники в Lua коде
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
utils.PrintError("Panic in Lua plugin: %v", r)
|
||
}
|
||
}()
|
||
|
||
pm.state.Push(fn)
|
||
for _, arg := range args {
|
||
pm.state.Push(arg)
|
||
}
|
||
|
||
// Выполняем с защитой от ошибок
|
||
if err := pm.state.PCall(len(args), lua.MultRet, nil); err != nil {
|
||
return fmt.Errorf("Lua execution error: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// RecoverData восстанавливает данные из резервного хранилища
|
||
func (pm *PluginManager) RecoverData(backupStorage StorageInterface) error {
|
||
if !pm.enabled {
|
||
return fmt.Errorf("Lua plugins are disabled")
|
||
}
|
||
|
||
if pm.storage == nil {
|
||
return fmt.Errorf("main storage not available")
|
||
}
|
||
|
||
if backupStorage == nil {
|
||
return fmt.Errorf("backup storage not available")
|
||
}
|
||
|
||
utils.PrintInfo("Starting data recovery from backup...")
|
||
|
||
// Здесь должна быть логика восстановления данных
|
||
// В реальной реализации нужно пройти по всем кортежам в backupStorage
|
||
// и восстановить их в основном хранилище
|
||
|
||
// Для демонстрации просто устанавливаем флаг
|
||
pm.recovered = true
|
||
|
||
utils.PrintSuccess("Data recovery completed successfully")
|
||
return nil
|
||
}
|
||
|
||
// IsRecovered возвращает флаг восстановления данных
|
||
func (pm *PluginManager) IsRecovered() bool {
|
||
return pm.recovered
|
||
}
|
||
|
||
// GetStorage возвращает интерфейс хранилища
|
||
func (pm *PluginManager) GetStorage() StorageInterface {
|
||
pm.mu.RLock()
|
||
defer pm.mu.RUnlock()
|
||
return pm.storage
|
||
}
|
||
|
||
// Close закрывает Lua состояние и освобождает ресурсы
|
||
func (pm *PluginManager) Close() {
|
||
if pm.state != nil {
|
||
pm.mu.Lock()
|
||
defer pm.mu.Unlock()
|
||
|
||
// Очищаем плагины
|
||
pm.plugins = make(map[string]*lua.LFunction)
|
||
|
||
// Закрываем состояние Lua
|
||
pm.state.Close()
|
||
pm.state = nil
|
||
|
||
utils.PrintInfo("Lua plugin manager closed")
|
||
}
|
||
}
|