mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: 支持memcached
This commit is contained in:
@@ -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"
|
||||
|
||||
20
internal/apps/memcached/init.go
Normal file
20
internal/apps/memcached/init.go
Normal 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)
|
||||
},
|
||||
})
|
||||
}
|
||||
5
internal/apps/memcached/request.go
Normal file
5
internal/apps/memcached/request.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package memcached
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
97
internal/apps/memcached/service.go
Normal file
97
internal/apps/memcached/service.go
Normal 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)
|
||||
}
|
||||
8
web/src/api/apps/memcached/index.ts
Normal file
8
web/src/api/apps/memcached/index.ts
Normal 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 })
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
180
web/src/views/apps/memcached/IndexView.vue
Normal file
180
web/src/views/apps/memcached/IndexView.vue
Normal 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>
|
||||
23
web/src/views/apps/memcached/route.ts
Normal file
23
web/src/views/apps/memcached/route.ts
Normal 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
|
||||
Reference in New Issue
Block a user