// /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") } }