mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 06:47:20 +08:00
feat: apache
This commit is contained in:
@@ -8,6 +8,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/apps/apache"
|
||||
"github.com/acepanel/panel/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/internal/apps/docker"
|
||||
"github.com/acepanel/panel/internal/apps/fail2ban"
|
||||
@@ -127,6 +128,7 @@ func initWeb() (*app.Web, error) {
|
||||
toolboxLogService := service.NewToolboxLogService(locale, db, containerImageRepo, settingRepo)
|
||||
webHookRepo := data.NewWebHookRepo(locale, db, logger)
|
||||
webHookService := service.NewWebHookService(webHookRepo)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
@@ -147,7 +149,7 @@ func initWeb() (*app.Web, error) {
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseUserService, backupService, certService, certDNSService, certAccountService, appService, environmentService, environmentPHPService, cronService, processService, safeService, firewallService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, webHookService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo)
|
||||
ws := route.NewWs(wsService)
|
||||
|
||||
@@ -8,6 +8,7 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/apps/apache"
|
||||
"github.com/acepanel/panel/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/internal/apps/docker"
|
||||
"github.com/acepanel/panel/internal/apps/fail2ban"
|
||||
@@ -72,6 +73,7 @@ func initCli() (*app.Cli, error) {
|
||||
cli := route.NewCli(locale, cliService)
|
||||
command := bootstrap.NewCli(locale, cli)
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
@@ -92,7 +94,7 @@ func initCli() (*app.Cli, error) {
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
appCli := app.NewCli(command, gormigrate, loader)
|
||||
return appCli, nil
|
||||
}
|
||||
|
||||
157
internal/apps/apache/app.go
Normal file
157
internal/apps/apache/app.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package apache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"github.com/acepanel/panel/pkg/tools"
|
||||
"github.com/acepanel/panel/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.SaveConfig)
|
||||
r.Get("/error_log", s.ErrorLog)
|
||||
r.Post("/clear_error_log", s.ClearErrorLog)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *App) SaveConfig(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(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root), req.Config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("apache"); err != nil {
|
||||
_, err = shell.Execf("%s/server/apache/bin/apachectl configtest", app.Root)
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload apache: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) ErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, fmt.Sprintf("%s/%s", app.Root, "server/apache/logs/error_log"))
|
||||
}
|
||||
|
||||
func (s *App) ClearErrorLog(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := shell.Execf("cat /dev/null > %s/%s", app.Root, "server/apache/logs/error_log"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := shell.Execf("curl -s http://127.0.0.1/server_status?auto 2>/dev/null || true")
|
||||
if err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var data []types.NV
|
||||
|
||||
workers, err := shell.Execf("ps aux | grep httpd | grep -v grep | wc -l")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get apache workers: %v", err))
|
||||
return
|
||||
}
|
||||
data = append(data, types.NV{
|
||||
Name: s.t.Get("Workers"),
|
||||
Value: workers,
|
||||
})
|
||||
|
||||
out, err := shell.Execf("ps aux | grep httpd | grep -v grep | awk '{memsum+=$6};END {print memsum}'")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get apache workers: %v", err))
|
||||
return
|
||||
}
|
||||
mem := tools.FormatBytes(cast.ToFloat64(out))
|
||||
data = append(data, types.NV{
|
||||
Name: s.t.Get("Memory"),
|
||||
Value: mem,
|
||||
})
|
||||
|
||||
// Parse server-status output
|
||||
if match := regexp.MustCompile(`Total Accesses:\s*(\d+)`).FindStringSubmatch(status); len(match) == 2 {
|
||||
data = append(data, types.NV{
|
||||
Name: s.t.Get("Total Accesses"),
|
||||
Value: match[1],
|
||||
})
|
||||
}
|
||||
|
||||
if match := regexp.MustCompile(`Total kBytes:\s*(\d+)`).FindStringSubmatch(status); len(match) == 2 {
|
||||
data = append(data, types.NV{
|
||||
Name: s.t.Get("Total Traffic"),
|
||||
Value: tools.FormatBytes(cast.ToFloat64(match[1]) * 1024),
|
||||
})
|
||||
}
|
||||
|
||||
if match := regexp.MustCompile(`BusyWorkers:\s*(\d+)`).FindStringSubmatch(status); len(match) == 2 {
|
||||
data = append(data, types.NV{
|
||||
Name: s.t.Get("Busy Workers"),
|
||||
Value: match[1],
|
||||
})
|
||||
}
|
||||
|
||||
if match := regexp.MustCompile(`IdleWorkers:\s*(\d+)`).FindStringSubmatch(status); len(match) == 2 {
|
||||
data = append(data, types.NV{
|
||||
Name: s.t.Get("Idle Workers"),
|
||||
Value: match[1],
|
||||
})
|
||||
}
|
||||
|
||||
if match := regexp.MustCompile(`ReqPerSec:\s*([\d.]+)`).FindStringSubmatch(status); len(match) == 2 {
|
||||
data = append(data, types.NV{
|
||||
Name: s.t.Get("Requests/sec"),
|
||||
Value: match[1],
|
||||
})
|
||||
}
|
||||
|
||||
if match := regexp.MustCompile(`BytesPerSec:\s*([\d.]+)`).FindStringSubmatch(status); len(match) == 2 {
|
||||
data = append(data, types.NV{
|
||||
Name: s.t.Get("Bytes/sec"),
|
||||
Value: tools.FormatBytes(cast.ToFloat64(match[1])),
|
||||
})
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
5
internal/apps/apache/request.go
Normal file
5
internal/apps/apache/request.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package apache
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package apps
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/internal/apps/apache"
|
||||
"github.com/acepanel/panel/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/internal/apps/docker"
|
||||
"github.com/acepanel/panel/internal/apps/fail2ban"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
apache.NewApp,
|
||||
codeserver.NewApp,
|
||||
docker.NewApp,
|
||||
fail2ban.NewApp,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/internal/apps/apache"
|
||||
"github.com/acepanel/panel/internal/apps/codeserver"
|
||||
"github.com/acepanel/panel/internal/apps/docker"
|
||||
"github.com/acepanel/panel/internal/apps/fail2ban"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
)
|
||||
|
||||
func NewLoader(
|
||||
apache *apache.App,
|
||||
codeserver *codeserver.App,
|
||||
docker *docker.App,
|
||||
fail2ban *fail2ban.App,
|
||||
@@ -47,6 +49,6 @@ func NewLoader(
|
||||
supervisor *supervisor.App,
|
||||
) *apploader.Loader {
|
||||
loader := new(apploader.Loader)
|
||||
loader.Add(codeserver, docker, fail2ban, frp, gitea, mariadb, memcached, minio, mysql, nginx, openresty, percona, phpmyadmin, podman, postgresql, pureftpd, redis, rsync, s3fs, supervisor)
|
||||
loader.Add(apache, codeserver, docker, fail2ban, frp, gitea, mariadb, memcached, minio, mysql, nginx, openresty, percona, phpmyadmin, podman, postgresql, pureftpd, redis, rsync, s3fs, supervisor)
|
||||
return loader
|
||||
}
|
||||
|
||||
@@ -165,7 +165,12 @@ func (v *baseVhost) Listen() []types.Listen {
|
||||
func (v *baseVhost) SetListen(listens []types.Listen) error {
|
||||
var args []string
|
||||
for _, l := range listens {
|
||||
args = append(args, l.Address)
|
||||
addr := l.Address
|
||||
// 如果只是端口号,添加 *: 前缀
|
||||
if !strings.Contains(addr, ":") {
|
||||
addr = "*:" + addr
|
||||
}
|
||||
args = append(args, addr)
|
||||
}
|
||||
v.vhost.Args = args
|
||||
return nil
|
||||
|
||||
14
web/src/api/apps/apache/index.ts
Normal file
14
web/src/api/apps/apache/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 负载状态
|
||||
load: (): any => http.Get('/apps/apache/load'),
|
||||
// 获取配置
|
||||
config: (): any => http.Get('/apps/apache/config'),
|
||||
// 保存配置
|
||||
saveConfig: (config: string): any => http.Post('/apps/apache/config', { config }),
|
||||
// 获取错误日志
|
||||
errorLog: (): any => http.Get('/apps/apache/error_log'),
|
||||
// 清空错误日志
|
||||
clearErrorLog: (): any => http.Post('/apps/apache/clear_error_log')
|
||||
}
|
||||
102
web/src/views/apps/apache/IndexView.vue
Normal file
102
web/src/views/apps/apache/IndexView.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'apps-apache-index'
|
||||
})
|
||||
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import apache from '@/api/apps/apache'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
|
||||
const { data: config } = useRequest(apache.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: errorLog } = useRequest(apache.errorLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(apache.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(apache.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearErrorLog = () => {
|
||||
useRequest(apache.clearErrorLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<service-status service="apache" show-reload />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the %{name} main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!',
|
||||
{ name: 'Apache' }
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" lang="apache" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<realtime-log service="apache" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" :tab="$gettext('Error Logs')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleClearErrorLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
</template>
|
||||
22
web/src/views/apps/apache/route.ts
Normal file
22
web/src/views/apps/apache/route.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { RouteType } from '~/types/router'
|
||||
|
||||
const Layout = () => import('@/layout/IndexView.vue')
|
||||
|
||||
export default {
|
||||
name: 'apache',
|
||||
path: '/apps/apache',
|
||||
component: Layout,
|
||||
isHidden: true,
|
||||
children: [
|
||||
{
|
||||
name: 'apps-apache-index',
|
||||
path: '',
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: 'Apache',
|
||||
role: ['admin'],
|
||||
requireAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} as RouteType
|
||||
Reference in New Issue
Block a user