From aa162e12db9d10e88ac69fb01eab4c01fe6d980c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Sat, 2 Nov 2024 17:48:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81memcached?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/apps/init.go | 1 + internal/apps/memcached/init.go | 20 +++ internal/apps/memcached/request.go | 5 + internal/apps/memcached/service.go | 97 +++++++++++ web/src/api/apps/memcached/index.ts | 8 + web/src/views/app/IndexView.vue | 168 +++++++++---------- web/src/views/apps/memcached/IndexView.vue | 180 +++++++++++++++++++++ web/src/views/apps/memcached/route.ts | 23 +++ 8 files changed, 419 insertions(+), 83 deletions(-) create mode 100644 internal/apps/memcached/init.go create mode 100644 internal/apps/memcached/request.go create mode 100644 internal/apps/memcached/service.go create mode 100644 web/src/api/apps/memcached/index.ts create mode 100644 web/src/views/apps/memcached/IndexView.vue create mode 100644 web/src/views/apps/memcached/route.ts diff --git a/internal/apps/init.go b/internal/apps/init.go index e481ccb7..f9cca706 100644 --- a/internal/apps/init.go +++ b/internal/apps/init.go @@ -8,6 +8,7 @@ import ( _ "github.com/TheTNB/panel/internal/apps/fail2ban" _ "github.com/TheTNB/panel/internal/apps/frp" _ "github.com/TheTNB/panel/internal/apps/gitea" + _ "github.com/TheTNB/panel/internal/apps/memcached" _ "github.com/TheTNB/panel/internal/apps/mysql" _ "github.com/TheTNB/panel/internal/apps/nginx" _ "github.com/TheTNB/panel/internal/apps/php" diff --git a/internal/apps/memcached/init.go b/internal/apps/memcached/init.go new file mode 100644 index 00000000..dc1ecc06 --- /dev/null +++ b/internal/apps/memcached/init.go @@ -0,0 +1,20 @@ +package memcached + +import ( + "github.com/go-chi/chi/v5" + + "github.com/TheTNB/panel/pkg/apploader" + "github.com/TheTNB/panel/pkg/types" +) + +func init() { + apploader.Register(&types.App{ + Slug: "memcached", + Route: func(r chi.Router) { + service := NewService() + r.Get("/load", service.Load) + r.Get("/config", service.GetConfig) + r.Post("/config", service.UpdateConfig) + }, + }) +} diff --git a/internal/apps/memcached/request.go b/internal/apps/memcached/request.go new file mode 100644 index 00000000..150b2ca8 --- /dev/null +++ b/internal/apps/memcached/request.go @@ -0,0 +1,5 @@ +package memcached + +type UpdateConfig struct { + Config string `form:"config" json:"config" validate:"required"` +} diff --git a/internal/apps/memcached/service.go b/internal/apps/memcached/service.go new file mode 100644 index 00000000..6e6d7290 --- /dev/null +++ b/internal/apps/memcached/service.go @@ -0,0 +1,97 @@ +package memcached + +import ( + "bufio" + "net" + "net/http" + "regexp" + + "github.com/TheTNB/panel/internal/service" + "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/systemctl" + "github.com/TheTNB/panel/pkg/types" +) + +type Service struct{} + +func NewService() *Service { + return &Service{} +} + +func (s *Service) Load(w http.ResponseWriter, r *http.Request) { + status, err := systemctl.Status("memcached") + if err != nil { + service.Error(w, http.StatusInternalServerError, "failed to get Memcached status: %v", err) + return + } + if !status { + service.Success(w, []types.NV{}) + return + } + + conn, err := net.Dial("tcp", "127.0.0.1:11211") + if err != nil { + service.Success(w, []types.NV{}) + return + } + defer conn.Close() + + _, err = conn.Write([]byte("stats\nquit\n")) + if err != nil { + service.Error(w, http.StatusInternalServerError, "failed to write to Memcached: %v", err) + return + } + + data := make([]types.NV, 0) + re := regexp.MustCompile(`STAT\s(\S+)\s(\S+)`) + scanner := bufio.NewScanner(conn) + for scanner.Scan() { + line := scanner.Text() + if matches := re.FindStringSubmatch(line); matches != nil && len(matches) == 3 { + data = append(data, types.NV{ + Name: matches[1], + Value: matches[2], + }) + } + if line == "END" { + break + } + } + + if err = scanner.Err(); err != nil { + service.Error(w, http.StatusInternalServerError, "failed to read from Memcached: %v", err) + return + } + + service.Success(w, data) +} + +func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) { + config, err := io.Read("/etc/systemd/system/memcached.service") + if err != nil { + service.Error(w, http.StatusInternalServerError, "%v", err) + return + } + + service.Success(w, config) +} + +func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) { + req, err := service.Bind[UpdateConfig](r) + if err != nil { + service.Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = io.Write("/etc/systemd/system/memcached.service", req.Config, 0644); err != nil { + service.Error(w, http.StatusInternalServerError, "%v", err) + return + } + + if err = systemctl.Restart("memcached"); err != nil { + service.Error(w, http.StatusInternalServerError, "%v", err) + return + } + + service.Success(w, nil) +} diff --git a/web/src/api/apps/memcached/index.ts b/web/src/api/apps/memcached/index.ts new file mode 100644 index 00000000..209f6370 --- /dev/null +++ b/web/src/api/apps/memcached/index.ts @@ -0,0 +1,8 @@ +import { http } from '@/utils' + +// 负载状态 +export const getLoad = () => http.Get('/apps/memcached/load') +// 获取配置 +export const getConfig = () => http.Get('/apps/memcached/config') +// 保存配置 +export const updateConfig = (config: string) => http.Post('/apps/memcached/config', { config }) diff --git a/web/src/views/app/IndexView.vue b/web/src/views/app/IndexView.vue index f2433070..27e82dfe 100644 --- a/web/src/views/app/IndexView.vue +++ b/web/src/views/app/IndexView.vue @@ -79,94 +79,96 @@ const columns: any = [ { justify: 'center' }, - [ - row.installed && row.update_exist - ? h( - NPopconfirm, - { - onPositiveClick: () => handleUpdate(row.slug) - }, - { - default: () => { - return t('appIndex.confirm.update', { app: row.name }) + { + default: () => [ + row.installed && row.update_exist + ? h( + NPopconfirm, + { + onPositiveClick: () => handleUpdate(row.slug) }, - trigger: () => { - return h( - NButton, - { - size: 'small', - type: 'warning' - }, - { - default: () => t('appIndex.buttons.update'), - icon: renderIcon('material-symbols:arrow-circle-up-outline-rounded', { - size: 14 - }) - } - ) + { + default: () => { + return t('appIndex.confirm.update', { app: row.name }) + }, + trigger: () => { + return h( + NButton, + { + size: 'small', + type: 'warning' + }, + { + default: () => t('appIndex.buttons.update'), + icon: renderIcon('material-symbols:arrow-circle-up-outline-rounded', { + size: 14 + }) + } + ) + } } - } - ) - : null, - row.installed - ? h( - NButton, - { - size: 'small', - type: 'success', - onClick: () => handleManage(row.slug) - }, - { - default: () => t('appIndex.buttons.manage'), - icon: renderIcon('material-symbols:settings-outline', { size: 14 }) - } - ) - : null, - row.installed - ? h( - NPopconfirm, - { - onPositiveClick: () => handleUninstall(row.slug) - }, - { - default: () => { - return t('appIndex.confirm.uninstall', { app: row.name }) + ) + : null, + row.installed + ? h( + NButton, + { + size: 'small', + type: 'success', + onClick: () => handleManage(row.slug) }, - trigger: () => { - return h( - NButton, - { - size: 'small', - type: 'error' - }, - { - default: () => t('appIndex.buttons.uninstall'), - icon: renderIcon('material-symbols:delete-outline', { size: 14 }) - } - ) + { + default: () => t('appIndex.buttons.manage'), + icon: renderIcon('material-symbols:settings-outline', { size: 14 }) } - } - ) - : null, - !row.installed - ? h( - NButton, - { - size: 'small', - type: 'info', - onClick: () => { - versionModalShow.value = true - versionModalOperation.value = '安装' - versionModalInfo.value = row + ) + : null, + row.installed + ? h( + NPopconfirm, + { + onPositiveClick: () => handleUninstall(row.slug) + }, + { + default: () => { + return t('appIndex.confirm.uninstall', { app: row.name }) + }, + trigger: () => { + return h( + NButton, + { + size: 'small', + type: 'error' + }, + { + default: () => t('appIndex.buttons.uninstall'), + icon: renderIcon('material-symbols:delete-outline', { size: 14 }) + } + ) + } } - }, - { - default: () => t('appIndex.buttons.install'), - icon: renderIcon('material-symbols:download-rounded', { size: 14 }) - } - ) - : null - ] + ) + : null, + !row.installed + ? h( + NButton, + { + size: 'small', + type: 'info', + onClick: () => { + versionModalShow.value = true + versionModalOperation.value = '安装' + versionModalInfo.value = row + } + }, + { + default: () => t('appIndex.buttons.install'), + icon: renderIcon('material-symbols:download-rounded', { size: 14 }) + } + ) + : null + ] + } ) } } diff --git a/web/src/views/apps/memcached/IndexView.vue b/web/src/views/apps/memcached/IndexView.vue new file mode 100644 index 00000000..64f20f22 --- /dev/null +++ b/web/src/views/apps/memcached/IndexView.vue @@ -0,0 +1,180 @@ + + + diff --git a/web/src/views/apps/memcached/route.ts b/web/src/views/apps/memcached/route.ts new file mode 100644 index 00000000..5e19e24f --- /dev/null +++ b/web/src/views/apps/memcached/route.ts @@ -0,0 +1,23 @@ +import type { RouteType } from '~/types/router' + +const Layout = () => import('@/layout/IndexView.vue') + +export default { + name: 'memcached', + path: '/apps/memcached', + component: Layout, + isHidden: true, + children: [ + { + name: 'apps-memcached-index', + path: '', + component: () => import('./IndexView.vue'), + meta: { + title: 'Memcached', + icon: 'logos:memcached', + role: ['admin'], + requireAuth: true + } + } + ] +} as RouteType