2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 06:47:20 +08:00

feat: 支持memcached

This commit is contained in:
耗子
2024-11-02 17:48:52 +08:00
parent 7169b90c1d
commit aa162e12db
8 changed files with 419 additions and 83 deletions

View File

@@ -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"

View File

@@ -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)
},
})
}

View File

@@ -0,0 +1,5 @@
package memcached
type UpdateConfig struct {
Config string `form:"config" json:"config" validate:"required"`
}

View File

@@ -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)
}

View File

@@ -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 })

View File

@@ -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
]
}
)
}
}

View File

@@ -0,0 +1,180 @@
<script setup lang="ts">
defineOptions({
name: 'apps-memcached-index'
})
import Editor from '@guolao/vue-monaco-editor'
import { NButton, NDataTable, NPopconfirm } from 'naive-ui'
import { getConfig, getLoad, updateConfig } from '@/api/apps/memcached'
import systemctl from '@/api/panel/systemctl'
const currentTab = ref('status')
const status = ref(false)
const isEnabled = ref(false)
const statusType = computed(() => {
return status.value ? 'success' : 'error'
})
const statusStr = computed(() => {
return status.value ? '正常运行中' : '已停止运行'
})
const loadColumns: any = [
{
title: '属性',
key: 'name',
minWidth: 200,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '当前值',
key: 'value',
minWidth: 200,
ellipsis: { tooltip: true }
}
]
const { data: load }: { data: any } = useRequest(getLoad, {
initialData: []
})
const getStatus = async () => {
await systemctl.status('memcached').then((res: any) => {
status.value = res.data
})
}
const getIsEnabled = async () => {
await systemctl.isEnabled('memcached').then((res: any) => {
isEnabled.value = res.data
})
}
const { data: config }: { data: any } = useRequest(getConfig, {
initialData: {
config: ''
}
})
const handleSaveConfig = async () => {
useRequest(() => updateConfig(config.value)).onSuccess(() => {
window.$message.success('保存成功')
})
}
const handleStart = async () => {
await systemctl.start('memcached')
window.$message.success('启动成功')
await getStatus()
}
const handleIsEnabled = async () => {
if (isEnabled.value) {
await systemctl.enable('memcached')
window.$message.success('开启自启动成功')
} else {
await systemctl.disable('memcached')
window.$message.success('禁用自启动成功')
}
await getIsEnabled()
}
const handleStop = async () => {
await systemctl.stop('memcached')
window.$message.success('停止成功')
await getStatus()
}
const handleRestart = async () => {
await systemctl.restart('memcached')
window.$message.success('重启成功')
await getStatus()
}
onMounted(() => {
getStatus()
getIsEnabled()
})
</script>
<template>
<common-page show-footer>
<template #action>
<n-button
v-if="currentTab == 'config'"
class="ml-16"
type="primary"
@click="handleSaveConfig"
>
<TheIcon :size="18" icon="material-symbols:save-outline" />
保存
</n-button>
</template>
<n-tabs v-model:value="currentTab" type="line" animated>
<n-tab-pane name="status" tab="运行状态">
<n-space vertical>
<n-card title="运行状态" rounded-10>
<template #header-extra>
<n-switch v-model:value="isEnabled" @update:value="handleIsEnabled">
<template #checked> 自启动开 </template>
<template #unchecked> 自启动关 </template>
</n-switch>
</template>
<n-space vertical>
<n-alert :type="statusType">
{{ statusStr }}
</n-alert>
<n-space>
<n-button type="success" @click="handleStart">
<TheIcon :size="24" icon="material-symbols:play-arrow-outline-rounded" />
启动
</n-button>
<n-popconfirm @positive-click="handleStop">
<template #trigger>
<n-button type="error">
<TheIcon :size="24" icon="material-symbols:stop-outline-rounded" />
停止
</n-button>
</template>
停止 Memcached 会导致使用 Memcached 的网站无法访问确定要停止吗
</n-popconfirm>
<n-button type="warning" @click="handleRestart">
<TheIcon :size="18" icon="material-symbols:replay-rounded" />
重启
</n-button>
</n-space>
</n-space>
</n-card>
</n-space>
</n-tab-pane>
<n-tab-pane name="config" tab="服务配置">
<n-space vertical>
<Editor
v-model:value="config"
language="ini"
theme="vs-dark"
height="60vh"
mt-8
:options="{
automaticLayout: true,
formatOnType: true,
formatOnPaste: true
}"
/>
</n-space>
</n-tab-pane>
<n-tab-pane name="load" tab="负载状态">
<n-data-table
striped
remote
:scroll-x="1000"
:loading="false"
:columns="loadColumns"
:data="load"
/>
</n-tab-pane>
</n-tabs>
</common-page>
</template>

View File

@@ -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