mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-31 01:12:17 +08:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a364076e73 | |||
| f1a984deb3 | |||
| ad98ebce57 | |||
| 3e453f4510 | |||
| 77aa2a3450 | |||
| 0dfe577fc4 | |||
| af23f49811 | |||
| d69caa6aed | |||
| ecd403a334 | |||
| a729718fd9 | |||
| cab0e0ae2b | |||
| 5c729082a5 | |||
| a7e17c9cbe | |||
| 6a888b14f6 | |||
| 5de88d4846 | |||
| c5a3858601 | |||
| 73bc213a0f | |||
| 2dcd3dcdc6 | |||
| 4e67b31d40 | |||
| c1b1278db4 | |||
| 5391911a97 | |||
| 335fbf697f | |||
| 94d90015f4 | |||
| d8b2a6e574 | |||
| d7fc81f550 | |||
| 6e97789db2 | |||
| e280b05891 | |||
| bdab2c3e83 | |||
| 049d5c5343 | |||
| a6c2a60e55 | |||
| db88d0d20b | |||
| 926a9dcac6 | |||
| b0b1e741bd | |||
| cafec4a3cc | |||
| 8109915e20 | |||
| 19372d68ce | |||
| 237b1bfcee | |||
| e501896943 | |||
| e8f7811aba | |||
| 5654ff907d | |||
| 6406d676be | |||
| 61e28afa31 | |||
| b070d2d0d0 | |||
| acf90cd177 | |||
| 556dd6fb07 | |||
| a459c41b53 | |||
| 2b97845b79 | |||
| 00d3b72649 | |||
| e238422310 | |||
| dac99e9e00 | |||
| 2bde7f7a73 | |||
| dbc44f1d17 | |||
| 0fa22de098 | |||
| 882103bb14 | |||
| 5fbd84b039 | |||
| 745e88df69 | |||
| 7f6b86b700 | |||
| 8cfb409c2f | |||
| e9df1f28f0 | |||
| 89874d7cdf | |||
| adfc71e451 | |||
| a32ffbe6e0 | |||
| 7f94f80256 | |||
| be71610215 | |||
| b0ebf0f7a0 | |||
| a1b042e7f7 | |||
| 540fa85278 | |||
| 9dac05e25c | |||
| fd20e7fad9 | |||
| e81e60189b | |||
| 3a0e6e43b4 | |||
| b81b1ba727 | |||
| 2a63a022e0 | |||
| 5b6f4e50ba | |||
| 69072ac60f | |||
| 255cb98d9d | |||
| 1e03d5fa01 | |||
| b32cc9a6d6 | |||
| faf3648a78 | |||
| 5d48412f72 | |||
| ddabbc2fee | |||
| 5303979097 | |||
| 724f4d762f | |||
| 2fed89539a | |||
| 5c55f276f1 | |||
| c3a37645fb | |||
| 8aa987f71b | |||
| 987caef5de | |||
| ca42c9ec16 | |||
| 4d79af9513 | |||
| 17cbb1ad2a | |||
| e0bc1f18e7 | |||
| 49125becf3 | |||
| d3ea65baf3 | |||
| a37a0d7d75 | |||
| 930e186e58 | |||
| cdfe6abfc3 | |||
| 5fe389c571 | |||
| 11c18b5e23 | |||
| 1aff534556 | |||
| d9e3564fa2 | |||
| 20192fd754 | |||
| cd59e65744 | |||
| dbff2ae39c | |||
| 365379551a |
@@ -2,8 +2,8 @@ name: L10n
|
||||
on:
|
||||
workflow_dispatch:
|
||||
concurrency:
|
||||
group: l10n
|
||||
cancel-in-progress: true
|
||||
group: codegen
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
@@ -36,14 +36,21 @@ jobs:
|
||||
- name: Install xgotext
|
||||
run: |
|
||||
go install github.com/leonelquinteros/gotext/cli/xgotext@latest
|
||||
- name: Generate pot files
|
||||
- name: Generate pot files and commit
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
~/go/bin/xgotext -default backend -pkg-tree ./cmd/ace -out ./pkg/embed/locales
|
||||
cd web && pnpm run gettext:extract
|
||||
- uses: stefanzweifel/git-auto-commit-action@v7
|
||||
name: Commit changes
|
||||
with:
|
||||
commit_message: "chore(l10n): update pot files"
|
||||
(cd web && pnpm run gettext:extract)
|
||||
# extract 会连带重写译文 po,但它们由 Crowdin 托管且折行格式不同,还原以免格式差异淹没真实翻译
|
||||
find web/src/locales -name '*.po' ! -name 'en.po' -exec git checkout -- {} +
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "pot 文件无变更,跳过提交"
|
||||
exit 0
|
||||
fi
|
||||
git add -A
|
||||
git commit -m "chore(l10n): update pot files"
|
||||
git push origin HEAD:main
|
||||
- name: Sync with Crowdin
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
|
||||
@@ -3,6 +3,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
concurrency:
|
||||
group: codegen
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
@@ -19,11 +22,15 @@ jobs:
|
||||
- name: Install Mockery
|
||||
run: |
|
||||
go install github.com/vektra/mockery/v2@latest
|
||||
- name: Generate Mocks
|
||||
- name: Generate mocks and commit
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
~/go/bin/mockery
|
||||
git pull
|
||||
- uses: stefanzweifel/git-auto-commit-action@v7
|
||||
name: Commit changes
|
||||
with:
|
||||
commit_message: "chore: update mocks"
|
||||
if [ -z "$(git status --porcelain)" ]; then
|
||||
echo "mock 无变更,跳过提交"
|
||||
exit 0
|
||||
fi
|
||||
git add -A
|
||||
git commit -m "chore: update mocks"
|
||||
git push origin HEAD:main
|
||||
|
||||
+154
-723
@@ -8,7 +8,6 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/acewaf"
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
@@ -57,777 +56,221 @@ import (
|
||||
// Injectors from wire.go:
|
||||
|
||||
func initAce() (*app.Ace, func(), error) {
|
||||
acewafApp, err := acewaf.NewApp()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
config, err := bootstrap.NewConf()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
apacheApp, err := apache.NewApp(locale)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
locale := bootstrap.NewT(config)
|
||||
apacheApp := apache.NewApp(locale)
|
||||
db, err := bootstrap.NewDB(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseServerRepo, err := data.NewDatabaseServerRepo(db)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
settingRepo, err := data.NewSettingRepo(config, db)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
clickhouseApp, err := clickhouse.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
codeserverApp, err := codeserver.NewApp()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
dockerApp, err := docker.NewApp()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
elasticsearchApp, err := elasticsearch.NewApp(locale)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteRepo, err := data.NewWebsiteRepo(db, locale, settingRepo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
fail2banApp, err := fail2ban.NewApp(locale, websiteRepo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
frpApp, err := frp.NewApp()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
giteaApp, err := gitea.NewApp()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
grafanaApp, err := grafana.NewApp(locale)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
kafkaApp, err := kafka.NewApp(locale)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mysqlApp, err := mysql.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mariadbApp, err := mariadb.NewApp(mysqlApp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
memcachedApp, err := memcached.NewApp(locale)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
minioApp, err := minio.NewApp()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mongodbApp, err := mongodb.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
nginxApp, err := nginx.NewApp(locale)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
openrestyApp, err := openresty.NewApp(nginxApp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
opensearchApp, err := opensearch.NewApp(locale)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
perconaApp, err := percona.NewApp(mysqlApp)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pgadminApp, err := pgadmin.NewApp(config, locale, databaseServerRepo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
phpmyadminApp, err := phpmyadmin.NewApp(config, locale, databaseServerRepo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
podmanApp, err := podman.NewApp()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
postgresqlApp, err := postgresql.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(db)
|
||||
settingRepo := data.NewSettingRepo(config, db)
|
||||
clickhouseApp := clickhouse.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
codeserverApp := codeserver.NewApp()
|
||||
dockerApp := docker.NewApp()
|
||||
elasticsearchApp := elasticsearch.NewApp(locale)
|
||||
websiteRepo := data.NewWebsiteRepo(db, locale, settingRepo)
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp(locale)
|
||||
giteaApp := gitea.NewApp()
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
kafkaApp := kafka.NewApp(locale)
|
||||
logger, cleanup, err := bootstrap.NewLogger(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
slogLogger := bootstrap.NewSlog(logger)
|
||||
notifyChannelRepo, err := data.NewNotifyChannelRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
notifyUsecase, err := biz.NewNotifyUsecase(locale, slogLogger, notifyChannelRepo, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
taskRunner, err := bootstrap.NewRunner(notifyUsecase, db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
taskRepo, err := data.NewTaskRepo(db, locale, slogLogger, taskRunner)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
prometheusApp, err := prometheus.NewApp(config, locale, taskRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
pureftpdApp, err := pureftpd.NewApp(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
redisApp, err := redis.NewApp(locale, databaseServerRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
rocketmqApp, err := rocketmq.NewApp(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
rsyncApp, err := rsync.NewApp(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
s3fsApp, err := s3fs.NewApp(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
supervisorApp, err := supervisor.NewApp(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
valkeyApp, err := valkey.NewApp(locale, databaseServerRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
loader, err := bootstrap.NewLoader(acewafApp, apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, pgadminApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
notifyChannelRepo := data.NewNotifyChannelRepo(db)
|
||||
notifyUsecase := biz.NewNotifyUsecase(locale, slogLogger, notifyChannelRepo, settingRepo)
|
||||
taskRunner := bootstrap.NewRunner(notifyUsecase, db, locale, slogLogger)
|
||||
taskRepo := data.NewTaskRepo(db, locale, slogLogger, taskRunner)
|
||||
mysqlApp := mysql.NewApp(locale, databaseServerRepo, settingRepo, taskRepo)
|
||||
mariadbApp := mariadb.NewApp(mysqlApp)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
mongodbApp := mongodb.NewApp(locale, databaseServerRepo, settingRepo)
|
||||
nginxApp := nginx.NewApp(locale)
|
||||
openrestyApp := openresty.NewApp(nginxApp)
|
||||
opensearchApp := opensearch.NewApp(locale)
|
||||
perconaApp := percona.NewApp(mysqlApp)
|
||||
pgadminApp := pgadmin.NewApp(config, locale, databaseServerRepo)
|
||||
phpmyadminApp := phpmyadmin.NewApp(config, locale, databaseServerRepo)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, config, databaseServerRepo, settingRepo, taskRepo)
|
||||
prometheusApp := prometheus.NewApp(config, locale, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo, taskRepo)
|
||||
rocketmqApp := rocketmq.NewApp(locale)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
valkeyApp := valkey.NewApp(locale, databaseServerRepo, taskRepo)
|
||||
loader := bootstrap.NewLoader(apacheApp, clickhouseApp, codeserverApp, dockerApp, elasticsearchApp, fail2banApp, frpApp, giteaApp, grafanaApp, kafkaApp, mariadbApp, memcachedApp, minioApp, mongodbApp, mysqlApp, nginxApp, openrestyApp, opensearchApp, perconaApp, pgadminApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rocketmqApp, rsyncApp, s3fsApp, supervisorApp, valkeyApp)
|
||||
manager, err := bootstrap.NewSession(config, db, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
appRepo, err := data.NewAppRepo(config, db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userTokenRepo, err := data.NewUserTokenRepo(config, db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
appRepo := data.NewAppRepo(config, db, locale, slogLogger)
|
||||
userTokenRepo := data.NewUserTokenRepo(config, db, locale)
|
||||
middlewares, err := middleware.NewMiddlewares(config, locale, manager, appRepo, userTokenRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
validator, err := bootstrap.NewValidator(config, db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
validator := bootstrap.NewValidator(config, db)
|
||||
alertRepo, err := data.NewAlertRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerRepo, err := data.NewContainerRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
alertUsecase, err := biz.NewAlertUsecase(notifyUsecase, loader, locale, slogLogger, alertRepo, appRepo, containerRepo, databaseServerRepo, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
alertService, err := service.NewAlertService(alertUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cacheRepo, err := data.NewCacheRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
appUsecase, err := biz.NewAppUsecase(locale, appRepo, cacheRepo, taskRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerRepo := data.NewContainerRepo()
|
||||
alertUsecase := biz.NewAlertUsecase(notifyUsecase, loader, locale, slogLogger, alertRepo, appRepo, containerRepo, databaseServerRepo, settingRepo)
|
||||
alertService := service.NewAlertService(alertUsecase)
|
||||
cacheRepo := data.NewCacheRepo(db)
|
||||
appUsecase := biz.NewAppUsecase(locale, appRepo, cacheRepo, taskRepo)
|
||||
cacheUsecase := biz.NewCacheUsecase(cacheRepo)
|
||||
settingUsecase, err := biz.NewSettingUsecase(locale, slogLogger, settingRepo, taskRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
appService, err := service.NewAppService(loader, appUsecase, cacheUsecase, settingUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupRepo, err := data.NewBackupRepo(config, db, locale, slogLogger, settingRepo, websiteRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupUsecase, err := biz.NewBackupUsecase(notifyUsecase, locale, slogLogger, backupRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
settingUsecase := biz.NewSettingUsecase(locale, slogLogger, settingRepo, taskRepo)
|
||||
appService := service.NewAppService(loader, appUsecase, cacheUsecase, settingUsecase, locale)
|
||||
backupRepo := data.NewBackupRepo(config, db, locale, slogLogger, settingRepo, websiteRepo)
|
||||
backupUsecase := biz.NewBackupUsecase(notifyUsecase, locale, slogLogger, backupRepo)
|
||||
taskUsecase := biz.NewTaskUsecase(taskRepo)
|
||||
backupService, err := service.NewBackupService(backupUsecase, taskUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupAccountRepo, err := data.NewBackupAccountRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupAccountUsecase, err := biz.NewBackupAccountUsecase(locale, slogLogger, backupAccountRepo, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupStorageService, err := service.NewBackupStorageService(backupAccountUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certRepo, err := data.NewCertRepo(db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certUsecase, err := biz.NewCertUsecase(locale, slogLogger, certRepo, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certService, err := service.NewCertService(certUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certAccountRepo, err := data.NewCertAccountRepo(db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userRepo, err := data.NewUserRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certAccountUsecase, err := biz.NewCertAccountUsecase(locale, slogLogger, certAccountRepo, userRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certAccountService, err := service.NewCertAccountService(certAccountUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certDNSRepo, err := data.NewCertDNSRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupService := service.NewBackupService(backupUsecase, taskUsecase, locale)
|
||||
backupAccountRepo := data.NewBackupAccountRepo(db)
|
||||
backupAccountUsecase := biz.NewBackupAccountUsecase(locale, slogLogger, backupAccountRepo, settingRepo)
|
||||
backupStorageService := service.NewBackupStorageService(backupAccountUsecase, locale)
|
||||
certRepo := data.NewCertRepo(db, locale, slogLogger)
|
||||
certUsecase := biz.NewCertUsecase(locale, slogLogger, certRepo, settingRepo)
|
||||
certService := service.NewCertService(certUsecase, locale)
|
||||
certAccountRepo := data.NewCertAccountRepo(db, locale, slogLogger)
|
||||
userRepo := data.NewUserRepo(db, locale)
|
||||
certAccountUsecase := biz.NewCertAccountUsecase(locale, slogLogger, certAccountRepo, userRepo)
|
||||
certAccountService := service.NewCertAccountService(certAccountUsecase)
|
||||
certDNSRepo := data.NewCertDNSRepo(db)
|
||||
certDNSUsecase := biz.NewCertDNSUsecase(certDNSRepo, slogLogger)
|
||||
certDNSService, err := service.NewCertDNSService(certDNSUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerUsecase := biz.NewContainerUsecase(containerRepo, settingRepo)
|
||||
containerService, err := service.NewContainerService(containerUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerComposeRepo, err := data.NewContainerComposeRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certDNSService := service.NewCertDNSService(certDNSUsecase)
|
||||
containerUsecase := biz.NewContainerUsecase(locale, containerRepo, settingRepo, taskRepo)
|
||||
containerService := service.NewContainerService(containerUsecase)
|
||||
containerComposeRepo := data.NewContainerComposeRepo()
|
||||
containerComposeUsecase := biz.NewContainerComposeUsecase(containerComposeRepo)
|
||||
containerComposeService, err := service.NewContainerComposeService(containerComposeUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerImageRepo, err := data.NewContainerImageRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerImageUsecase := biz.NewContainerImageUsecase(containerImageRepo, settingRepo)
|
||||
containerImageService, err := service.NewContainerImageService(containerImageUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerNetworkRepo, err := data.NewContainerNetworkRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerComposeService := service.NewContainerComposeService(containerComposeUsecase)
|
||||
containerImageRepo := data.NewContainerImageRepo()
|
||||
containerImageUsecase := biz.NewContainerImageUsecase(locale, containerImageRepo, settingRepo, taskRepo)
|
||||
containerImageService := service.NewContainerImageService(containerImageUsecase)
|
||||
containerNetworkRepo := data.NewContainerNetworkRepo()
|
||||
containerNetworkUsecase := biz.NewContainerNetworkUsecase(containerNetworkRepo, settingRepo)
|
||||
containerNetworkService, err := service.NewContainerNetworkService(containerNetworkUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerVolumeRepo, err := data.NewContainerVolumeRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerNetworkService := service.NewContainerNetworkService(containerNetworkUsecase)
|
||||
containerVolumeRepo := data.NewContainerVolumeRepo()
|
||||
containerVolumeUsecase := biz.NewContainerVolumeUsecase(containerVolumeRepo, settingRepo)
|
||||
containerVolumeService, err := service.NewContainerVolumeService(containerVolumeUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cronRepo, err := data.NewCronRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
containerVolumeService := service.NewContainerVolumeService(containerVolumeUsecase)
|
||||
cronRepo := data.NewCronRepo(db, locale)
|
||||
cronUsecase := biz.NewCronUsecase(cronRepo, slogLogger)
|
||||
cronService, err := service.NewCronService(cronUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUserRepo, err := data.NewDatabaseUserRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUserUsecase, err := biz.NewDatabaseUserUsecase(slogLogger, databaseServerRepo, databaseUserRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseRepo, err := data.NewDatabaseRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUsecase, err := biz.NewDatabaseUsecase(databaseUserUsecase, locale, slogLogger, databaseRepo, databaseServerRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseService, err := service.NewDatabaseService(databaseUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseElasticsearchRepo, err := data.NewDatabaseElasticsearchRepo(db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cronService := service.NewCronService(cronUsecase)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(db)
|
||||
databaseUserUsecase := biz.NewDatabaseUserUsecase(slogLogger, databaseServerRepo, databaseUserRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(db)
|
||||
databaseUsecase := biz.NewDatabaseUsecase(databaseUserUsecase, locale, slogLogger, databaseRepo, databaseServerRepo)
|
||||
databaseService := service.NewDatabaseService(databaseUsecase)
|
||||
databaseElasticsearchRepo := data.NewDatabaseElasticsearchRepo(db, locale, slogLogger)
|
||||
databaseElasticsearchUsecase := biz.NewDatabaseElasticsearchUsecase(databaseElasticsearchRepo)
|
||||
databaseElasticsearchService, err := service.NewDatabaseElasticsearchService(databaseElasticsearchUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseRedisRepo, err := data.NewDatabaseRedisRepo(db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseElasticsearchService := service.NewDatabaseElasticsearchService(databaseElasticsearchUsecase)
|
||||
databaseRedisRepo := data.NewDatabaseRedisRepo(db, locale, slogLogger)
|
||||
databaseRedisUsecase := biz.NewDatabaseRedisUsecase(databaseRedisRepo)
|
||||
databaseRedisService, err := service.NewDatabaseRedisService(databaseRedisUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseServerUsecase, err := biz.NewDatabaseServerUsecase(locale, slogLogger, databaseServerRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseServerService, err := service.NewDatabaseServerService(databaseServerUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUserService, err := service.NewDatabaseUserService(databaseUserUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentRepo, err := data.NewEnvironmentRepo(config, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentUsecase, err := biz.NewEnvironmentUsecase(locale, cacheRepo, environmentRepo, taskRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentService, err := service.NewEnvironmentService(environmentUsecase, taskUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentDotnetService, err := service.NewEnvironmentDotnetService(environmentUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentGoService, err := service.NewEnvironmentGoService(environmentUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentJavaService, err := service.NewEnvironmentJavaService(environmentUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentNodejsService, err := service.NewEnvironmentNodejsService(environmentUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentPHPService, err := service.NewEnvironmentPHPService(environmentUsecase, taskUsecase, config, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
environmentPythonService, err := service.NewEnvironmentPythonService(environmentUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseRedisService := service.NewDatabaseRedisService(databaseRedisUsecase)
|
||||
databaseServerUsecase := biz.NewDatabaseServerUsecase(locale, slogLogger, databaseServerRepo)
|
||||
databaseServerService := service.NewDatabaseServerService(databaseServerUsecase)
|
||||
databaseUserService := service.NewDatabaseUserService(databaseUserUsecase)
|
||||
environmentRepo := data.NewEnvironmentRepo(config, locale)
|
||||
environmentUsecase := biz.NewEnvironmentUsecase(locale, cacheRepo, environmentRepo, taskRepo)
|
||||
environmentService := service.NewEnvironmentService(environmentUsecase, taskUsecase, locale)
|
||||
environmentDotnetService := service.NewEnvironmentDotnetService(environmentUsecase, locale)
|
||||
environmentGoService := service.NewEnvironmentGoService(environmentUsecase, locale)
|
||||
environmentJavaService := service.NewEnvironmentJavaService(environmentUsecase, locale)
|
||||
environmentNodejsService := service.NewEnvironmentNodejsService(environmentUsecase, locale)
|
||||
environmentPHPService := service.NewEnvironmentPHPService(environmentUsecase, taskUsecase, config, locale)
|
||||
environmentPythonService := service.NewEnvironmentPythonService(environmentUsecase, locale)
|
||||
tamperRepo, err := data.NewTamperRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperUsecase, err := biz.NewTamperUsecase(notifyUsecase, settingUsecase, locale, slogLogger, tamperRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
fileService, err := service.NewFileService(containerUsecase, tamperUsecase, taskUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
fileShareRepo, err := data.NewFileShareRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
fileShareUsecase, err := biz.NewFileShareUsecase(slogLogger, fileShareRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
fileShareService, err := service.NewFileShareService(fileShareUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
firewallService, err := service.NewFirewallService(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperUsecase := biz.NewTamperUsecase(notifyUsecase, settingUsecase, locale, slogLogger, tamperRepo)
|
||||
fileService := service.NewFileService(containerUsecase, tamperUsecase, taskUsecase, locale)
|
||||
fileShareRepo := data.NewFileShareRepo(db, locale)
|
||||
fileShareUsecase := biz.NewFileShareUsecase(slogLogger, fileShareRepo)
|
||||
fileShareService := service.NewFileShareService(fileShareUsecase, locale)
|
||||
firewallService := service.NewFirewallService(locale)
|
||||
scanEventRepo, err := data.NewScanEventRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
scanEventUsecase := biz.NewScanEventUsecase(scanEventRepo, settingRepo)
|
||||
firewallScanService, err := service.NewFirewallScanService(scanEventUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
projectRepo, err := data.NewProjectRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
projectUsecase, err := biz.NewProjectUsecase(locale, slogLogger, projectRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
firewallScanService := service.NewFirewallScanService(scanEventUsecase)
|
||||
projectRepo := data.NewProjectRepo(db, locale)
|
||||
projectUsecase := biz.NewProjectUsecase(locale, slogLogger, projectRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteStatUsecase := biz.NewWebsiteStatUsecase(websiteStatRepo)
|
||||
websiteUsecase, err := biz.NewWebsiteUsecase(certAccountUsecase, certUsecase, databaseUsecase, databaseUserUsecase, tamperUsecase, websiteStatUsecase, locale, slogLogger, databaseServerRepo, websiteRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
homeService, err := service.NewHomeService(appUsecase, backupUsecase, containerUsecase, cronUsecase, databaseServerUsecase, environmentUsecase, projectUsecase, settingUsecase, taskUsecase, websiteUsecase, config, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
logRepo, err := data.NewLogRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteUsecase := biz.NewWebsiteUsecase(certAccountUsecase, certUsecase, databaseUsecase, databaseUserUsecase, tamperUsecase, websiteStatUsecase, locale, slogLogger, databaseServerRepo, websiteRepo)
|
||||
homeService := service.NewHomeService(appUsecase, backupUsecase, containerUsecase, cronUsecase, databaseServerUsecase, environmentUsecase, projectUsecase, settingUsecase, taskUsecase, websiteUsecase, config, locale)
|
||||
logRepo := data.NewLogRepo(db)
|
||||
logUsecase := biz.NewLogUsecase(logRepo)
|
||||
logService, err := service.NewLogService(logUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
monitorRepo, err := data.NewMonitorRepo(db)
|
||||
logService := service.NewLogService(logUsecase, locale)
|
||||
monitorRepo, err := data.NewMonitorRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
monitorUsecase := biz.NewMonitorUsecase(monitorRepo, settingRepo)
|
||||
monitorService, err := service.NewMonitorService(monitorUsecase, settingUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
notifyService, err := service.NewNotifyService(notifyUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
processService, err := service.NewProcessService()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
projectService, err := service.NewProjectService(projectUsecase, settingUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
safeRepo, err := data.NewSafeRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
monitorService := service.NewMonitorService(monitorUsecase, settingUsecase)
|
||||
notifyService := service.NewNotifyService(notifyUsecase)
|
||||
processService := service.NewProcessService()
|
||||
projectService := service.NewProjectService(projectUsecase, settingUsecase)
|
||||
safeRepo := data.NewSafeRepo()
|
||||
safeUsecase := biz.NewSafeUsecase(safeRepo, slogLogger)
|
||||
safeService, err := service.NewSafeService(safeUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
settingService, err := service.NewSettingService(certAccountUsecase, certUsecase, settingUsecase, db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
sshRepo, err := data.NewSSHRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
safeService := service.NewSafeService(safeUsecase)
|
||||
settingService := service.NewSettingService(certAccountUsecase, certUsecase, settingUsecase, db, locale)
|
||||
sshRepo := data.NewSSHRepo(db, locale)
|
||||
sshUsecase := biz.NewSSHUsecase(sshRepo, slogLogger)
|
||||
sshService, err := service.NewSSHService(sshUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
systemctlService, err := service.NewSystemctlService(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperService, err := service.NewTamperService(tamperUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
taskService, err := service.NewTaskService(taskUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
templateRepo, err := data.NewTemplateRepo(slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
templateUsecase, err := biz.NewTemplateUsecase(locale, cacheRepo, templateRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
templateService, err := service.NewTemplateService(settingUsecase, templateUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
toolboxBenchmarkService, err := service.NewToolboxBenchmarkService(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
toolboxDiskService, err := service.NewToolboxDiskService(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
toolboxLogService, err := service.NewToolboxLogService(containerImageUsecase, settingUsecase, db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
toolboxMigrationService, err := service.NewToolboxMigrationService(appUsecase, databaseServerUsecase, databaseUsecase, databaseUserUsecase, environmentUsecase, projectUsecase, settingUsecase, websiteUsecase, config, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
toolboxNetworkService, err := service.NewToolboxNetworkService()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
toolboxSSHService, err := service.NewToolboxSSHService(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
toolboxSystemService, err := service.NewToolboxSystemService(locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userUsecase, err := biz.NewUserUsecase(locale, slogLogger, userRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userService, err := service.NewUserService(notifyUsecase, userUsecase, config, locale, manager)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userPasskeyRepo, err := data.NewUserPasskeyRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
sshService := service.NewSSHService(sshUsecase)
|
||||
systemctlService := service.NewSystemctlService(locale)
|
||||
tamperService := service.NewTamperService(tamperUsecase, locale)
|
||||
taskService := service.NewTaskService(taskUsecase)
|
||||
templateRepo := data.NewTemplateRepo(slogLogger)
|
||||
templateUsecase := biz.NewTemplateUsecase(locale, cacheRepo, templateRepo)
|
||||
templateService := service.NewTemplateService(settingUsecase, templateUsecase, locale)
|
||||
toolboxBenchmarkService := service.NewToolboxBenchmarkService(locale)
|
||||
toolboxDiskService := service.NewToolboxDiskService(locale)
|
||||
toolboxLogService := service.NewToolboxLogService(containerImageUsecase, settingUsecase, db, locale)
|
||||
migrationSourceRepo := data.NewMigrationSourceRepo(locale)
|
||||
migrationRemoteRepo := data.NewMigrationRemoteRepo(locale)
|
||||
migrationArchiveRepo := data.NewMigrationArchiveRepo()
|
||||
toolboxMigrationUsecase := biz.NewToolboxMigrationUsecase(locale, slogLogger, migrationSourceRepo, migrationRemoteRepo, migrationArchiveRepo, settingUsecase, websiteUsecase, databaseUsecase, databaseServerUsecase, databaseUserUsecase, backupUsecase, projectUsecase, appUsecase, environmentUsecase)
|
||||
toolboxMigrationService := service.NewToolboxMigrationService(toolboxMigrationUsecase, config, locale, slogLogger)
|
||||
toolboxNetworkService := service.NewToolboxNetworkService(locale)
|
||||
toolboxSSHService := service.NewToolboxSSHService(locale)
|
||||
toolboxSystemService := service.NewToolboxSystemService(locale)
|
||||
userUsecase := biz.NewUserUsecase(locale, slogLogger, userRepo)
|
||||
userService := service.NewUserService(notifyUsecase, userUsecase, config, locale, manager)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
userPasskeyUsecase := biz.NewUserPasskeyUsecase(userPasskeyRepo)
|
||||
userPasskeyService, err := service.NewUserPasskeyService(notifyUsecase, userPasskeyUsecase, userUsecase, config, locale, manager)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userPasskeyService := service.NewUserPasskeyService(notifyUsecase, userPasskeyUsecase, userUsecase, config, locale, manager)
|
||||
userTokenUsecase := biz.NewUserTokenUsecase(userTokenRepo)
|
||||
userTokenService, err := service.NewUserTokenService(userTokenUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
webHookRepo, err := data.NewWebHookRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
webHookUsecase, err := biz.NewWebHookUsecase(locale, slogLogger, webHookRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
webHookService, err := service.NewWebHookService(webHookUsecase)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteService, err := service.NewWebsiteService(settingUsecase, websiteUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userTokenService := service.NewUserTokenService(userTokenUsecase, locale)
|
||||
webHookRepo := data.NewWebHookRepo(db, locale)
|
||||
webHookUsecase := biz.NewWebHookUsecase(locale, slogLogger, webHookRepo)
|
||||
webHookService := service.NewWebHookService(webHookUsecase)
|
||||
websiteService := service.NewWebsiteService(settingUsecase, websiteUsecase, locale)
|
||||
aggregator := websitestat.NewAggregator()
|
||||
websiteStatService, err := service.NewWebsiteStatService(settingUsecase, websiteStatUsecase, websiteUsecase, aggregator)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
wafRepo, err := data.NewWafRepo(locale, db, slogLogger, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
wafUsecase := biz.NewWafUsecase(wafRepo)
|
||||
wafService, err := service.NewWafService(wafUsecase, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
wsService, err := service.NewWsService(backupUsecase, certUsecase, sshUsecase, settingUsecase, taskUsecase, config, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteStatService := service.NewWebsiteStatService(settingUsecase, websiteStatUsecase, websiteUsecase, aggregator)
|
||||
wsService := service.NewWsService(backupUsecase, certUsecase, sshUsecase, settingUsecase, taskUsecase, config, locale, slogLogger)
|
||||
services := &route.Services{
|
||||
Alert: alertService,
|
||||
App: appService,
|
||||
@@ -884,7 +327,6 @@ func initAce() (*app.Ace, func(), error) {
|
||||
WebHook: webHookService,
|
||||
Website: websiteService,
|
||||
WebsiteStat: websiteStatService,
|
||||
Waf: wafService,
|
||||
Ws: wsService,
|
||||
}
|
||||
v := route.NewEndpoints(services)
|
||||
@@ -900,6 +342,7 @@ func initAce() (*app.Ace, func(), error) {
|
||||
Cert: certUsecase,
|
||||
CertAccount: certAccountUsecase,
|
||||
FileShare: fileShareUsecase,
|
||||
Monitor: monitorUsecase,
|
||||
Notify: notifyUsecase,
|
||||
ScanEvent: scanEventUsecase,
|
||||
Setting: settingUsecase,
|
||||
@@ -919,26 +362,14 @@ func initAce() (*app.Ace, func(), error) {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
gormigrate, err := bootstrap.NewMigrate(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
reloader, err := bootstrap.NewTLSReloader(config)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
server, err := bootstrap.NewHttp(mux, config, reloader)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
ace, err := app.NewAce(mux, config, cron, gormigrate, server, reloader, taskRunner)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
server := bootstrap.NewHttp(mux, config, reloader)
|
||||
ace := app.NewAce(mux, config, cron, gormigrate, server, reloader, taskRunner)
|
||||
return ace, func() {
|
||||
cleanup()
|
||||
}, nil
|
||||
|
||||
+34
-164
@@ -26,10 +26,7 @@ func initCli() (*app.Cli, func(), error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
locale, err := bootstrap.NewT(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
locale := bootstrap.NewT(config)
|
||||
db, err := bootstrap.NewDB(config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -39,181 +36,54 @@ func initCli() (*app.Cli, func(), error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
slogLogger := bootstrap.NewSlog(logger)
|
||||
appRepo, err := data.NewAppRepo(config, db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cacheRepo, err := data.NewCacheRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
notifyChannelRepo, err := data.NewNotifyChannelRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
settingRepo, err := data.NewSettingRepo(config, db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
notifyUsecase, err := biz.NewNotifyUsecase(locale, slogLogger, notifyChannelRepo, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
taskRunner, err := bootstrap.NewRunner(notifyUsecase, db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
taskRepo, err := data.NewTaskRepo(db, locale, slogLogger, taskRunner)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
appUsecase, err := biz.NewAppUsecase(locale, appRepo, cacheRepo, taskRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteRepo, err := data.NewWebsiteRepo(db, locale, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupRepo, err := data.NewBackupRepo(config, db, locale, slogLogger, settingRepo, websiteRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
backupUsecase, err := biz.NewBackupUsecase(notifyUsecase, locale, slogLogger, backupRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
appRepo := data.NewAppRepo(config, db, locale, slogLogger)
|
||||
cacheRepo := data.NewCacheRepo(db)
|
||||
notifyChannelRepo := data.NewNotifyChannelRepo(db)
|
||||
settingRepo := data.NewSettingRepo(config, db)
|
||||
notifyUsecase := biz.NewNotifyUsecase(locale, slogLogger, notifyChannelRepo, settingRepo)
|
||||
taskRunner := bootstrap.NewRunner(notifyUsecase, db, locale, slogLogger)
|
||||
taskRepo := data.NewTaskRepo(db, locale, slogLogger, taskRunner)
|
||||
appUsecase := biz.NewAppUsecase(locale, appRepo, cacheRepo, taskRepo)
|
||||
websiteRepo := data.NewWebsiteRepo(db, locale, settingRepo)
|
||||
backupRepo := data.NewBackupRepo(config, db, locale, slogLogger, settingRepo, websiteRepo)
|
||||
backupUsecase := biz.NewBackupUsecase(notifyUsecase, locale, slogLogger, backupRepo)
|
||||
cacheUsecase := biz.NewCacheUsecase(cacheRepo)
|
||||
certAccountRepo, err := data.NewCertAccountRepo(db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userRepo, err := data.NewUserRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certAccountUsecase, err := biz.NewCertAccountUsecase(locale, slogLogger, certAccountRepo, userRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certRepo, err := data.NewCertRepo(db, locale, slogLogger)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certUsecase, err := biz.NewCertUsecase(locale, slogLogger, certRepo, settingRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cronRepo, err := data.NewCronRepo(db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
certAccountRepo := data.NewCertAccountRepo(db, locale, slogLogger)
|
||||
userRepo := data.NewUserRepo(db, locale)
|
||||
certAccountUsecase := biz.NewCertAccountUsecase(locale, slogLogger, certAccountRepo, userRepo)
|
||||
certRepo := data.NewCertRepo(db, locale, slogLogger)
|
||||
certUsecase := biz.NewCertUsecase(locale, slogLogger, certRepo, settingRepo)
|
||||
cronRepo := data.NewCronRepo(db, locale)
|
||||
cronUsecase := biz.NewCronUsecase(cronRepo, slogLogger)
|
||||
databaseServerRepo, err := data.NewDatabaseServerRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseServerUsecase, err := biz.NewDatabaseServerUsecase(locale, slogLogger, databaseServerRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
settingUsecase, err := biz.NewSettingUsecase(locale, slogLogger, settingRepo, taskRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userPasskeyRepo, err := data.NewUserPasskeyRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseServerRepo := data.NewDatabaseServerRepo(db)
|
||||
databaseServerUsecase := biz.NewDatabaseServerUsecase(locale, slogLogger, databaseServerRepo)
|
||||
settingUsecase := biz.NewSettingUsecase(locale, slogLogger, settingRepo, taskRepo)
|
||||
userPasskeyRepo := data.NewUserPasskeyRepo(db)
|
||||
userPasskeyUsecase := biz.NewUserPasskeyUsecase(userPasskeyRepo)
|
||||
userUsecase, err := biz.NewUserUsecase(locale, slogLogger, userRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUserRepo, err := data.NewDatabaseUserRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUserUsecase, err := biz.NewDatabaseUserUsecase(slogLogger, databaseServerRepo, databaseUserRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseRepo, err := data.NewDatabaseRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
databaseUsecase, err := biz.NewDatabaseUsecase(databaseUserUsecase, locale, slogLogger, databaseRepo, databaseServerRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
userUsecase := biz.NewUserUsecase(locale, slogLogger, userRepo)
|
||||
databaseUserRepo := data.NewDatabaseUserRepo(db)
|
||||
databaseUserUsecase := biz.NewDatabaseUserUsecase(slogLogger, databaseServerRepo, databaseUserRepo)
|
||||
databaseRepo := data.NewDatabaseRepo(db)
|
||||
databaseUsecase := biz.NewDatabaseUsecase(databaseUserUsecase, locale, slogLogger, databaseRepo, databaseServerRepo)
|
||||
tamperRepo, err := data.NewTamperRepo(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperUsecase, err := biz.NewTamperUsecase(notifyUsecase, settingUsecase, locale, slogLogger, tamperRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
tamperUsecase := biz.NewTamperUsecase(notifyUsecase, settingUsecase, locale, slogLogger, tamperRepo)
|
||||
websiteStatRepo, err := data.NewWebsiteStatRepo()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteStatUsecase := biz.NewWebsiteStatUsecase(websiteStatRepo)
|
||||
websiteUsecase, err := biz.NewWebsiteUsecase(certAccountUsecase, certUsecase, databaseUsecase, databaseUserUsecase, tamperUsecase, websiteStatUsecase, locale, slogLogger, databaseServerRepo, websiteRepo)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cliService, err := service.NewCliService(appUsecase, backupUsecase, cacheUsecase, certAccountUsecase, certUsecase, cronUsecase, databaseServerUsecase, notifyUsecase, settingUsecase, userPasskeyUsecase, userUsecase, websiteUsecase, config, db, locale)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
websiteUsecase := biz.NewWebsiteUsecase(certAccountUsecase, certUsecase, databaseUsecase, databaseUserUsecase, tamperUsecase, websiteStatUsecase, locale, slogLogger, databaseServerRepo, websiteRepo)
|
||||
validator := bootstrap.NewValidator(config, db)
|
||||
cliService := service.NewCliService(appUsecase, backupUsecase, cacheUsecase, certAccountUsecase, certUsecase, cronUsecase, databaseServerUsecase, notifyUsecase, settingUsecase, userPasskeyUsecase, userUsecase, websiteUsecase, config, db, locale, validator)
|
||||
v := command.Commands(locale, cliService)
|
||||
cliCommand, err := bootstrap.NewCli(locale, v)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
gormigrate, err := bootstrap.NewMigrate(db)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cli, err := app.NewCli(cliCommand, gormigrate)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return nil, nil, err
|
||||
}
|
||||
cliCommand := bootstrap.NewCli(locale, v)
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
cli := app.NewCli(cliCommand, gormigrate)
|
||||
return cli, func() {
|
||||
cleanup()
|
||||
}, nil
|
||||
|
||||
@@ -12,6 +12,7 @@ require (
|
||||
github.com/coreos/go-systemd/v22 v22.7.0
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/dchest/captcha v1.1.0
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/expr-lang/expr v1.17.8
|
||||
github.com/fsnotify/fsnotify v1.10.1
|
||||
github.com/go-chi/chi/v5 v5.3.1
|
||||
@@ -23,6 +24,7 @@ require (
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/hashicorp/go-version v1.9.0
|
||||
github.com/klauspost/compress v1.19.2
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/libdns/alidns v1.0.7
|
||||
@@ -40,7 +42,7 @@ require (
|
||||
github.com/libtnb/gormstore v1.3.0
|
||||
github.com/libtnb/logrotate v0.1.2
|
||||
github.com/libtnb/sessions v1.5.0
|
||||
github.com/libtnb/sqlite v1.2.1
|
||||
github.com/libtnb/sqlite v1.2.2
|
||||
github.com/libtnb/utils v1.2.2
|
||||
github.com/libtnb/validator v0.4.1
|
||||
github.com/libtnb/validator/contrib/openapi v0.2.0
|
||||
@@ -49,11 +51,12 @@ require (
|
||||
github.com/moby/moby/api v1.55.0
|
||||
github.com/moby/moby/client v0.5.1
|
||||
github.com/orandin/slog-gorm v1.4.0
|
||||
github.com/pelletier/go-toml/v2 v2.4.3
|
||||
github.com/pkg/sftp v1.13.11
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/samber/lo v1.53.0
|
||||
github.com/sethvargo/go-limiter v1.2.0
|
||||
github.com/shirou/gopsutil/v4 v4.26.6
|
||||
github.com/shirou/gopsutil/v4 v4.26.7
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/studio-b12/gowebdav v0.13.0
|
||||
@@ -63,12 +66,13 @@ require (
|
||||
github.com/wneessen/go-mail v0.8.1
|
||||
github.com/xuri/excelize/v2 v2.11.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
golang.org/x/crypto v0.54.0
|
||||
golang.org/x/net v0.57.0
|
||||
golang.org/x/crypto v0.55.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.22.0
|
||||
golang.org/x/sys v0.47.0
|
||||
golang.org/x/term v0.45.0
|
||||
gorm.io/gorm v1.31.2
|
||||
modernc.org/sqlite v1.55.0
|
||||
modernc.org/sqlite v1.56.0
|
||||
resty.dev/v3 v3.0.0-rc.3
|
||||
)
|
||||
|
||||
@@ -80,11 +84,10 @@ require (
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/boyter/go-string v1.0.5 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/ebitengine/purego v0.10.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
@@ -106,7 +109,7 @@ require (
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/libtnb/securecookie v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@@ -135,10 +138,10 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
modernc.org/libc v1.74.1 // indirect
|
||||
golang.org/x/mod v0.38.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/tools v0.48.0 // indirect
|
||||
modernc.org/libc v1.74.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -77,8 +77,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
|
||||
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
@@ -156,8 +156,8 @@ github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:E
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
@@ -223,6 +223,8 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
@@ -276,8 +278,8 @@ github.com/libtnb/securecookie v1.4.0 h1:SkKHO7T5I4aRGV7/6fnYYsleQDnnDzeAmTDA0GM
|
||||
github.com/libtnb/securecookie v1.4.0/go.mod h1:mg1i9HfstsYBGwCfQdU+3Z1GuieyZRAxbkFUnrzchJU=
|
||||
github.com/libtnb/sessions v1.5.0 h1:gvTRkSR8lpp1PB0Nms/kc+gh1BwJ411fxLjnZLYItLs=
|
||||
github.com/libtnb/sessions v1.5.0/go.mod h1:/Q/+lO7DcnJJEU1nL59ScPV/V8ct34F8BjoEMwwxjRw=
|
||||
github.com/libtnb/sqlite v1.2.1 h1:lrhN3yG49pj1gmtMMP6Es0lhKIkzDeqBul2EWOLyjpY=
|
||||
github.com/libtnb/sqlite v1.2.1/go.mod h1:pGq13BirCjkM6ihkWWxs0PGFhBiwUyA0PqdSmgDWfmM=
|
||||
github.com/libtnb/sqlite v1.2.2 h1:Ku5hAPP5B3A4kQcDn4Z3qyNMafKzuAbnFZX4oD9wR4I=
|
||||
github.com/libtnb/sqlite v1.2.2/go.mod h1:JkAuxM7HHo0tc7dQENjIGpp/yIfrLX5nKr5ME9wuvmI=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7 h1:ZzONsNhw69uC/wJQlMj66n1zfsRgKAElfJ//E9G380U=
|
||||
github.com/libtnb/testify v0.0.0-20260406170114-25da2dad39e7/go.mod h1:HeQeTfKU6tj2Lx1z79UacwYeDioo6M4ZD7BDDI6+rrg=
|
||||
github.com/libtnb/utils v1.2.2 h1:HUqgAIb+TIrZPXRytYB5XqzkEXG2U4a4/ZGpprNtL5A=
|
||||
@@ -291,8 +293,8 @@ github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIv
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
@@ -333,6 +335,8 @@ github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsT
|
||||
github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
|
||||
github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -376,8 +380,8 @@ github.com/sethvargo/go-limiter v1.2.0 h1:XKL1vsaQ2zztVJrnZSzpRWCq/aLQqMllJ/3D/0
|
||||
github.com/sethvargo/go-limiter v1.2.0/go.mod h1:RC+qY2R7PAK81mBCrZEJlUlKnXSIqqQ8B7G44UgZ/1E=
|
||||
github.com/shamaton/msgpack/v3 v3.2.0 h1:1q2Ms+MWmuRju+PuDMSFDB7p7621npeX4zprJN5Zck8=
|
||||
github.com/shamaton/msgpack/v3 v3.2.0/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6 h1:Mzr/npDtQC/xpeEuQKHZt8Zo9CmPvhTj8nkR8w5TLDs=
|
||||
github.com/shirou/gopsutil/v4 v4.26.6/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@@ -470,8 +474,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -494,8 +498,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -510,8 +514,8 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -546,8 +550,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -567,8 +571,8 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@@ -612,8 +616,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
|
||||
modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
||||
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
@@ -624,8 +628,8 @@ modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
|
||||
modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
|
||||
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
|
||||
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
@@ -634,8 +638,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
|
||||
modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
|
||||
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
|
||||
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ type Ace struct {
|
||||
runner types.TaskRunner
|
||||
}
|
||||
|
||||
func NewAce(router *chi.Mux, conf *config.Config, cron *cron.Cron, migrator *gormigrate.Gormigrate, server *hlfhr.Server, reloader *tlscert.Reloader, runner types.TaskRunner) (*Ace, error) {
|
||||
func NewAce(router *chi.Mux, conf *config.Config, cron *cron.Cron, migrator *gormigrate.Gormigrate, server *hlfhr.Server, reloader *tlscert.Reloader, runner types.TaskRunner) *Ace {
|
||||
return &Ace{
|
||||
conf: conf,
|
||||
router: router,
|
||||
@@ -39,7 +39,7 @@ func NewAce(router *chi.Mux, conf *config.Config, cron *cron.Cron, migrator *gor
|
||||
migrator: migrator,
|
||||
cron: cron,
|
||||
runner: runner,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Ace) Run() error {
|
||||
|
||||
+2
-2
@@ -13,12 +13,12 @@ type Cli struct {
|
||||
migrator *gormigrate.Gormigrate
|
||||
}
|
||||
|
||||
func NewCli(cmd *cli.Command, migrator *gormigrate.Gormigrate) (*Cli, error) {
|
||||
func NewCli(cmd *cli.Command, migrator *gormigrate.Gormigrate) *Cli {
|
||||
IsCli = true
|
||||
return &Cli{
|
||||
cmd: cmd,
|
||||
migrator: migrator,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Cli) Run() error {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package acewaf
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
}
|
||||
|
||||
func (s *App) Route(_ chi.Router) {
|
||||
// WAF 管理路由由 route 贡献(route/waf.go)统一提供
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("acewaf")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
@@ -18,14 +19,16 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
var mpmEventRegexp = regexp.MustCompile(`(?s)<IfModule mpm_event_module>(.*?)</IfModule>`)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -34,6 +37,8 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.SaveConfig)
|
||||
r.Get("/error_log", s.ErrorLog)
|
||||
r.Post("/clear_error_log", s.ClearErrorLog)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
@@ -42,7 +47,7 @@ func (s *App) Status() string {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/apache/conf/httpd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -58,7 +63,7 @@ func (s *App) SaveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/apache/conf/httpd.conf", app.Root), req.Config, 0600); err != nil {
|
||||
if err = io.Write(app.Root+"/server/apache/conf/httpd.conf", req.Config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -160,3 +165,83 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Apache 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
defaultConf, err := io.Read(app.Root + "/server/apache/conf/extra/httpd-default.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
mpmConf, _ := io.Read(app.Root + "/server/apache/conf/extra/httpd-mpm.conf")
|
||||
eventBlock := ""
|
||||
if m := mpmEventRegexp.FindStringSubmatch(mpmConf); len(m) > 1 {
|
||||
eventBlock = m[1]
|
||||
}
|
||||
|
||||
// 面板统一写入 httpd-default.conf,未写入过的从 httpd-mpm.conf 的 event 块读取默认值
|
||||
get := func(key string) string {
|
||||
if v := confval.Directive.Get(defaultConf, key); v != "" {
|
||||
return v
|
||||
}
|
||||
return confval.Directive.Get(eventBlock, key)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
// MPM 事件模型
|
||||
StartServers: get("StartServers"),
|
||||
MinSpareThreads: get("MinSpareThreads"),
|
||||
MaxSpareThreads: get("MaxSpareThreads"),
|
||||
ThreadsPerChild: get("ThreadsPerChild"),
|
||||
MaxRequestWorkers: get("MaxRequestWorkers"),
|
||||
MaxConnectionsPerChild: get("MaxConnectionsPerChild"),
|
||||
// 连接设置
|
||||
Timeout: get("Timeout"),
|
||||
KeepAlive: get("KeepAlive"),
|
||||
MaxKeepAliveRequests: get("MaxKeepAliveRequests"),
|
||||
KeepAliveTimeout: get("KeepAliveTimeout"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Apache 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confPath := app.Root + "/server/apache/conf/extra/httpd-default.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// MPM 参数一并写入 httpd-default.conf,其 Include 顺序在 httpd-mpm.conf 之后,顶层定义覆盖块内默认值
|
||||
config = confval.Directive.Set(config, "StartServers", req.StartServers)
|
||||
config = confval.Directive.Set(config, "MinSpareThreads", req.MinSpareThreads)
|
||||
config = confval.Directive.Set(config, "MaxSpareThreads", req.MaxSpareThreads)
|
||||
config = confval.Directive.Set(config, "ThreadsPerChild", req.ThreadsPerChild)
|
||||
config = confval.Directive.Set(config, "MaxRequestWorkers", req.MaxRequestWorkers)
|
||||
config = confval.Directive.Set(config, "MaxConnectionsPerChild", req.MaxConnectionsPerChild)
|
||||
config = confval.Directive.Set(config, "Timeout", req.Timeout)
|
||||
config = confval.Directive.Set(config, "KeepAlive", req.KeepAlive)
|
||||
config = confval.Directive.Set(config, "MaxKeepAliveRequests", req.MaxKeepAliveRequests)
|
||||
config = confval.Directive.Set(config, "KeepAliveTimeout", req.KeepAliveTimeout)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("apache"); err != nil {
|
||||
out, _ := shell.Execf("%s/server/apache/bin/apachectl configtest", app.Root)
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload apache: %v %s", err, out))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -3,3 +3,19 @@ package apache
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Apache 配置调整
|
||||
type ConfigTune struct {
|
||||
// MPM 事件模型
|
||||
StartServers string `form:"start_servers" json:"start_servers"`
|
||||
MinSpareThreads string `form:"min_spare_threads" json:"min_spare_threads"`
|
||||
MaxSpareThreads string `form:"max_spare_threads" json:"max_spare_threads"`
|
||||
ThreadsPerChild string `form:"threads_per_child" json:"threads_per_child"`
|
||||
MaxRequestWorkers string `form:"max_request_workers" json:"max_request_workers"`
|
||||
MaxConnectionsPerChild string `form:"max_connections_per_child" json:"max_connections_per_child"`
|
||||
// 连接设置
|
||||
Timeout string `form:"timeout" json:"timeout"`
|
||||
KeepAlive string `form:"keep_alive" json:"keep_alive"`
|
||||
MaxKeepAliveRequests string `form:"max_keep_alive_requests" json:"max_keep_alive_requests"`
|
||||
KeepAliveTimeout string `form:"keep_alive_timeout" json:"keep_alive_timeout"`
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package apps
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/acewaf"
|
||||
"github.com/acepanel/panel/v3/internal/apps/apache"
|
||||
"github.com/acepanel/panel/v3/internal/apps/clickhouse"
|
||||
"github.com/acepanel/panel/v3/internal/apps/codeserver"
|
||||
@@ -48,5 +47,4 @@ var ProviderSet = wire.NewSet(
|
||||
postgresql.NewApp, prometheus.NewApp, pureftpd.NewApp,
|
||||
redis.NewApp, rocketmq.NewApp, rsync.NewApp,
|
||||
s3fs.NewApp, supervisor.NewApp, valkey.NewApp,
|
||||
acewaf.NewApp,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package clickhouse
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -9,11 +10,12 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -27,12 +29,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) (*App, error) {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -106,29 +108,12 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *App) 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(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("clickhouse-server"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "clickhouse-server")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
@@ -141,14 +126,14 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
ListenHost: s.getYAMLValue(cfg, "listen_host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http_port"),
|
||||
TCPPort: s.getYAMLValue(cfg, "tcp_port"),
|
||||
MaxMemoryUsage: s.getYAMLValue(cfg, "max_memory_usage"),
|
||||
MaxThreads: s.getYAMLValue(cfg, "max_threads"),
|
||||
Path: s.getYAMLValue(cfg, "path"),
|
||||
TmpPath: s.getYAMLValue(cfg, "tmp_path"),
|
||||
LogLevel: s.getYAMLValue(cfg, "logger.level"),
|
||||
ListenHost: confval.GetYAML(cfg, "listen_host"),
|
||||
HTTPPort: confval.GetYAML(cfg, "http_port"),
|
||||
TCPPort: confval.GetYAML(cfg, "tcp_port"),
|
||||
MaxMemoryUsage: confval.GetYAML(cfg, "max_memory_usage"),
|
||||
MaxThreads: confval.GetYAML(cfg, "max_threads"),
|
||||
Path: confval.GetYAML(cfg, "path"),
|
||||
TmpPath: confval.GetYAML(cfg, "tmp_path"),
|
||||
LogLevel: confval.GetYAML(cfg, "logger.level"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -218,7 +203,7 @@ func (s *App) SetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 计算 SHA256 哈希
|
||||
hash := sha256.Sum256([]byte(req.Password))
|
||||
hexHash := fmt.Sprintf("%x", hash)
|
||||
hexHash := hex.EncodeToString(hash[:])
|
||||
|
||||
// 读取 users.d/default.yaml 并更新密码
|
||||
raw, _ := io.Read(s.usersConfigPath())
|
||||
@@ -262,12 +247,12 @@ func (s *App) SetDefaultPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回主配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/clickhouse/config/config.yaml", app.Root)
|
||||
return app.Root + "/server/clickhouse/config/config.yaml"
|
||||
}
|
||||
|
||||
// usersConfigPath 返回用户密码配置文件路径(users.d/ 由 ConfigProcessor 自动合并到 users.yaml)
|
||||
func (s *App) usersConfigPath() string {
|
||||
return fmt.Sprintf("%s/server/clickhouse/config/users.d/default.yaml", app.Root)
|
||||
return app.Root + "/server/clickhouse/config/users.d/default.yaml"
|
||||
}
|
||||
|
||||
// getPort 从配置中获取 HTTP 端口
|
||||
@@ -276,35 +261,13 @@ func (s *App) getPort() string {
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http_port"); v != "" {
|
||||
if v := confval.GetYAML(cfg, "http_port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "8123"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,支持嵌套键
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 先尝试平铺键
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置平铺 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package clickhouse
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// SetDefaultPassword 设置 default 用户密码
|
||||
type SetDefaultPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Package common 承载各应用共用的 HTTP 处理逻辑
|
||||
package common
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
)
|
||||
|
||||
// ServeConfig 返回配置文件原始内容
|
||||
func ServeConfig(w http.ResponseWriter, path string) {
|
||||
config, _ := io.Read(path)
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// SaveConfig 写入配置文件并重启对应服务
|
||||
func SaveConfig(w http.ResponseWriter, r *http.Request, path, unit string) {
|
||||
req, err := service.Bind[request.AppUpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(path, req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart(unit); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// Package confval 读写行式配置文件中的键值项
|
||||
package confval
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Codec 描述一种行式配置文件的书写约定
|
||||
type Codec struct {
|
||||
// Delim 读取时分隔键与值的字符,空表示按空白切分
|
||||
Delim string
|
||||
// Assign 写入时的键值分隔符,如 " "、"="、" = "
|
||||
Assign string
|
||||
// Terminator 行尾终止符,如 nginx 的 ";"
|
||||
Terminator string
|
||||
// Quote 写入时给值添加的引号,读取时一并去除
|
||||
Quote string
|
||||
// Comments 识别为注释的前缀,第一个用于注释掉配置项
|
||||
Comments []string
|
||||
// Sections 是否按 [section] 分组匹配
|
||||
Sections bool
|
||||
// InlineComment 该格式支持行尾注释
|
||||
// 仅对确实支持的格式开启:redis.conf 与 java properties 不支持,
|
||||
// 值里的 "#" 是数据(如密码),误判会损坏配置
|
||||
InlineComment bool
|
||||
// RequireValue 匹配时要求该行已带值,用于区分「开关」与「键 值」
|
||||
RequireValue bool
|
||||
// AppendIndent 追加新配置项时使用的缩进
|
||||
AppendIndent string
|
||||
}
|
||||
|
||||
// line 为一行配置解析后的各组成部分
|
||||
type line struct {
|
||||
indent string // 行首缩进
|
||||
commented string // 命中的注释前缀,非注释行为空
|
||||
key string
|
||||
value string // 已去引号
|
||||
comment string // 行尾注释(含前缀),无则为空
|
||||
ok bool // 是否为可识别的键值行
|
||||
}
|
||||
|
||||
// 各应用配置文件的书写约定
|
||||
var (
|
||||
// Directive 「键 值」,用于 redis、valkey
|
||||
Directive = Codec{Assign: " ", Comments: []string{"#"}}
|
||||
// FTP 「键 值」,pure-ftpd 的配置项必须带值
|
||||
FTP = Codec{Assign: " ", Comments: []string{"#"}, RequireValue: true}
|
||||
// Properties 「键=值」,用于 kafka、rocketmq
|
||||
Properties = Codec{Delim: "=", Assign: "=", Comments: []string{"#"}}
|
||||
// Nginx 「键 值;」,支持行尾注释
|
||||
Nginx = Codec{Assign: " ", Terminator: ";", Comments: []string{"#"}, InlineComment: true, RequireValue: true, AppendIndent: " "}
|
||||
// Postgres 「键 = '值'」,支持行尾注释
|
||||
Postgres = Codec{Delim: "=", Assign: " = ", Quote: "'", Comments: []string{"#"}, InlineComment: true}
|
||||
// INI 「键 = 值」,忽略分组,用于 mysql
|
||||
INI = Codec{Delim: "=", Assign: " = ", Comments: []string{"#", ";"}}
|
||||
// SectionINI 分组感知的 INI,用于 grafana
|
||||
SectionINI = Codec{Delim: "=", Assign: " = ", Comments: []string{"#", ";"}, Sections: true}
|
||||
// PHPINI PHP 的 ini,注释优先使用 ";"
|
||||
PHPINI = Codec{Delim: "=", Assign: " = ", Comments: []string{";", "#"}}
|
||||
// Supervisor 「键=值」,注释同时支持 ";" 与 "#"
|
||||
Supervisor = Codec{Delim: "=", Assign: "=", Comments: []string{";", "#"}}
|
||||
)
|
||||
|
||||
// Get 读取配置项,未命中返回空串
|
||||
func (c Codec) Get(content, key string) string {
|
||||
return c.GetIn(content, "", key)
|
||||
}
|
||||
|
||||
// GetIn 在指定分组内读取配置项,section 为空表示不限分组
|
||||
func (c Codec) GetIn(content, section, key string) string {
|
||||
current := ""
|
||||
for raw := range strings.SplitSeq(content, "\n") {
|
||||
if name, ok := sectionName(raw); ok {
|
||||
current = name
|
||||
continue
|
||||
}
|
||||
if c.Sections && current != section {
|
||||
continue
|
||||
}
|
||||
|
||||
// 被注释掉的配置项视为未设置
|
||||
parsed := c.parse(raw)
|
||||
if parsed.ok && parsed.commented == "" && parsed.key == key {
|
||||
return parsed.value
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Set 写入配置项,值为空时注释掉该项
|
||||
func (c Codec) Set(content, key, value string) string {
|
||||
return c.SetIn(content, "", key, value)
|
||||
}
|
||||
|
||||
// SetIn 在指定分组内写入配置项,分组不存在时在末尾补建
|
||||
func (c Codec) SetIn(content, section, key, value string) string {
|
||||
value = strings.NewReplacer("\n", "", "\r", "").Replace(value)
|
||||
|
||||
raws := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(raws)+3)
|
||||
current := ""
|
||||
found := false
|
||||
// 目标分组最后一行在 result 中的位置,用于在组内补插新项
|
||||
sectionEnd := -1
|
||||
|
||||
for _, raw := range raws {
|
||||
if name, ok := sectionName(raw); ok {
|
||||
current = name
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
if c.Sections && current == section && strings.TrimSpace(raw) != "" {
|
||||
sectionEnd = len(result)
|
||||
}
|
||||
if c.Sections && current != section {
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
|
||||
// 注释掉的配置项同样参与匹配,便于重新启用
|
||||
parsed := c.parse(raw)
|
||||
if !parsed.ok || parsed.key != key {
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
|
||||
// 同名项只保留第一处,后续重复行丢弃
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
|
||||
if value == "" {
|
||||
// 值为空时注释掉该配置项,已注释的原样保留
|
||||
if parsed.commented == "" {
|
||||
result = append(result, parsed.indent+c.comment()+" "+strings.TrimSpace(raw))
|
||||
} else {
|
||||
result = append(result, raw)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 保留原行的缩进与行尾注释,只替换值
|
||||
result = append(result, parsed.indent+c.format(key, value)+trailing(parsed.comment))
|
||||
if c.Sections {
|
||||
sectionEnd = len(result) - 1
|
||||
}
|
||||
}
|
||||
|
||||
if found || value == "" {
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
newLine := c.AppendIndent + c.format(key, value)
|
||||
switch {
|
||||
case !c.Sections:
|
||||
result = append(result, newLine)
|
||||
case sectionEnd >= 0:
|
||||
result = slices.Insert(result, sectionEnd+1, newLine)
|
||||
default:
|
||||
// 分组不存在,在文件末尾补建
|
||||
result = append(result, "", "["+section+"]", newLine)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
// parse 将一行拆解为缩进、注释状态、键、值与行尾注释
|
||||
func (c Codec) parse(raw string) line {
|
||||
parsed := line{indent: indentOf(raw)}
|
||||
|
||||
body := strings.TrimSpace(raw)
|
||||
if body == "" {
|
||||
return parsed
|
||||
}
|
||||
if parsed.commented = c.commentOf(body); parsed.commented != "" {
|
||||
body = strings.TrimSpace(strings.TrimPrefix(body, parsed.commented))
|
||||
}
|
||||
|
||||
// 行尾注释在终止符之后,需先摘除
|
||||
if c.InlineComment {
|
||||
body, parsed.comment = c.cutComment(body)
|
||||
}
|
||||
if c.Terminator != "" {
|
||||
if !strings.HasSuffix(body, c.Terminator) {
|
||||
return parsed
|
||||
}
|
||||
body = strings.TrimSpace(strings.TrimSuffix(body, c.Terminator))
|
||||
}
|
||||
|
||||
key, value, ok := c.split(body)
|
||||
if !ok {
|
||||
return parsed
|
||||
}
|
||||
parsed.key, parsed.value, parsed.ok = key, c.unquote(value), true
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
// split 从已剥离注释与终止符的正文中取出键与值
|
||||
func (c Codec) split(body string) (key, value string, ok bool) {
|
||||
if c.Delim != "" {
|
||||
k, v, found := strings.Cut(body, c.Delim)
|
||||
if !found {
|
||||
return "", "", false
|
||||
}
|
||||
return strings.TrimSpace(k), strings.TrimSpace(v), true
|
||||
}
|
||||
|
||||
parts := strings.Fields(body)
|
||||
if len(parts) == 0 || (c.RequireValue && len(parts) < 2) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
return parts[0], strings.Join(parts[1:], " "), true
|
||||
}
|
||||
|
||||
// cutComment 切出行尾注释,引号内的注释符是数据而非注释
|
||||
func (c Codec) cutComment(body string) (string, string) {
|
||||
inSingle, inDouble := false, false
|
||||
for i := range len(body) {
|
||||
switch ch := body[i]; {
|
||||
case ch == '\'' && !inDouble:
|
||||
inSingle = !inSingle
|
||||
case ch == '"' && !inSingle:
|
||||
inDouble = !inDouble
|
||||
case !inSingle && !inDouble:
|
||||
for _, prefix := range c.Comments {
|
||||
if strings.HasPrefix(body[i:], prefix) {
|
||||
return strings.TrimRight(body[:i], " \t"), body[i:]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return body, ""
|
||||
}
|
||||
|
||||
// unquote 去除值两端的引号
|
||||
func (c Codec) unquote(value string) string {
|
||||
if c.Quote == "" {
|
||||
return value
|
||||
}
|
||||
|
||||
return strings.Trim(value, `'"`)
|
||||
}
|
||||
|
||||
// format 拼出一条完整的配置行
|
||||
func (c Codec) format(key, value string) string {
|
||||
return key + c.Assign + c.Quote + value + c.Quote + c.Terminator
|
||||
}
|
||||
|
||||
// comment 注释掉配置项时使用的前缀
|
||||
func (c Codec) comment() string {
|
||||
if len(c.Comments) == 0 {
|
||||
return "#"
|
||||
}
|
||||
|
||||
return c.Comments[0]
|
||||
}
|
||||
|
||||
// commentOf 返回该行命中的注释前缀,非注释行返回空串
|
||||
func (c Codec) commentOf(trimmed string) string {
|
||||
for _, prefix := range c.Comments {
|
||||
if strings.HasPrefix(trimmed, prefix) {
|
||||
return prefix
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// trailing 把行尾注释接回改写后的行
|
||||
func trailing(comment string) string {
|
||||
if comment == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return " " + comment
|
||||
}
|
||||
|
||||
// sectionName 解析 [section] 行
|
||||
func sectionName(raw string) (string, bool) {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if !strings.HasPrefix(trimmed, "[") || !strings.HasSuffix(trimmed, "]") {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return strings.TrimSpace(trimmed[1 : len(trimmed)-1]), true
|
||||
}
|
||||
|
||||
// indentOf 取出行首缩进
|
||||
func indentOf(raw string) string {
|
||||
return raw[:len(raw)-len(strings.TrimLeft(raw, " \t"))]
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package confval
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type ConfValTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestConfValTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConfValTestSuite))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveGet() {
|
||||
conf := "# comment\nbind 127.0.0.1 ::1\nport 6379\n\n# maxmemory 100mb\n"
|
||||
|
||||
s.Equal("127.0.0.1 ::1", Directive.Get(conf, "bind"))
|
||||
s.Equal("6379", Directive.Get(conf, "port"))
|
||||
// 注释掉的项不应被读出
|
||||
s.Empty(Directive.Get(conf, "maxmemory"))
|
||||
s.Empty(Directive.Get(conf, "absent"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetReplaces() {
|
||||
s.Equal("port 6380", Directive.Set("port 6379", "port", "6380"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetAppendsWhenMissing() {
|
||||
s.Equal("port 6379\ntimeout 30", Directive.Set("port 6379", "timeout", "30"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetCommentsOutEmptyValue() {
|
||||
s.Equal("# port 6379", Directive.Set("port 6379", "port", ""))
|
||||
// 已注释的保持原样,不重复加注释符
|
||||
s.Equal("# port 6379", Directive.Set("# port 6379", "port", ""))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetReenablesCommented() {
|
||||
s.Equal("maxmemory 200mb", Directive.Set("# maxmemory 100mb", "maxmemory", "200mb"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestDirectiveSetOnlyKeepsFirstMatch() {
|
||||
s.Equal("port 6380", Directive.Set("port 6379\nport 7000", "port", "6380"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSetStripsNewlinesFromValue() {
|
||||
s.Equal("port 63opq79", Directive.Set("port 1", "port", "63\nopq\r79"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestFTPRequiresValueToMatch() {
|
||||
// 裸键行不算配置项,避免把开关误判为键值对
|
||||
s.Empty(FTP.Get("NoAnonymous\n", "NoAnonymous"))
|
||||
s.Equal("yes", FTP.Get("NoAnonymous yes\n", "NoAnonymous"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestPropertiesRoundTrip() {
|
||||
conf := "# broker\nnum.network.threads=3\n"
|
||||
|
||||
s.Equal("3", Properties.Get(conf, "num.network.threads"))
|
||||
s.Equal("# broker\nnum.network.threads=5\n", Properties.Set(conf, "num.network.threads", "5"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestNginxTerminatorAndIndent() {
|
||||
conf := "http {\n keepalive_timeout 60;\n}"
|
||||
|
||||
s.Equal("60", Nginx.Get(conf, "keepalive_timeout"))
|
||||
// 缩进保留,行尾分号补回
|
||||
s.Equal("http {\n keepalive_timeout 75;\n}", Nginx.Set(conf, "keepalive_timeout", "75"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestNginxIgnoresLinesWithoutTerminator() {
|
||||
s.Empty(Nginx.Get("http {\n keepalive_timeout 60\n}", "keepalive_timeout"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestNginxKeepsTrailingComment() {
|
||||
conf := "http {\n keepalive_timeout 60; # 保持连接\n}"
|
||||
|
||||
// 注释在分号之后,读取时不能混进值里
|
||||
s.Equal("60", Nginx.Get(conf, "keepalive_timeout"))
|
||||
s.Equal("http {\n keepalive_timeout 75; # 保持连接\n}", Nginx.Set(conf, "keepalive_timeout", "75"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestCommentOutKeepsWholeLine() {
|
||||
// 注释掉时整行保留,行尾注释不丢
|
||||
s.Equal("# max_connections = 100 # note", Postgres.Set("max_connections = 100 # note", "max_connections", ""))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestNginxAppendsWithIndent() {
|
||||
s.Equal("a b;\n c d;", Nginx.Set("a b;", "c", "d"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestPostgresQuotesAndInlineComment() {
|
||||
conf := "max_connections = 100 # note\nlisten_addresses = '*'\n"
|
||||
|
||||
s.Equal("100", Postgres.Get(conf, "max_connections"))
|
||||
s.Equal("*", Postgres.Get(conf, "listen_addresses"))
|
||||
// 只替换值,行尾注释保留
|
||||
s.Equal("max_connections = '200' # note\nlisten_addresses = '*'\n", Postgres.Set(conf, "max_connections", "200"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestInlineCommentNotAppliedWhereUnsupported() {
|
||||
// redis.conf 不支持行尾注释,密码里的 # 是数据不能当注释切掉
|
||||
s.Equal("p@ss#word", Directive.Get("requirepass p@ss#word\n", "requirepass"))
|
||||
s.Equal("requirepass a#b", Directive.Set("requirepass p@ss#word", "requirepass", "a#b"))
|
||||
// java properties 同理
|
||||
s.Equal("a#b", Properties.Get("pass=a#b\n", "pass"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestQuotedCommentCharIsData() {
|
||||
s.Equal("a#b", Postgres.Get("password = 'a#b' # real note\n", "password"))
|
||||
s.Equal("password = 'x#y' # real note", Postgres.Set("password = 'a#b' # real note", "password", "x#y"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestINIIgnoresSectionHeaders() {
|
||||
conf := "[mysqld]\nport = 3306\n; skip-name-resolve = 1\n"
|
||||
|
||||
s.Equal("3306", INI.Get(conf, "port"))
|
||||
s.Empty(INI.Get(conf, "mysqld"))
|
||||
// 分号注释同样能被重新启用
|
||||
s.Equal("[mysqld]\nport = 3306\nskip-name-resolve = 2\n", INI.Set(conf, "skip-name-resolve", "2"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSectionINIMatchesOnlyTargetSection() {
|
||||
conf := "[server]\nhttp_port = 3000\n\n[database]\nhttp_port = 9999\n"
|
||||
|
||||
s.Equal("3000", SectionINI.GetIn(conf, "server", "http_port"))
|
||||
s.Equal("9999", SectionINI.GetIn(conf, "database", "http_port"))
|
||||
s.Empty(SectionINI.GetIn(conf, "absent", "http_port"))
|
||||
|
||||
got := SectionINI.SetIn(conf, "database", "http_port", "8888")
|
||||
s.Equal("3000", SectionINI.GetIn(got, "server", "http_port"))
|
||||
s.Equal("8888", SectionINI.GetIn(got, "database", "http_port"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSectionINIInsertsInsideSection() {
|
||||
conf := "[server]\nhttp_port = 3000\n\n[database]\ntype = sqlite3\n"
|
||||
|
||||
got := SectionINI.SetIn(conf, "server", "domain", "example.com")
|
||||
s.Equal("example.com", SectionINI.GetIn(got, "server", "domain"))
|
||||
// 新项必须落在 server 段内,不能漏进 database 段
|
||||
s.Empty(SectionINI.GetIn(got, "database", "domain"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSectionINICreatesMissingSection() {
|
||||
got := SectionINI.SetIn("[server]\nhttp_port = 3000\n", "smtp", "host", "localhost:25")
|
||||
|
||||
s.Equal("localhost:25", SectionINI.GetIn(got, "smtp", "host"))
|
||||
s.Equal("3000", SectionINI.GetIn(got, "server", "http_port"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestPHPINICommentsWithSemicolon() {
|
||||
s.Equal("; memory_limit = 128M", PHPINI.Set("memory_limit = 128M", "memory_limit", ""))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestGetYAMLFlatAndNested() {
|
||||
s.Equal("single-node", GetYAML(map[string]any{"discovery.type": "single-node"}, "discovery.type"))
|
||||
s.Equal("single-node", GetYAML(map[string]any{"discovery": map[string]any{"type": "single-node"}}, "discovery.type"))
|
||||
s.Empty(GetYAML(map[string]any{}, "discovery.type"))
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSetYAMLFlattensAndClearsNested() {
|
||||
cfg := map[string]any{"discovery": map[string]any{"type": "old"}}
|
||||
SetYAML(cfg, "discovery.type", "single-node")
|
||||
|
||||
s.Equal("single-node", cfg["discovery.type"])
|
||||
// 嵌套键清空后整个父键一并移除
|
||||
_, ok := cfg["discovery"]
|
||||
s.False(ok)
|
||||
}
|
||||
|
||||
func (s *ConfValTestSuite) TestSetYAMLIgnoresEmptyValue() {
|
||||
cfg := map[string]any{}
|
||||
SetYAML(cfg, "discovery.type", "")
|
||||
|
||||
s.Empty(cfg)
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package confval
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// tomlCodec TOML 根表的书写约定,值的引号由 formatTOML 按类型决定
|
||||
var tomlCodec = Codec{Delim: "=", Assign: " = ", Comments: []string{"#"}, InlineComment: true}
|
||||
|
||||
// GetTOML 读取根表配置项,兼容点号键(auth.token = "x")与分组写法([auth] 下的 token = "x")
|
||||
func GetTOML(content, key string) string {
|
||||
section, leaf, nested := cutLast(key)
|
||||
|
||||
current := ""
|
||||
for raw := range strings.SplitSeq(content, "\n") {
|
||||
if name, ok := tableName(raw); ok {
|
||||
current = name
|
||||
continue
|
||||
}
|
||||
|
||||
parsed := tomlCodec.parse(raw)
|
||||
if !parsed.ok || parsed.commented != "" {
|
||||
continue
|
||||
}
|
||||
// 根表下写点号全键,分组内写末段键,两种写法都算命中
|
||||
matched := (current == "" && parsed.key == key) || (nested && current == section && parsed.key == leaf)
|
||||
if matched {
|
||||
return unquoteTOML(parsed.value)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetTOML 写入根表配置项,值为空时注释掉该项;新键以点号形式插入到首个表头之前
|
||||
func SetTOML(content, key string, value any) string {
|
||||
literal := formatTOML(value)
|
||||
section, leaf, nested := cutLast(key)
|
||||
|
||||
raws := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(raws)+1)
|
||||
current := ""
|
||||
found := false
|
||||
// 首个表头在 result 中的位置,新键只能插在它之前才仍属于根表
|
||||
firstTable := -1
|
||||
|
||||
for _, raw := range raws {
|
||||
if name, ok := tableName(raw); ok {
|
||||
current = name
|
||||
if firstTable < 0 {
|
||||
firstTable = len(result)
|
||||
}
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
|
||||
parsed := tomlCodec.parse(raw)
|
||||
matched := parsed.ok && ((current == "" && parsed.key == key) || (nested && current == section && parsed.key == leaf))
|
||||
if !matched || found {
|
||||
result = append(result, raw)
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
|
||||
if literal == "" {
|
||||
// 值为空时注释掉该配置项,已注释的原样保留
|
||||
if parsed.commented == "" {
|
||||
result = append(result, parsed.indent+"# "+strings.TrimSpace(raw))
|
||||
} else {
|
||||
result = append(result, raw)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 保留原行的写法、缩进与行尾注释,只替换值
|
||||
result = append(result, parsed.indent+parsed.key+tomlCodec.Assign+literal+trailing(parsed.comment))
|
||||
}
|
||||
|
||||
if found || literal == "" {
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
if firstTable < 0 {
|
||||
firstTable = len(result)
|
||||
}
|
||||
|
||||
return strings.Join(slices.Insert(result, firstTable, key+tomlCodec.Assign+literal), "\n")
|
||||
}
|
||||
|
||||
// formatTOML 零值返回空串,表示删除该项
|
||||
func formatTOML(value any) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if v == "" {
|
||||
return ""
|
||||
}
|
||||
return strconv.Quote(v)
|
||||
case bool:
|
||||
return strconv.FormatBool(v)
|
||||
default:
|
||||
return cast.ToString(v)
|
||||
}
|
||||
}
|
||||
|
||||
// unquoteTOML 去除字符串值的引号,非字符串值原样返回
|
||||
func unquoteTOML(value string) string {
|
||||
if unquoted, err := strconv.Unquote(value); err == nil {
|
||||
return unquoted
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// tableName 解析 [table] 与 [[table]] 行,后者比 sectionName 多剥一层括号
|
||||
func tableName(raw string) (string, bool) {
|
||||
name, ok := sectionName(raw)
|
||||
return strings.Trim(name, "[]"), ok
|
||||
}
|
||||
|
||||
// cutLast 切出点号键的分组前缀与末段,如 auth.token -> auth, token
|
||||
func cutLast(key string) (section, leaf string, nested bool) {
|
||||
idx := strings.LastIndex(key, ".")
|
||||
if idx < 0 {
|
||||
return "", key, false
|
||||
}
|
||||
|
||||
return key[:idx], key[idx+1:], true
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package confval
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type TOMLTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestTOMLTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(TOMLTestSuite))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestGetDottedKey() {
|
||||
conf := "bindPort = 7000\nauth.token = \"12345678\"\n# maxPortsPerClient = 0\n"
|
||||
|
||||
s.Equal("7000", GetTOML(conf, "bindPort"))
|
||||
s.Equal("12345678", GetTOML(conf, "auth.token"))
|
||||
// 注释掉的项视为未设置
|
||||
s.Empty(GetTOML(conf, "maxPortsPerClient"))
|
||||
s.Empty(GetTOML(conf, "absent"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestGetSectionKey() {
|
||||
conf := "bindPort = 7000\n\n[auth]\nmethod = \"token\"\ntoken = \"abc\"\n"
|
||||
|
||||
s.Equal("token", GetTOML(conf, "auth.method"))
|
||||
s.Equal("abc", GetTOML(conf, "auth.token"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestGetIgnoresProxyBlock() {
|
||||
conf := "serverAddr = \"1.1.1.1\"\n\n[[proxies]]\nname = \"ssh\"\nserverAddr = \"2.2.2.2\"\n"
|
||||
|
||||
s.Equal("1.1.1.1", GetTOML(conf, "serverAddr"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetReplacesInPlace() {
|
||||
conf := "# 监听端口\nbindPort = 7000 # 默认\nauth.token = \"old\"\n"
|
||||
|
||||
s.Equal("# 监听端口\nbindPort = 7001 # 默认\nauth.token = \"old\"\n", SetTOML(conf, "bindPort", 7001))
|
||||
s.Equal("# 监听端口\nbindPort = 7000 # 默认\nauth.token = \"new\"\n", SetTOML(conf, "auth.token", "new"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetReplacesInSection() {
|
||||
conf := "bindPort = 7000\n\n[auth]\ntoken = \"old\"\n"
|
||||
|
||||
s.Equal("bindPort = 7000\n\n[auth]\ntoken = \"new\"\n", SetTOML(conf, "auth.token", "new"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetInsertsBeforeFirstTable() {
|
||||
conf := "bindPort = 7000\n\n[[proxies]]\nname = \"ssh\"\n"
|
||||
|
||||
s.Equal("bindPort = 7000\n\nauth.token = \"abc\"\n[[proxies]]\nname = \"ssh\"\n", SetTOML(conf, "auth.token", "abc"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetAppendsWhenNoTable() {
|
||||
s.Equal("bindPort = 7000\nauth.token = \"abc\"", SetTOML("bindPort = 7000", "auth.token", "abc"))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetCommentsOutEmptyValue() {
|
||||
s.Equal("# bindPort = 7000", SetTOML("bindPort = 7000", "bindPort", ""))
|
||||
s.Equal("# bindPort = 7000", SetTOML("# bindPort = 7000", "bindPort", ""))
|
||||
// 键不存在时不应插入空值
|
||||
s.Equal("bindPort = 7000", SetTOML("bindPort = 7000", "auth.token", ""))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetReenablesCommented() {
|
||||
s.Equal("maxPortsPerClient = 5", SetTOML("# maxPortsPerClient = 0", "maxPortsPerClient", 5))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetBool() {
|
||||
s.Equal("bindPort = 7000\ntransport.tls.force = true", SetTOML("bindPort = 7000", "transport.tls.force", true))
|
||||
}
|
||||
|
||||
func (s *TOMLTestSuite) TestSetDoesNotTouchProxyBlock() {
|
||||
conf := "serverAddr = \"1.1.1.1\"\n\n[[proxies]]\n# 手写的隧道\nname = \"ssh\"\nserverPort = 22\n"
|
||||
|
||||
s.Equal("serverAddr = \"2.2.2.2\"\n\n[[proxies]]\n# 手写的隧道\nname = \"ssh\"\nserverPort = 22\n", SetTOML(conf, "serverAddr", "2.2.2.2"))
|
||||
// serverPort 只存在于 proxies 表内,不应被改写,而是插到首个表头前
|
||||
s.Equal("serverAddr = \"1.1.1.1\"\n\nserverPort = 7000\n[[proxies]]\n# 手写的隧道\nname = \"ssh\"\nserverPort = 22\n", SetTOML(conf, "serverPort", 7000))
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package confval
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// GetYAML 读取 YAML 配置项,优先平铺键(安装脚本用 sed 生成的格式),回退到嵌套键
|
||||
func GetYAML(cfg map[string]any, key string) string {
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
|
||||
prefix, rest, nested := strings.Cut(key, ".")
|
||||
val, ok := cfg[prefix]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if !nested {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
child, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return GetYAML(child, rest)
|
||||
}
|
||||
|
||||
// SetYAML 以平铺键写入 YAML 配置项,同时清理可能残留的同名嵌套键
|
||||
func SetYAML(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
|
||||
cfg[key] = value
|
||||
prefix, rest, nested := strings.Cut(key, ".")
|
||||
if !nested {
|
||||
return
|
||||
}
|
||||
child, ok := cfg[prefix].(map[string]any)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
delete(child, rest)
|
||||
if len(child) == 0 {
|
||||
delete(cfg, prefix)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -16,8 +17,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -33,33 +34,11 @@ func (s *App) Status() string {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/docker/daemon.json")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, "/etc/docker/daemon.json")
|
||||
}
|
||||
|
||||
func (s *App) 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/docker/daemon.json", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("docker"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, "/etc/docker/daemon.json", "docker")
|
||||
}
|
||||
|
||||
// GetSettings 获取 Docker 设置
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package docker
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// LogOpts 日志配置选项
|
||||
type LogOpts struct {
|
||||
MaxSize string `json:"max-size,omitempty"` // 日志文件最大大小,如 "10m"
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -25,8 +26,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
return &App{t: t}, nil
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -92,28 +93,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) 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(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("elasticsearch"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "elasticsearch")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 ElasticSearch 配置调整参数
|
||||
@@ -135,13 +119,13 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
ClusterName: s.getYAMLValue(cfg, "cluster.name"),
|
||||
NodeName: s.getYAMLValue(cfg, "node.name"),
|
||||
NetworkHost: s.getYAMLValue(cfg, "network.host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http.port"),
|
||||
DiscoveryType: s.getYAMLValue(cfg, "discovery.type"),
|
||||
PathData: s.getYAMLValue(cfg, "path.data"),
|
||||
PathLogs: s.getYAMLValue(cfg, "path.logs"),
|
||||
ClusterName: confval.GetYAML(cfg, "cluster.name"),
|
||||
NodeName: confval.GetYAML(cfg, "node.name"),
|
||||
NetworkHost: confval.GetYAML(cfg, "network.host"),
|
||||
HTTPPort: confval.GetYAML(cfg, "http.port"),
|
||||
DiscoveryType: confval.GetYAML(cfg, "discovery.type"),
|
||||
PathData: confval.GetYAML(cfg, "path.data"),
|
||||
PathLogs: confval.GetYAML(cfg, "path.logs"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
@@ -164,13 +148,13 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
s.setYAMLValue(cfg, "cluster.name", req.ClusterName)
|
||||
s.setYAMLValue(cfg, "node.name", req.NodeName)
|
||||
s.setYAMLValue(cfg, "network.host", req.NetworkHost)
|
||||
s.setYAMLValue(cfg, "http.port", req.HTTPPort)
|
||||
s.setYAMLValue(cfg, "discovery.type", req.DiscoveryType)
|
||||
s.setYAMLValue(cfg, "path.data", req.PathData)
|
||||
s.setYAMLValue(cfg, "path.logs", req.PathLogs)
|
||||
confval.SetYAML(cfg, "cluster.name", req.ClusterName)
|
||||
confval.SetYAML(cfg, "node.name", req.NodeName)
|
||||
confval.SetYAML(cfg, "network.host", req.NetworkHost)
|
||||
confval.SetYAML(cfg, "http.port", req.HTTPPort)
|
||||
confval.SetYAML(cfg, "discovery.type", req.DiscoveryType)
|
||||
confval.SetYAML(cfg, "path.data", req.PathData)
|
||||
confval.SetYAML(cfg, "path.logs", req.PathLogs)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
@@ -202,17 +186,17 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/elasticsearch.yml", app.Root)
|
||||
return app.Root + "/server/elasticsearch/config/elasticsearch.yml"
|
||||
}
|
||||
|
||||
// jvmOptionsPath 返回 JVM 选项文件路径
|
||||
func (s *App) jvmOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/jvm.options", app.Root)
|
||||
return app.Root + "/server/elasticsearch/config/jvm.options"
|
||||
}
|
||||
|
||||
// jvmHeapOptionsPath 返回 JVM 堆内存配置文件路径(ES 9.x 推荐方式)
|
||||
func (s *App) jvmHeapOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/elasticsearch/config/jvm.options.d/heap.options", app.Root)
|
||||
return app.Root + "/server/elasticsearch/config/jvm.options.d/heap.options"
|
||||
}
|
||||
|
||||
// getPort 从配置中获取 HTTP 端口
|
||||
@@ -221,53 +205,13 @@ func (s *App) getPort() string {
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http.port"); v != "" {
|
||||
if v := confval.GetYAML(cfg, "http.port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "9200"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,优先匹配平铺键(如 "path.data"),回退到嵌套键(如 path -> data)
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 优先匹配平铺键(安装脚本用 sed 生成的格式)
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
// 使用平铺键,同时清理可能存在的嵌套键
|
||||
cfg[key] = value
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
if nested, ok := cfg[parts[0]].(map[string]any); ok {
|
||||
delete(nested, parts[1])
|
||||
if len(nested) == 0 {
|
||||
delete(cfg, parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseJVMHeap 从 jvm.options 中提取堆内存配置
|
||||
func (s *App) parseJVMHeap(content string) (initSize, maxSize string) {
|
||||
reInit := regexp.MustCompile(`(?m)^-Xms(\S+)`)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package elasticsearch
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune ElasticSearch 配置调整
|
||||
type ConfigTune struct {
|
||||
// 集群
|
||||
|
||||
+126
-185
@@ -1,25 +1,25 @@
|
||||
package fail2ban
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/libtnb/utils/str"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
webserver "github.com/acepanel/panel/v3/pkg/webserver/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
@@ -27,18 +27,19 @@ type App struct {
|
||||
websiteRepo biz.WebsiteRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, websiteRepo biz.WebsiteRepo) (*App, error) {
|
||||
func NewApp(t *gotext.Locale, websiteRepo biz.WebsiteRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
websiteRepo: websiteRepo,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/jails", s.List)
|
||||
r.Post("/jails", s.Create)
|
||||
r.Delete("/jails", s.Delete)
|
||||
r.Get("/jails/{name}", s.BanList)
|
||||
r.Post("/jails/{name}", s.Update)
|
||||
r.Delete("/jails/{name}", s.Delete)
|
||||
r.Get("/jails/{name}/ban", s.BanList)
|
||||
r.Post("/unban", s.Unban)
|
||||
r.Post("/white_list", s.SetWhiteList)
|
||||
r.Get("/white_list", s.GetWhiteList)
|
||||
@@ -51,39 +52,12 @@ func (s *App) Status() string {
|
||||
|
||||
// List 所有规则
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
jails, err := listJails()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
jailList := regexp.MustCompile(`\[(.*?)]`).FindAllStringSubmatch(raw, -1)
|
||||
|
||||
jails := make([]Jail, 0)
|
||||
for i, jail := range jailList {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
jailName := jail[1]
|
||||
jailRaw := str.Cut(raw, "# "+jailName+"-START", "# "+jailName+"-END")
|
||||
if len(jailRaw) == 0 {
|
||||
continue
|
||||
}
|
||||
jailEnabled := strings.Contains(jailRaw, "enabled = true")
|
||||
jailMaxRetry := regexp.MustCompile(`maxretry = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailFindTime := regexp.MustCompile(`findtime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
jailBanTime := regexp.MustCompile(`bantime = (.*)`).FindStringSubmatch(jailRaw)
|
||||
|
||||
jails = append(jails, Jail{
|
||||
Name: jailName,
|
||||
Enabled: jailEnabled,
|
||||
MaxRetry: cast.ToInt(jailMaxRetry[1]),
|
||||
FindTime: cast.ToInt(jailFindTime[1]),
|
||||
BanTime: cast.ToInt(jailBanTime[1]),
|
||||
})
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, jails)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
@@ -99,82 +73,36 @@ func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
jailName := req.Name
|
||||
jailType := req.Type
|
||||
jailMaxRetry := cast.ToString(req.MaxRetry)
|
||||
jailFindTime := cast.ToString(req.FindTime)
|
||||
jailBanTime := cast.ToString(req.BanTime)
|
||||
jailWebsiteName := req.WebsiteName
|
||||
jailWebsiteMode := req.WebsiteMode
|
||||
jailWebsitePath := req.WebsitePath
|
||||
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if (strings.Contains(raw, "["+jailName+"]") && jailType == "service") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-cc") && jailType == "website" && jailWebsiteMode == "cc") || (strings.Contains(raw, "["+jailWebsiteName+"]"+"-path") && jailType == "website" && jailWebsiteMode == "path") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule already exists"))
|
||||
return
|
||||
jail := Jail{
|
||||
Enabled: true,
|
||||
MaxRetry: req.MaxRetry,
|
||||
FindTime: req.FindTime,
|
||||
BanTime: req.BanTime,
|
||||
}
|
||||
|
||||
switch jailType {
|
||||
switch req.Type {
|
||||
case "website":
|
||||
website, err := s.websiteRepo.GetByName(jailWebsiteName)
|
||||
website, err := s.websiteRepo.GetByName(req.WebsiteName)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
var ports string
|
||||
for _, listen := range website.Listens {
|
||||
if port, err := cast.ToIntE(listen.Address); err == nil {
|
||||
ports += fmt.Sprintf("%d", port) + ","
|
||||
}
|
||||
}
|
||||
ports = strings.TrimSuffix(ports, ",")
|
||||
|
||||
rule := `
|
||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-START
|
||||
[` + jailWebsiteName + `-` + jailWebsiteMode + `]
|
||||
enabled = true
|
||||
filter = haozi-` + jailWebsiteName + `-` + jailWebsiteMode + `
|
||||
port = ` + ports + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
logpath = ` + app.Root + `/sites/` + website.Name + `/log/access.log
|
||||
# ` + jailWebsiteName + `-` + jailWebsiteMode + `-END
|
||||
`
|
||||
raw += rule
|
||||
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
ports := lo.FilterMap(website.Listens, func(listen webserver.Listen, _ int) (string, bool) {
|
||||
port, err := cast.ToIntE(listen.Address)
|
||||
return strconv.Itoa(port), err == nil
|
||||
})
|
||||
|
||||
var filter string
|
||||
if jailWebsiteMode == "cc" {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
} else {
|
||||
filter = `
|
||||
[Definition]
|
||||
failregex = ^<HOST>\s-.*\s` + jailWebsitePath + `.*HTTP/.*$
|
||||
ignoreregex =
|
||||
`
|
||||
}
|
||||
if err = io.Write("/etc/fail2ban/filter.d/haozi-"+jailWebsiteName+"-"+jailWebsiteMode+".conf", filter, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
jail.Name = req.WebsiteName + "-" + req.WebsiteMode
|
||||
jail.Filter = panelFilterPrefix + jail.Name
|
||||
jail.Port = strings.Join(ports, ",")
|
||||
jail.LogPath = app.Root + "/sites/" + website.Name + "/log/access.log"
|
||||
|
||||
case "service":
|
||||
var filter string
|
||||
var port string
|
||||
var filter, port string
|
||||
var err error
|
||||
switch jailName {
|
||||
switch req.Name {
|
||||
case "ssh":
|
||||
filter = "sshd"
|
||||
port, err = shell.Execf("cat /etc/ssh/sshd_config | grep 'Port ' | awk '{print $2}' | paste -sd ','")
|
||||
@@ -193,99 +121,130 @@ ignoreregex =
|
||||
return
|
||||
}
|
||||
|
||||
rule := `
|
||||
# ` + jailName + `-START
|
||||
[` + jailName + `]
|
||||
enabled = true
|
||||
filter = ` + filter + `
|
||||
port = ` + port + `
|
||||
maxretry = ` + jailMaxRetry + `
|
||||
findtime = ` + jailFindTime + `
|
||||
bantime = ` + jailBanTime + `
|
||||
# ` + jailName + `-END
|
||||
`
|
||||
raw += rule
|
||||
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
jail.Name = req.Name
|
||||
jail.Filter = filter
|
||||
jail.Port = port
|
||||
}
|
||||
|
||||
if io.Exists(jailPath(jail.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule already exists"))
|
||||
return
|
||||
}
|
||||
|
||||
// 网站规则的过滤器由面板生成,服务规则复用 fail2ban 自带的
|
||||
if req.Type == "website" {
|
||||
failRegex := `^<HOST>\s-.*HTTP/.*$`
|
||||
if req.WebsiteMode == "path" {
|
||||
failRegex = `^<HOST>\s-.*\s` + req.WebsitePath + `.*HTTP/.*$`
|
||||
}
|
||||
filter := "[Definition]\nfailregex = " + failRegex + "\nignoreregex =\n"
|
||||
if err = io.Write(filterPath(jail.Filter), filter, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("fail2ban-client reload"); err != nil {
|
||||
if err = writeJail(jail); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
s.reload(w)
|
||||
}
|
||||
|
||||
// Delete 删除规则
|
||||
func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Delete](r)
|
||||
// Update 修改规则
|
||||
func (s *App) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Update](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
jail, err := readJail(req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(raw, "["+req.Name+"]") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule not found"))
|
||||
return
|
||||
}
|
||||
|
||||
rule := str.Cut(raw, "# "+req.Name+"-START", "# "+req.Name+"-END")
|
||||
raw = strings.ReplaceAll(raw, "\n# "+req.Name+"-START"+rule+"# "+req.Name+"-END", "")
|
||||
raw = strings.TrimSpace(raw)
|
||||
if err := io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
jail.Enabled = req.Enabled
|
||||
jail.MaxRetry = req.MaxRetry
|
||||
jail.FindTime = req.FindTime
|
||||
jail.BanTime = req.BanTime
|
||||
|
||||
if err = writeJail(jail); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
s.reload(w)
|
||||
}
|
||||
|
||||
// BanList 获取封禁列表
|
||||
func (s *App) BanList(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[BanList](r)
|
||||
// Delete 删除规则
|
||||
func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[JailName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
currentlyBan, err := shell.Execf(`fail2ban-client status %s | grep "Currently banned" | awk '{print $4}'`, req.Name)
|
||||
jail, err := readJail(req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get current banned list"))
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("rule not found"))
|
||||
return
|
||||
}
|
||||
totalBan, err := shell.Execf(`fail2ban-client status %s | grep "Total banned" | awk '{print $4}'`, req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get total banned list"))
|
||||
return
|
||||
}
|
||||
bannedIp, err := shell.Execf(`fail2ban-client status %s | grep "Banned IP list" | sed 's/.*Banned IP list:[[:space:]]*//'`, req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get banned ip list"))
|
||||
return
|
||||
}
|
||||
bannedIpList := strings.Split(bannedIp, " ")
|
||||
|
||||
list := lo.FilterMap(bannedIpList, func(ip string, _ int) (map[string]string, bool) {
|
||||
if len(ip) == 0 {
|
||||
return nil, false
|
||||
if err = io.Remove(jailPath(jail.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
// 面板生成的过滤器随规则一起删除,fail2ban 自带的保留
|
||||
if strings.HasPrefix(jail.Filter, panelFilterPrefix) {
|
||||
if err = io.Remove(filterPath(jail.Filter)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.reload(w)
|
||||
}
|
||||
|
||||
// BanList 获取封禁列表
|
||||
func (s *App) BanList(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[JailName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
out, err := shell.Execf("fail2ban-client status %s", req.Name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get the status of rule %s: %v", req.Name, err))
|
||||
return
|
||||
}
|
||||
|
||||
// 输出形如 " |- Currently banned: 1",行首有树状前缀,按首个冒号切分后比对尾部
|
||||
var currentlyBan, totalBan, bannedIP string
|
||||
for line := range strings.SplitSeq(out, "\n") {
|
||||
label, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasSuffix(label, "Currently banned"):
|
||||
currentlyBan = strings.TrimSpace(value)
|
||||
case strings.HasSuffix(label, "Total banned"):
|
||||
totalBan = strings.TrimSpace(value)
|
||||
case strings.HasSuffix(label, "Banned IP list"):
|
||||
bannedIP = strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
|
||||
list := lo.Map(strings.Fields(bannedIP), func(ip string, _ int) map[string]string {
|
||||
return map[string]string{
|
||||
"name": req.Name,
|
||||
"ip": ip,
|
||||
}, true
|
||||
}
|
||||
})
|
||||
|
||||
service.Success(w, chix.M{
|
||||
@@ -319,45 +278,27 @@ func (s *App) SetWhiteList(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
// 正则替换
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*.*\n`)
|
||||
if reg.MatchString(raw) {
|
||||
raw = reg.ReplaceAllString(raw, "ignoreip = "+req.IP+"\n")
|
||||
} else {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse the ignoreip of fail2ban"))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/fail2ban/jail.local", raw, 0644); err != nil {
|
||||
raw, _ := io.Read(jailLocal)
|
||||
if err = io.Write(jailLocal, confval.SectionINI.SetIn(raw, "DEFAULT", "ignoreip", req.IP), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = shell.Execf("fail2ban-client reload"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
service.Success(w, nil)
|
||||
s.reload(w)
|
||||
}
|
||||
|
||||
// GetWhiteList 获取白名单
|
||||
func (s *App) GetWhiteList(w http.ResponseWriter, r *http.Request) {
|
||||
raw, err := io.Read("/etc/fail2ban/jail.local")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
reg := regexp.MustCompile(`ignoreip\s*=\s*(.*)\n`)
|
||||
if reg.MatchString(raw) {
|
||||
ignoreIp := reg.FindStringSubmatch(raw)[1]
|
||||
service.Success(w, ignoreIp)
|
||||
} else {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to parse the ignoreip of fail2ban"))
|
||||
return
|
||||
}
|
||||
raw, _ := io.Read(jailLocal)
|
||||
|
||||
service.Success(w, confval.SectionINI.GetIn(raw, "DEFAULT", "ignoreip"))
|
||||
}
|
||||
|
||||
func (s *App) reload(w http.ResponseWriter) {
|
||||
if _, err := shell.Execf("fail2ban-client reload"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package fail2ban
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
)
|
||||
|
||||
const (
|
||||
// jailLocal 只承载 [DEFAULT] 全局项,规则一律放 jail.d
|
||||
jailLocal = "/etc/fail2ban/jail.local"
|
||||
// jailDDir fail2ban 原生的 drop-in 目录,.local 后缀的读取优先级最高
|
||||
jailDDir = "/etc/fail2ban/jail.d"
|
||||
filterDir = "/etc/fail2ban/filter.d"
|
||||
// panelFilterPrefix 面板为网站规则生成的过滤器前缀,用于区分 fail2ban 自带的过滤器
|
||||
panelFilterPrefix = "haozi-"
|
||||
)
|
||||
|
||||
func jailPath(name string) string {
|
||||
return filepath.Join(jailDDir, name+".local")
|
||||
}
|
||||
|
||||
func filterPath(filter string) string {
|
||||
return filepath.Join(filterDir, filter+".conf")
|
||||
}
|
||||
|
||||
func readJail(name string) (Jail, error) {
|
||||
raw, err := io.Read(jailPath(name))
|
||||
if err != nil {
|
||||
return Jail{}, err
|
||||
}
|
||||
|
||||
return Jail{
|
||||
Name: name,
|
||||
Enabled: cast.ToBool(confval.SectionINI.GetIn(raw, name, "enabled")),
|
||||
MaxRetry: cast.ToInt(confval.SectionINI.GetIn(raw, name, "maxretry")),
|
||||
FindTime: cast.ToInt(confval.SectionINI.GetIn(raw, name, "findtime")),
|
||||
BanTime: cast.ToInt(confval.SectionINI.GetIn(raw, name, "bantime")),
|
||||
Filter: confval.SectionINI.GetIn(raw, name, "filter"),
|
||||
Port: confval.SectionINI.GetIn(raw, name, "port"),
|
||||
LogPath: confval.SectionINI.GetIn(raw, name, "logpath"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func listJails() ([]Jail, error) {
|
||||
// Glob 在目录不存在时返回空,无需额外判断
|
||||
paths, err := filepath.Glob(jailPath("*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jails := make([]Jail, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
jail, err := readJail(strings.TrimSuffix(filepath.Base(path), ".local"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jails = append(jails, jail)
|
||||
}
|
||||
|
||||
return jails, nil
|
||||
}
|
||||
|
||||
func writeJail(jail Jail) error {
|
||||
config := fmt.Sprintf(
|
||||
"[%s]\nenabled = %t\nfilter = %s\nport = %s\nmaxretry = %d\nfindtime = %d\nbantime = %d\n",
|
||||
jail.Name, jail.Enabled, jail.Filter, jail.Port, jail.MaxRetry, jail.FindTime, jail.BanTime,
|
||||
)
|
||||
if jail.LogPath != "" {
|
||||
config += "backend = auto\nlogpath = " + jail.LogPath + "\n"
|
||||
}
|
||||
|
||||
return io.Write(jailPath(jail.Name), config, 0644)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package fail2ban
|
||||
|
||||
type Add struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Name string `json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Type string `json:"type" validate:"required && in:service,website"`
|
||||
MaxRetry int `json:"maxretry" validate:"required && min:1"`
|
||||
FindTime int `json:"findtime" validate:"required && min:1"`
|
||||
@@ -11,16 +11,21 @@ type Add struct {
|
||||
WebsitePath string `json:"website_path"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
type JailName struct {
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
}
|
||||
|
||||
type BanList struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
// Update 只允许改这几项,过滤器、端口与日志路径随规则文件原样保留
|
||||
type Update struct {
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Enabled bool `json:"enabled"`
|
||||
MaxRetry int `json:"max_retry" validate:"required && min:1"`
|
||||
FindTime int `json:"find_time" validate:"required && min:1"`
|
||||
BanTime int `json:"ban_time" validate:"required && min:1"`
|
||||
}
|
||||
|
||||
type Unban struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
IP string `json:"ip" validate:"required && ip"`
|
||||
}
|
||||
|
||||
|
||||
@@ -6,4 +6,9 @@ type Jail struct {
|
||||
MaxRetry int `json:"max_retry"`
|
||||
FindTime int `json:"find_time"`
|
||||
BanTime int `json:"ban_time"`
|
||||
|
||||
// 以下几项在新增时由面板推导,之后随规则文件原样保留
|
||||
Filter string `json:"filter"`
|
||||
Port string `json:"port"`
|
||||
LogPath string `json:"log_path"`
|
||||
}
|
||||
|
||||
+304
-14
@@ -5,18 +5,23 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -24,6 +29,20 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/user", s.GetUser)
|
||||
r.Post("/user", s.UpdateUser)
|
||||
r.Get("/server", s.GetServer)
|
||||
r.Post("/server", s.UpdateServer)
|
||||
r.Get("/client", s.GetClient)
|
||||
r.Post("/client", s.UpdateClient)
|
||||
r.Get("/proxies", s.Proxies)
|
||||
r.Post("/proxies", s.CreateProxy)
|
||||
r.Get("/proxies/{name}", s.GetProxy)
|
||||
r.Post("/proxies/{name}", s.UpdateProxy)
|
||||
r.Delete("/proxies/{name}", s.DeleteProxy)
|
||||
r.Get("/visitors", s.Visitors)
|
||||
r.Post("/visitors", s.CreateVisitor)
|
||||
r.Get("/visitors/{name}", s.GetVisitor)
|
||||
r.Post("/visitors/{name}", s.UpdateVisitor)
|
||||
r.Delete("/visitors/{name}", s.DeleteVisitor)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
@@ -39,7 +58,7 @@ func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/frp/%s.toml", app.Root, req.Name))
|
||||
config, err := io.Read(confPath(req.Name))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -55,7 +74,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/frp/%s.toml", app.Root, req.Name), req.Config, 0644); err != nil {
|
||||
if err = io.Write(confPath(req.Name), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -114,18 +133,19 @@ func (s *App) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
hasGroup := groupRegex.MatchString(content)
|
||||
|
||||
// 替换或添加 User 和 Group 配置
|
||||
if hasUser && hasGroup {
|
||||
switch {
|
||||
case hasUser && hasGroup:
|
||||
// 两者都存在,分别替换
|
||||
content = userRegex.ReplaceAllString(content, fmt.Sprintf("User=%s", req.User))
|
||||
content = groupRegex.ReplaceAllString(content, fmt.Sprintf("Group=%s", req.Group))
|
||||
} else if hasUser && !hasGroup {
|
||||
content = userRegex.ReplaceAllString(content, "User="+req.User)
|
||||
content = groupRegex.ReplaceAllString(content, "Group="+req.Group)
|
||||
case hasUser:
|
||||
// 只有 User,替换 User 并添加 Group
|
||||
content = userRegex.ReplaceAllString(content, fmt.Sprintf("User=%s\nGroup=%s", req.User, req.Group))
|
||||
} else if !hasUser && hasGroup {
|
||||
case hasGroup:
|
||||
// 只有 Group,添加 User 并替换 Group
|
||||
content = serviceRegex.ReplaceAllString(content, fmt.Sprintf("[Service]\nUser=%s", req.User))
|
||||
content = groupRegex.ReplaceAllString(content, fmt.Sprintf("Group=%s", req.Group))
|
||||
} else {
|
||||
content = serviceRegex.ReplaceAllString(content, "[Service]\nUser="+req.User)
|
||||
content = groupRegex.ReplaceAllString(content, "Group="+req.Group)
|
||||
default:
|
||||
// 两者都不存在,在 [Service] 后添加两者
|
||||
content = serviceRegex.ReplaceAllString(content, fmt.Sprintf("[Service]\nUser=%s\nGroup=%s", req.User, req.Group))
|
||||
}
|
||||
@@ -146,3 +166,273 @@ func (s *App) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetServer 获取 frps 可视化参数
|
||||
func (s *App) GetServer(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(confPath("frps"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := new(ServerTune)
|
||||
readTune(config, serverFields(tune))
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateServer 更新 frps 可视化参数
|
||||
func (s *App) UpdateServer(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ServerTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
path := confPath("frps")
|
||||
config, err := io.Read(path)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(path, writeTune(config, serverFields(req)), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("frps"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetClient 获取 frpc 可视化公共参数
|
||||
func (s *App) GetClient(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(confPath("frpc"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := new(ClientTune)
|
||||
readTune(config, clientFields(tune))
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateClient 更新 frpc 可视化公共参数
|
||||
func (s *App) UpdateClient(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ClientTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
path := confPath("frpc")
|
||||
config, err := io.Read(path)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(path, writeTune(config, clientFields(req)), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("frpc"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Proxies 代理列表
|
||||
func (s *App) Proxies(w http.ResponseWriter, r *http.Request) {
|
||||
confD, err := listConfD()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, confD.Proxies)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateProxy 新增代理
|
||||
func (s *App) CreateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Proxy](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if io.Exists(itemPath(proxyPrefix, req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("proxy %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
s.save(w, proxyPrefix, req.Name, ConfD{Proxies: []Proxy{*req}})
|
||||
}
|
||||
|
||||
// GetProxy 获取单个代理
|
||||
func (s *App) GetProxy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ItemName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confD, err := readConfD(itemPath(proxyPrefix, req.Name))
|
||||
if err != nil || len(confD.Proxies) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("proxy %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, confD.Proxies[0])
|
||||
}
|
||||
|
||||
// UpdateProxy 更新代理
|
||||
func (s *App) UpdateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Proxy](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !io.Exists(itemPath(proxyPrefix, req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("proxy %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
s.save(w, proxyPrefix, req.Name, ConfD{Proxies: []Proxy{*req}})
|
||||
}
|
||||
|
||||
// DeleteProxy 删除代理
|
||||
func (s *App) DeleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ItemName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Remove(itemPath(proxyPrefix, req.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("frpc"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Visitors 访问者列表
|
||||
func (s *App) Visitors(w http.ResponseWriter, r *http.Request) {
|
||||
confD, err := listConfD()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, confD.Visitors)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": paged,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateVisitor 新增访问者
|
||||
func (s *App) CreateVisitor(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Visitor](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if io.Exists(itemPath(visitorPrefix, req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("visitor %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
s.save(w, visitorPrefix, req.Name, ConfD{Visitors: []Visitor{*req}})
|
||||
}
|
||||
|
||||
// GetVisitor 获取单个访问者
|
||||
func (s *App) GetVisitor(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ItemName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confD, err := readConfD(itemPath(visitorPrefix, req.Name))
|
||||
if err != nil || len(confD.Visitors) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("visitor %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, confD.Visitors[0])
|
||||
}
|
||||
|
||||
// UpdateVisitor 更新访问者
|
||||
func (s *App) UpdateVisitor(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Visitor](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !io.Exists(itemPath(visitorPrefix, req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("visitor %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
s.save(w, visitorPrefix, req.Name, ConfD{Visitors: []Visitor{*req}})
|
||||
}
|
||||
|
||||
// DeleteVisitor 删除访问者
|
||||
func (s *App) DeleteVisitor(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ItemName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Remove(itemPath(visitorPrefix, req.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("frpc"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) save(w http.ResponseWriter, prefix, name string, confD ConfD) {
|
||||
if err := writeConfD(prefix, name, confD); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("frpc"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package frp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
)
|
||||
|
||||
const (
|
||||
proxyPrefix = "proxy"
|
||||
visitorPrefix = "visitor"
|
||||
)
|
||||
|
||||
func confPath(name string) string {
|
||||
return filepath.Join(app.Root, "server", "frp", name+".toml")
|
||||
}
|
||||
|
||||
func confDDir() string {
|
||||
return filepath.Join(app.Root, "server", "frp", "conf.d")
|
||||
}
|
||||
|
||||
func confDGlob() string {
|
||||
return filepath.Join(confDDir(), "*.toml")
|
||||
}
|
||||
|
||||
func itemPath(prefix, name string) string {
|
||||
return filepath.Join(confDDir(), prefix+"-"+name+".toml")
|
||||
}
|
||||
|
||||
func readConfD(path string) (ConfD, error) {
|
||||
raw, err := io.Read(path)
|
||||
if err != nil {
|
||||
return ConfD{}, err
|
||||
}
|
||||
|
||||
var confD ConfD
|
||||
return confD, toml.Unmarshal([]byte(raw), &confD)
|
||||
}
|
||||
|
||||
func listConfD() (ConfD, error) {
|
||||
var all ConfD
|
||||
|
||||
// Glob 在目录不存在时返回空,无需额外判断
|
||||
paths, err := filepath.Glob(confDGlob())
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
confD, err := readConfD(path)
|
||||
if err != nil {
|
||||
return all, err
|
||||
}
|
||||
|
||||
all.Proxies = append(all.Proxies, confD.Proxies...)
|
||||
all.Visitors = append(all.Visitors, confD.Visitors...)
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
func writeConfD(prefix, name string, confD ConfD) error {
|
||||
if err := os.MkdirAll(confDDir(), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
raw, err := toml.Marshal(confD)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return io.Write(itemPath(prefix, name), string(raw), 0644)
|
||||
}
|
||||
@@ -14,3 +14,8 @@ type UpdateUser struct {
|
||||
User string `form:"user" json:"user" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
|
||||
Group string `form:"group" json:"group" validate:"required && regex:\"^[a-zA-Z0-9_-]+$\""`
|
||||
}
|
||||
|
||||
// ItemName 代理或访问者的名称,来自路径参数
|
||||
type ItemName struct {
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package frp
|
||||
|
||||
import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
)
|
||||
|
||||
type tuneKind uint8
|
||||
|
||||
const (
|
||||
tuneString tuneKind = iota
|
||||
tuneInt
|
||||
tuneBool
|
||||
)
|
||||
|
||||
// tuneField 可视化参数与 TOML 键的对应关系,表单一律用字符串收值,kind 决定写回时的字面量类型
|
||||
type tuneField struct {
|
||||
key string
|
||||
value *string
|
||||
kind tuneKind
|
||||
}
|
||||
|
||||
// literal 空串表示注释掉该项
|
||||
func (f tuneField) literal() any {
|
||||
if *f.value == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch f.kind {
|
||||
case tuneInt:
|
||||
return cast.ToInt(*f.value)
|
||||
case tuneBool:
|
||||
return cast.ToBool(*f.value)
|
||||
default:
|
||||
return *f.value
|
||||
}
|
||||
}
|
||||
|
||||
func readTune(config string, fields []tuneField) {
|
||||
for _, f := range fields {
|
||||
*f.value = confval.GetTOML(config, f.key)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTune(config string, fields []tuneField) string {
|
||||
for _, f := range fields {
|
||||
config = confval.SetTOML(config, f.key, f.literal())
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
func serverFields(t *ServerTune) []tuneField {
|
||||
return []tuneField{
|
||||
{"bindAddr", &t.BindAddr, tuneString},
|
||||
{"bindPort", &t.BindPort, tuneInt},
|
||||
{"kcpBindPort", &t.KCPBindPort, tuneInt},
|
||||
{"quicBindPort", &t.QUICBindPort, tuneInt},
|
||||
{"proxyBindAddr", &t.ProxyBindAddr, tuneString},
|
||||
{"subDomainHost", &t.SubDomainHost, tuneString},
|
||||
{"maxPortsPerClient", &t.MaxPortsPerClient, tuneInt},
|
||||
{"udpPacketSize", &t.UDPPacketSize, tuneInt},
|
||||
{"detailedErrorsToClient", &t.DetailedErrorsToClient, tuneBool},
|
||||
|
||||
{"auth.method", &t.AuthMethod, tuneString},
|
||||
{"auth.token", &t.AuthToken, tuneString},
|
||||
|
||||
{"vhostHTTPPort", &t.VhostHTTPPort, tuneInt},
|
||||
{"vhostHTTPSPort", &t.VhostHTTPSPort, tuneInt},
|
||||
{"vhostHTTPTimeout", &t.VhostHTTPTimeout, tuneInt},
|
||||
{"tcpmuxHTTPConnectPort", &t.TCPMuxHTTPConnectPort, tuneInt},
|
||||
{"custom404Page", &t.Custom404Page, tuneString},
|
||||
|
||||
{"webServer.addr", &t.WebServerAddr, tuneString},
|
||||
{"webServer.port", &t.WebServerPort, tuneInt},
|
||||
{"webServer.user", &t.WebServerUser, tuneString},
|
||||
{"webServer.password", &t.WebServerPassword, tuneString},
|
||||
{"webServer.pprofEnable", &t.WebServerPprofEnable, tuneBool},
|
||||
{"enablePrometheus", &t.EnablePrometheus, tuneBool},
|
||||
|
||||
{"transport.maxPoolCount", &t.TransportMaxPoolCount, tuneInt},
|
||||
{"transport.tcpMux", &t.TransportTCPMux, tuneBool},
|
||||
{"transport.tcpMuxKeepaliveInterval", &t.TransportTCPMuxKeepaliveInterval, tuneInt},
|
||||
{"transport.tcpKeepalive", &t.TransportTCPKeepalive, tuneInt},
|
||||
{"transport.heartbeatTimeout", &t.TransportHeartbeatTimeout, tuneInt},
|
||||
{"transport.tls.force", &t.TransportTLSForce, tuneBool},
|
||||
{"transport.tls.certFile", &t.TransportTLSCertFile, tuneString},
|
||||
{"transport.tls.keyFile", &t.TransportTLSKeyFile, tuneString},
|
||||
{"transport.tls.trustedCaFile", &t.TransportTLSTrustedCaFile, tuneString},
|
||||
|
||||
{"log.to", &t.LogTo, tuneString},
|
||||
{"log.level", &t.LogLevel, tuneString},
|
||||
{"log.maxDays", &t.LogMaxDays, tuneInt},
|
||||
{"log.disablePrintColor", &t.LogDisablePrintColor, tuneBool},
|
||||
}
|
||||
}
|
||||
|
||||
func clientFields(t *ClientTune) []tuneField {
|
||||
return []tuneField{
|
||||
{"user", &t.User, tuneString},
|
||||
{"serverAddr", &t.ServerAddr, tuneString},
|
||||
{"serverPort", &t.ServerPort, tuneInt},
|
||||
{"loginFailExit", &t.LoginFailExit, tuneBool},
|
||||
{"natHoleStunServer", &t.NatHoleStunServer, tuneString},
|
||||
{"dnsServer", &t.DNSServer, tuneString},
|
||||
{"udpPacketSize", &t.UDPPacketSize, tuneInt},
|
||||
|
||||
{"auth.method", &t.AuthMethod, tuneString},
|
||||
{"auth.token", &t.AuthToken, tuneString},
|
||||
|
||||
{"transport.protocol", &t.TransportProtocol, tuneString},
|
||||
{"transport.poolCount", &t.TransportPoolCount, tuneInt},
|
||||
{"transport.tcpMux", &t.TransportTCPMux, tuneBool},
|
||||
{"transport.tcpMuxKeepaliveInterval", &t.TransportTCPMuxKeepaliveInterval, tuneInt},
|
||||
{"transport.dialServerTimeout", &t.TransportDialServerTimeout, tuneInt},
|
||||
{"transport.dialServerKeepalive", &t.TransportDialServerKeepalive, tuneInt},
|
||||
{"transport.heartbeatInterval", &t.TransportHeartbeatInterval, tuneInt},
|
||||
{"transport.heartbeatTimeout", &t.TransportHeartbeatTimeout, tuneInt},
|
||||
{"transport.connectServerLocalIP", &t.TransportConnectServerLocalIP, tuneString},
|
||||
{"transport.proxyURL", &t.TransportProxyURL, tuneString},
|
||||
{"transport.tls.enable", &t.TransportTLSEnable, tuneBool},
|
||||
{"transport.tls.certFile", &t.TransportTLSCertFile, tuneString},
|
||||
{"transport.tls.keyFile", &t.TransportTLSKeyFile, tuneString},
|
||||
{"transport.tls.trustedCaFile", &t.TransportTLSTrustedCaFile, tuneString},
|
||||
{"transport.tls.serverName", &t.TransportTLSServerName, tuneString},
|
||||
|
||||
{"webServer.addr", &t.WebServerAddr, tuneString},
|
||||
{"webServer.port", &t.WebServerPort, tuneInt},
|
||||
{"webServer.user", &t.WebServerUser, tuneString},
|
||||
{"webServer.password", &t.WebServerPassword, tuneString},
|
||||
{"webServer.pprofEnable", &t.WebServerPprofEnable, tuneBool},
|
||||
|
||||
{"log.to", &t.LogTo, tuneString},
|
||||
{"log.level", &t.LogLevel, tuneString},
|
||||
{"log.maxDays", &t.LogMaxDays, tuneInt},
|
||||
{"log.disablePrintColor", &t.LogDisablePrintColor, tuneBool},
|
||||
}
|
||||
}
|
||||
@@ -14,3 +14,216 @@ type UserInfo struct {
|
||||
User string `json:"user"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
// ConfD conf.d 目录下的配置文件,只承载代理与访问者
|
||||
type ConfD struct {
|
||||
Proxies []Proxy `toml:"proxies,omitempty"`
|
||||
Visitors []Visitor `toml:"visitors,omitempty"`
|
||||
}
|
||||
|
||||
// Proxy 代理配置,字段对齐 frp pkg/config/v1 的 ProxyConfigurer 实现
|
||||
type Proxy struct {
|
||||
Name string `toml:"name" form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Type string `toml:"type" json:"type" validate:"required && in:tcp,udp,http,https,tcpmux,stcp,sudp,xtcp"`
|
||||
Enabled *bool `toml:"enabled,omitempty" json:"enabled"`
|
||||
|
||||
LocalIP string `toml:"localIP,omitempty" json:"local_ip"`
|
||||
LocalPort int `toml:"localPort,omitempty" json:"local_port"`
|
||||
|
||||
// tcp、udp
|
||||
RemotePort int `toml:"remotePort,omitempty" json:"remote_port"`
|
||||
|
||||
// http、https、tcpmux
|
||||
CustomDomains []string `toml:"customDomains,omitempty" json:"custom_domains"`
|
||||
Subdomain string `toml:"subdomain,omitempty" json:"subdomain"`
|
||||
|
||||
// http、tcpmux
|
||||
HTTPUser string `toml:"httpUser,omitempty" json:"http_user"`
|
||||
HTTPPassword string `toml:"httpPassword,omitempty" json:"http_password"`
|
||||
RouteByHTTPUser string `toml:"routeByHTTPUser,omitempty" json:"route_by_http_user"`
|
||||
|
||||
// http
|
||||
Locations []string `toml:"locations,omitempty" json:"locations"`
|
||||
HostHeaderRewrite string `toml:"hostHeaderRewrite,omitempty" json:"host_header_rewrite"`
|
||||
RequestHeaders *HeaderOperations `toml:"requestHeaders,omitempty" json:"request_headers"`
|
||||
ResponseHeaders *HeaderOperations `toml:"responseHeaders,omitempty" json:"response_headers"`
|
||||
|
||||
// tcpmux
|
||||
Multiplexer string `toml:"multiplexer,omitempty" json:"multiplexer"`
|
||||
|
||||
// stcp、sudp、xtcp
|
||||
SecretKey string `toml:"secretKey,omitempty" json:"secret_key"`
|
||||
AllowUsers []string `toml:"allowUsers,omitempty" json:"allow_users"`
|
||||
|
||||
Transport *ProxyTransport `toml:"transport,omitempty" json:"transport"`
|
||||
LoadBalancer *LoadBalancer `toml:"loadBalancer,omitempty" json:"load_balancer"`
|
||||
HealthCheck *HealthCheck `toml:"healthCheck,omitempty" json:"health_check"`
|
||||
Plugin *Plugin `toml:"plugin,omitempty" json:"plugin"`
|
||||
|
||||
Metadatas map[string]string `toml:"metadatas,omitempty" json:"metadatas"`
|
||||
Annotations map[string]string `toml:"annotations,omitempty" json:"annotations"`
|
||||
}
|
||||
|
||||
// Visitor 访问者配置,对应 stcp、sudp、xtcp 的客户端一侧
|
||||
type Visitor struct {
|
||||
Name string `toml:"name" form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Type string `toml:"type" json:"type" validate:"required && in:stcp,sudp,xtcp"`
|
||||
Enabled *bool `toml:"enabled,omitempty" json:"enabled"`
|
||||
|
||||
ServerUser string `toml:"serverUser,omitempty" json:"server_user"`
|
||||
ServerName string `toml:"serverName,omitempty" json:"server_name"`
|
||||
SecretKey string `toml:"secretKey,omitempty" json:"secret_key"`
|
||||
BindAddr string `toml:"bindAddr,omitempty" json:"bind_addr"`
|
||||
BindPort int `toml:"bindPort,omitempty" json:"bind_port"`
|
||||
|
||||
// xtcp
|
||||
Protocol string `toml:"protocol,omitempty" json:"protocol"`
|
||||
KeepTunnelOpen bool `toml:"keepTunnelOpen,omitempty" json:"keep_tunnel_open"`
|
||||
MaxRetriesAnHour int `toml:"maxRetriesAnHour,omitempty" json:"max_retries_an_hour"`
|
||||
MinRetryInterval int `toml:"minRetryInterval,omitempty" json:"min_retry_interval"`
|
||||
FallbackTo string `toml:"fallbackTo,omitempty" json:"fallback_to"`
|
||||
FallbackTimeoutMs int `toml:"fallbackTimeoutMs,omitempty" json:"fallback_timeout_ms"`
|
||||
|
||||
Transport *VisitorTransport `toml:"transport,omitempty" json:"transport"`
|
||||
}
|
||||
|
||||
type ProxyTransport struct {
|
||||
UseEncryption bool `toml:"useEncryption,omitempty" json:"use_encryption"`
|
||||
UseCompression bool `toml:"useCompression,omitempty" json:"use_compression"`
|
||||
BandwidthLimit string `toml:"bandwidthLimit,omitempty" json:"bandwidth_limit"`
|
||||
BandwidthLimitMode string `toml:"bandwidthLimitMode,omitempty" json:"bandwidth_limit_mode"`
|
||||
ProxyProtocolVersion string `toml:"proxyProtocolVersion,omitempty" json:"proxy_protocol_version"`
|
||||
}
|
||||
|
||||
type VisitorTransport struct {
|
||||
UseEncryption bool `toml:"useEncryption,omitempty" json:"use_encryption"`
|
||||
UseCompression bool `toml:"useCompression,omitempty" json:"use_compression"`
|
||||
}
|
||||
|
||||
type LoadBalancer struct {
|
||||
Group string `toml:"group,omitempty" json:"group"`
|
||||
GroupKey string `toml:"groupKey,omitempty" json:"group_key"`
|
||||
}
|
||||
|
||||
type HealthCheck struct {
|
||||
Type string `toml:"type" json:"type"`
|
||||
TimeoutSeconds int `toml:"timeoutSeconds,omitempty" json:"timeout_seconds"`
|
||||
MaxFailed int `toml:"maxFailed,omitempty" json:"max_failed"`
|
||||
IntervalSeconds int `toml:"intervalSeconds,omitempty" json:"interval_seconds"`
|
||||
Path string `toml:"path,omitempty" json:"path"`
|
||||
}
|
||||
|
||||
type HeaderOperations struct {
|
||||
Set map[string]string `toml:"set,omitempty" json:"set"`
|
||||
}
|
||||
|
||||
// Plugin 客户端插件配置,各插件字段的并集
|
||||
type Plugin struct {
|
||||
Type string `toml:"type" json:"type"`
|
||||
|
||||
// unix_domain_socket
|
||||
UnixPath string `toml:"unixPath,omitempty" json:"unix_path"`
|
||||
|
||||
// http_proxy、static_file
|
||||
HTTPUser string `toml:"httpUser,omitempty" json:"http_user"`
|
||||
HTTPPassword string `toml:"httpPassword,omitempty" json:"http_password"`
|
||||
|
||||
// socks5
|
||||
Username string `toml:"username,omitempty" json:"username"`
|
||||
Password string `toml:"password,omitempty" json:"password"`
|
||||
|
||||
// static_file
|
||||
LocalPath string `toml:"localPath,omitempty" json:"local_path"`
|
||||
StripPrefix string `toml:"stripPrefix,omitempty" json:"strip_prefix"`
|
||||
|
||||
// https2http、https2https、http2https、http2http、tls2raw
|
||||
LocalAddr string `toml:"localAddr,omitempty" json:"local_addr"`
|
||||
HostHeaderRewrite string `toml:"hostHeaderRewrite,omitempty" json:"host_header_rewrite"`
|
||||
CrtPath string `toml:"crtPath,omitempty" json:"crt_path"`
|
||||
KeyPath string `toml:"keyPath,omitempty" json:"key_path"`
|
||||
}
|
||||
|
||||
// ServerTune frps 可视化参数
|
||||
type ServerTune struct {
|
||||
BindAddr string `form:"bind_addr" json:"bind_addr"`
|
||||
BindPort string `form:"bind_port" json:"bind_port"`
|
||||
KCPBindPort string `form:"kcp_bind_port" json:"kcp_bind_port"`
|
||||
QUICBindPort string `form:"quic_bind_port" json:"quic_bind_port"`
|
||||
ProxyBindAddr string `form:"proxy_bind_addr" json:"proxy_bind_addr"`
|
||||
SubDomainHost string `form:"sub_domain_host" json:"sub_domain_host"`
|
||||
MaxPortsPerClient string `form:"max_ports_per_client" json:"max_ports_per_client"`
|
||||
UDPPacketSize string `form:"udp_packet_size" json:"udp_packet_size"`
|
||||
DetailedErrorsToClient string `form:"detailed_errors_to_client" json:"detailed_errors_to_client"`
|
||||
|
||||
AuthMethod string `form:"auth_method" json:"auth_method"`
|
||||
AuthToken string `form:"auth_token" json:"auth_token"`
|
||||
|
||||
VhostHTTPPort string `form:"vhost_http_port" json:"vhost_http_port"`
|
||||
VhostHTTPSPort string `form:"vhost_https_port" json:"vhost_https_port"`
|
||||
VhostHTTPTimeout string `form:"vhost_http_timeout" json:"vhost_http_timeout"`
|
||||
TCPMuxHTTPConnectPort string `form:"tcpmux_http_connect_port" json:"tcpmux_http_connect_port"`
|
||||
Custom404Page string `form:"custom_404_page" json:"custom_404_page"`
|
||||
|
||||
WebServerAddr string `form:"web_server_addr" json:"web_server_addr"`
|
||||
WebServerPort string `form:"web_server_port" json:"web_server_port"`
|
||||
WebServerUser string `form:"web_server_user" json:"web_server_user"`
|
||||
WebServerPassword string `form:"web_server_password" json:"web_server_password"`
|
||||
WebServerPprofEnable string `form:"web_server_pprof_enable" json:"web_server_pprof_enable"`
|
||||
EnablePrometheus string `form:"enable_prometheus" json:"enable_prometheus"`
|
||||
|
||||
TransportMaxPoolCount string `form:"transport_max_pool_count" json:"transport_max_pool_count"`
|
||||
TransportTCPMux string `form:"transport_tcp_mux" json:"transport_tcp_mux"`
|
||||
TransportTCPMuxKeepaliveInterval string `form:"transport_tcp_mux_keepalive_interval" json:"transport_tcp_mux_keepalive_interval"`
|
||||
TransportTCPKeepalive string `form:"transport_tcp_keepalive" json:"transport_tcp_keepalive"`
|
||||
TransportHeartbeatTimeout string `form:"transport_heartbeat_timeout" json:"transport_heartbeat_timeout"`
|
||||
TransportTLSForce string `form:"transport_tls_force" json:"transport_tls_force"`
|
||||
TransportTLSCertFile string `form:"transport_tls_cert_file" json:"transport_tls_cert_file"`
|
||||
TransportTLSKeyFile string `form:"transport_tls_key_file" json:"transport_tls_key_file"`
|
||||
TransportTLSTrustedCaFile string `form:"transport_tls_trusted_ca_file" json:"transport_tls_trusted_ca_file"`
|
||||
|
||||
LogTo string `form:"log_to" json:"log_to"`
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
LogMaxDays string `form:"log_max_days" json:"log_max_days"`
|
||||
LogDisablePrintColor string `form:"log_disable_print_color" json:"log_disable_print_color"`
|
||||
}
|
||||
|
||||
// ClientTune frpc 可视化公共参数
|
||||
type ClientTune struct {
|
||||
User string `form:"user" json:"user"`
|
||||
ServerAddr string `form:"server_addr" json:"server_addr"`
|
||||
ServerPort string `form:"server_port" json:"server_port"`
|
||||
LoginFailExit string `form:"login_fail_exit" json:"login_fail_exit"`
|
||||
NatHoleStunServer string `form:"nat_hole_stun_server" json:"nat_hole_stun_server"`
|
||||
DNSServer string `form:"dns_server" json:"dns_server"`
|
||||
UDPPacketSize string `form:"udp_packet_size" json:"udp_packet_size"`
|
||||
|
||||
AuthMethod string `form:"auth_method" json:"auth_method"`
|
||||
AuthToken string `form:"auth_token" json:"auth_token"`
|
||||
|
||||
TransportProtocol string `form:"transport_protocol" json:"transport_protocol"`
|
||||
TransportPoolCount string `form:"transport_pool_count" json:"transport_pool_count"`
|
||||
TransportTCPMux string `form:"transport_tcp_mux" json:"transport_tcp_mux"`
|
||||
TransportTCPMuxKeepaliveInterval string `form:"transport_tcp_mux_keepalive_interval" json:"transport_tcp_mux_keepalive_interval"`
|
||||
TransportDialServerTimeout string `form:"transport_dial_server_timeout" json:"transport_dial_server_timeout"`
|
||||
TransportDialServerKeepalive string `form:"transport_dial_server_keepalive" json:"transport_dial_server_keepalive"`
|
||||
TransportHeartbeatInterval string `form:"transport_heartbeat_interval" json:"transport_heartbeat_interval"`
|
||||
TransportHeartbeatTimeout string `form:"transport_heartbeat_timeout" json:"transport_heartbeat_timeout"`
|
||||
TransportConnectServerLocalIP string `form:"transport_connect_server_local_ip" json:"transport_connect_server_local_ip"`
|
||||
TransportProxyURL string `form:"transport_proxy_url" json:"transport_proxy_url"`
|
||||
TransportTLSEnable string `form:"transport_tls_enable" json:"transport_tls_enable"`
|
||||
TransportTLSCertFile string `form:"transport_tls_cert_file" json:"transport_tls_cert_file"`
|
||||
TransportTLSKeyFile string `form:"transport_tls_key_file" json:"transport_tls_key_file"`
|
||||
TransportTLSTrustedCaFile string `form:"transport_tls_trusted_ca_file" json:"transport_tls_trusted_ca_file"`
|
||||
TransportTLSServerName string `form:"transport_tls_server_name" json:"transport_tls_server_name"`
|
||||
|
||||
WebServerAddr string `form:"web_server_addr" json:"web_server_addr"`
|
||||
WebServerPort string `form:"web_server_port" json:"web_server_port"`
|
||||
WebServerUser string `form:"web_server_user" json:"web_server_user"`
|
||||
WebServerPassword string `form:"web_server_password" json:"web_server_password"`
|
||||
WebServerPprofEnable string `form:"web_server_pprof_enable" json:"web_server_pprof_enable"`
|
||||
|
||||
LogTo string `form:"log_to" json:"log_to"`
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
LogMaxDays string `form:"log_max_days" json:"log_max_days"`
|
||||
LogDisablePrintColor string `form:"log_disable_print_color" json:"log_disable_print_color"`
|
||||
}
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -30,26 +28,9 @@ func (s *App) Status() string {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(fmt.Sprintf("%s/server/gitea/app.ini", app.Root))
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, app.Root+"/server/gitea/app.ini")
|
||||
}
|
||||
|
||||
func (s *App) 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(fmt.Sprintf("%s/server/gitea/app.ini", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("gitea"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, app.Root+"/server/gitea/app.ini", "gitea")
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package gitea
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
+30
-150
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -14,6 +13,8 @@ import (
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -24,8 +25,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
return &App{t: t}, nil
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -59,7 +60,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// 从 defaults.ini 获取端口
|
||||
config, _ := io.Read(s.configPath())
|
||||
port := s.getINIValue(config, "server", "http_port")
|
||||
port := confval.SectionINI.GetIn(config, "server", "http_port")
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
}
|
||||
@@ -92,29 +93,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) 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(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("grafana"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "grafana")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Grafana 配置调整参数
|
||||
@@ -122,7 +105,7 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
get := func(section, key string) string {
|
||||
return s.getINIValue(config, section, key)
|
||||
return confval.SectionINI.GetIn(config, section, key)
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
@@ -168,31 +151,31 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
// [server]
|
||||
config = s.setINIValue(config, "server", "http_port", req.HTTPPort)
|
||||
config = s.setINIValue(config, "server", "domain", req.Domain)
|
||||
config = s.setINIValue(config, "server", "root_url", req.RootURL)
|
||||
config = s.setINIValue(config, "server", "protocol", req.Protocol)
|
||||
config = confval.SectionINI.SetIn(config, "server", "http_port", req.HTTPPort)
|
||||
config = confval.SectionINI.SetIn(config, "server", "domain", req.Domain)
|
||||
config = confval.SectionINI.SetIn(config, "server", "root_url", req.RootURL)
|
||||
config = confval.SectionINI.SetIn(config, "server", "protocol", req.Protocol)
|
||||
// [database]
|
||||
config = s.setINIValue(config, "database", "type", req.DBType)
|
||||
config = s.setINIValue(config, "database", "host", req.DBHost)
|
||||
config = s.setINIValue(config, "database", "name", req.DBName)
|
||||
config = s.setINIValue(config, "database", "user", req.DBUser)
|
||||
config = s.setINIValue(config, "database", "password", req.DBPassword)
|
||||
config = confval.SectionINI.SetIn(config, "database", "type", req.DBType)
|
||||
config = confval.SectionINI.SetIn(config, "database", "host", req.DBHost)
|
||||
config = confval.SectionINI.SetIn(config, "database", "name", req.DBName)
|
||||
config = confval.SectionINI.SetIn(config, "database", "user", req.DBUser)
|
||||
config = confval.SectionINI.SetIn(config, "database", "password", req.DBPassword)
|
||||
// [security]
|
||||
config = s.setINIValue(config, "security", "admin_user", req.AdminUser)
|
||||
config = s.setINIValue(config, "security", "admin_password", req.AdminPassword)
|
||||
config = confval.SectionINI.SetIn(config, "security", "admin_user", req.AdminUser)
|
||||
config = confval.SectionINI.SetIn(config, "security", "admin_password", req.AdminPassword)
|
||||
// [users]
|
||||
config = s.setINIValue(config, "users", "allow_sign_up", req.AllowSignUp)
|
||||
config = s.setINIValue(config, "users", "auto_assign_org_role", req.AutoAssignOrgRole)
|
||||
config = confval.SectionINI.SetIn(config, "users", "allow_sign_up", req.AllowSignUp)
|
||||
config = confval.SectionINI.SetIn(config, "users", "auto_assign_org_role", req.AutoAssignOrgRole)
|
||||
// [smtp]
|
||||
config = s.setINIValue(config, "smtp", "enabled", req.SMTPEnabled)
|
||||
config = s.setINIValue(config, "smtp", "host", req.SMTPHost)
|
||||
config = s.setINIValue(config, "smtp", "user", req.SMTPUser)
|
||||
config = s.setINIValue(config, "smtp", "password", req.SMTPPassword)
|
||||
config = s.setINIValue(config, "smtp", "from_address", req.SMTPFromAddress)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "enabled", req.SMTPEnabled)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "host", req.SMTPHost)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "user", req.SMTPUser)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "password", req.SMTPPassword)
|
||||
config = confval.SectionINI.SetIn(config, "smtp", "from_address", req.SMTPFromAddress)
|
||||
// [log]
|
||||
config = s.setINIValue(config, "log", "mode", req.LogMode)
|
||||
config = s.setINIValue(config, "log", "level", req.LogLevel)
|
||||
config = confval.SectionINI.SetIn(config, "log", "mode", req.LogMode)
|
||||
config = confval.SectionINI.SetIn(config, "log", "level", req.LogLevel)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -328,115 +311,12 @@ func (s *App) DeleteDataSource(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回 Grafana 主配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/grafana/conf/defaults.ini", app.Root)
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 配置中获取指定 section 下的 key 值
|
||||
func (s *App) getINIValue(content string, section string, key string) string {
|
||||
currentSection := ""
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, ";") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
currentSection = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
continue
|
||||
}
|
||||
if currentSection != section {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setINIValue 在 INI 配置中设置指定 section 下的 key 值
|
||||
func (s *App) setINIValue(content string, section string, key string, value string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
currentSection := ""
|
||||
found := false
|
||||
lastSectionLine := -1 // 目标 section 的最后一行索引
|
||||
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// 检测 section 头
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
// 如果离开目标 section 且未找到 key,在 section 末尾插入
|
||||
if currentSection == section && !found && lastSectionLine >= 0 {
|
||||
found = true
|
||||
if value != "" {
|
||||
// 在 section 末尾插入新行
|
||||
insertIdx := lastSectionLine + 1
|
||||
newLine := key + " = " + value
|
||||
result = append(result[:insertIdx+1], append([]string{newLine}, result[insertIdx+1:]...)...)
|
||||
}
|
||||
}
|
||||
currentSection = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
}
|
||||
|
||||
if currentSection == section {
|
||||
lastSectionLine = len(result)
|
||||
}
|
||||
|
||||
// 在目标 section 内匹配 key
|
||||
if currentSection == section && !found {
|
||||
checkLine := trimmed
|
||||
commented := false
|
||||
if strings.HasPrefix(checkLine, ";") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
commented = true
|
||||
} else if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
commented = true
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
|
||||
found = true
|
||||
if value == "" {
|
||||
// 值为空时注释掉
|
||||
if !commented {
|
||||
result = append(result, ";"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
} else {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
_ = i
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, line)
|
||||
}
|
||||
|
||||
// 如果在最后一个 section 中未找到 key
|
||||
if currentSection == section && !found {
|
||||
found = true
|
||||
if value != "" {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
}
|
||||
|
||||
// section 不存在,在文件末尾追加
|
||||
if !found && value != "" {
|
||||
result = append(result, "")
|
||||
result = append(result, "["+section+"]")
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
return app.Root + "/server/grafana/conf/defaults.ini"
|
||||
}
|
||||
|
||||
// datasourcePath 返回 provisioning 数据源文件路径
|
||||
func (s *App) datasourcePath() string {
|
||||
return fmt.Sprintf("%s/server/grafana/conf/provisioning/datasources/panel.yml", app.Root)
|
||||
return app.Root + "/server/grafana/conf/provisioning/datasources/panel.yml"
|
||||
}
|
||||
|
||||
// readDatasources 读取 provisioning 文件
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package grafana
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Grafana 配置调整
|
||||
type ConfigTune struct {
|
||||
// [server]
|
||||
|
||||
+26
-100
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -20,8 +21,8 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
return &App{t: t}, nil
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -51,40 +52,23 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Node ID"), Value: s.getPropertiesValue(config, "node.id")},
|
||||
{Name: s.t.Get("Listeners"), Value: s.getPropertiesValue(config, "listeners")},
|
||||
{Name: s.t.Get("Log Dirs"), Value: s.getPropertiesValue(config, "log.dirs")},
|
||||
{Name: s.t.Get("Num Partitions"), Value: s.getPropertiesValue(config, "num.partitions")},
|
||||
{Name: s.t.Get("Log Retention Hours"), Value: s.getPropertiesValue(config, "log.retention.hours")},
|
||||
{Name: s.t.Get("Log Segment Bytes"), Value: s.getPropertiesValue(config, "log.segment.bytes")},
|
||||
{Name: s.t.Get("Node ID"), Value: confval.Properties.Get(config, "node.id")},
|
||||
{Name: s.t.Get("Listeners"), Value: confval.Properties.Get(config, "listeners")},
|
||||
{Name: s.t.Get("Log Dirs"), Value: confval.Properties.Get(config, "log.dirs")},
|
||||
{Name: s.t.Get("Num Partitions"), Value: confval.Properties.Get(config, "num.partitions")},
|
||||
{Name: s.t.Get("Log Retention Hours"), Value: confval.Properties.Get(config, "log.retention.hours")},
|
||||
{Name: s.t.Get("Log Segment Bytes"), Value: confval.Properties.Get(config, "log.segment.bytes")},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) 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(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("kafka"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "kafka")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Kafka 配置调整参数
|
||||
@@ -95,12 +79,12 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
heapInit, heapMax := s.parseHeapEnv(heapRaw)
|
||||
|
||||
tune := ConfigTune{
|
||||
NodeID: s.getPropertiesValue(config, "node.id"),
|
||||
Listeners: s.getPropertiesValue(config, "listeners"),
|
||||
LogDirs: s.getPropertiesValue(config, "log.dirs"),
|
||||
NumPartitions: s.getPropertiesValue(config, "num.partitions"),
|
||||
RetentionHours: s.getPropertiesValue(config, "log.retention.hours"),
|
||||
LogSegmentBytes: s.getPropertiesValue(config, "log.segment.bytes"),
|
||||
NodeID: confval.Properties.Get(config, "node.id"),
|
||||
Listeners: confval.Properties.Get(config, "listeners"),
|
||||
LogDirs: confval.Properties.Get(config, "log.dirs"),
|
||||
NumPartitions: confval.Properties.Get(config, "num.partitions"),
|
||||
RetentionHours: confval.Properties.Get(config, "log.retention.hours"),
|
||||
LogSegmentBytes: confval.Properties.Get(config, "log.segment.bytes"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
@@ -118,12 +102,12 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
config = s.setPropertiesValue(config, "node.id", req.NodeID)
|
||||
config = s.setPropertiesValue(config, "listeners", req.Listeners)
|
||||
config = s.setPropertiesValue(config, "log.dirs", req.LogDirs)
|
||||
config = s.setPropertiesValue(config, "num.partitions", req.NumPartitions)
|
||||
config = s.setPropertiesValue(config, "log.retention.hours", req.RetentionHours)
|
||||
config = s.setPropertiesValue(config, "log.segment.bytes", req.LogSegmentBytes)
|
||||
config = confval.Properties.Set(config, "node.id", req.NodeID)
|
||||
config = confval.Properties.Set(config, "listeners", req.Listeners)
|
||||
config = confval.Properties.Set(config, "log.dirs", req.LogDirs)
|
||||
config = confval.Properties.Set(config, "num.partitions", req.NumPartitions)
|
||||
config = confval.Properties.Set(config, "log.retention.hours", req.RetentionHours)
|
||||
config = confval.Properties.Set(config, "log.segment.bytes", req.LogSegmentBytes)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -150,70 +134,12 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/kafka/config/server.properties", app.Root)
|
||||
return app.Root + "/server/kafka/config/server.properties"
|
||||
}
|
||||
|
||||
// heapEnvPath 返回 JVM 堆内存配置文件路径
|
||||
func (s *App) heapEnvPath() string {
|
||||
return fmt.Sprintf("%s/server/kafka/config/heap.env", app.Root)
|
||||
}
|
||||
|
||||
// getPropertiesValue 从 properties 内容中获取指定键的值
|
||||
func (s *App) getPropertiesValue(content string, key string) string {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(trimmed, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setPropertiesValue 在 properties 内容中设置指定键的值
|
||||
func (s *App) setPropertiesValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
k, _, ok := strings.Cut(checkLine, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+"="+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+"="+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
return app.Root + "/server/kafka/config/heap.env"
|
||||
}
|
||||
|
||||
// parseHeapEnv 从 heap.env 中提取堆内存配置
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package kafka
|
||||
|
||||
// UpdateConfig Kafka 配置更新
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Kafka 配置调整
|
||||
type ConfigTune struct {
|
||||
// Broker
|
||||
|
||||
@@ -10,10 +10,10 @@ type App struct {
|
||||
mysql *mysql.App
|
||||
}
|
||||
|
||||
func NewApp(mysqlApp *mysql.App) (*App, error) {
|
||||
func NewApp(mysqlApp *mysql.App) *App {
|
||||
return &App{
|
||||
mysql: mysqlApp,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -20,10 +21,10 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -90,33 +91,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) 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)
|
||||
common.ServeConfig(w, "/etc/systemd/system/memcached.service")
|
||||
}
|
||||
|
||||
func (s *App) 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)
|
||||
common.SaveConfig(w, r, "/etc/systemd/system/memcached.service", "memcached")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Memcached 配置调整参数
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package memcached
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Memcached 配置调整
|
||||
type ConfigTune struct {
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package mongodb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"go.yaml.in/yaml/v4"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -26,12 +26,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) (*App, error) {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -107,29 +107,12 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
func (s *App) 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(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("mongod"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "mongod")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取配置调整参数
|
||||
@@ -240,7 +223,7 @@ func (s *App) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// configPath 返回配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/mongodb/mongod.conf", app.Root)
|
||||
return app.Root + "/server/mongodb/mongod.conf"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取嵌套 YAML 值,支持 dot notation
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
package mongodb
|
||||
|
||||
// UpdateConfig 更新配置
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// SetAdminPassword 设置 admin 密码
|
||||
type SetAdminPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
|
||||
+575
-167
@@ -1,10 +1,12 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
@@ -26,14 +29,16 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) (*App, error) {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
taskRepo: taskRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -45,6 +50,19 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/root_password", s.SetRootPassword)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 性能
|
||||
r.Get("/processes", s.ProcessList)
|
||||
r.Post("/processes/{id}/kill", s.KillProcess)
|
||||
r.Get("/transactions", s.TransactionList)
|
||||
r.Get("/top_sql", s.TopSQL)
|
||||
r.Post("/top_sql/enable", s.EnableTopSQL)
|
||||
r.Post("/top_sql/reset", s.ResetTopSQL)
|
||||
// 维护
|
||||
r.Get("/tables", s.TableList)
|
||||
r.Post("/maintenance", s.RunMaintenance)
|
||||
r.Get("/binlogs", s.BinlogList)
|
||||
r.Post("/binlogs/purge", s.PurgeBinlog)
|
||||
r.Get("/replication", s.ReplicationStatus)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
@@ -52,38 +70,6 @@ func (s *App) Status() string {
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateConfig 保存配置
|
||||
func (s *App) 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(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart MySQL: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// Load 获取负载
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, _ := systemctl.Status("mysqld")
|
||||
@@ -98,19 +84,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Setenv("MYSQL_PWD", rootPassword); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set MYSQL_PWD env: %v", err))
|
||||
return
|
||||
}
|
||||
raw, err := shell.Execf(`mysqladmin -u root extended-status`)
|
||||
raw, err := shell.ExecfWithEnv([]string{"MYSQL_PWD=" + rootPassword}, `mysqladmin -u root extended-status`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get MySQL status: %v", err))
|
||||
return
|
||||
}
|
||||
if err = os.Unsetenv("MYSQL_PWD"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to unset MYSQL_PWD env: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
var load []map[string]string
|
||||
expressions := []struct {
|
||||
@@ -197,9 +175,41 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, load)
|
||||
}
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateConfig 保存配置
|
||||
func (s *App) 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(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart MySQL: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// SlowLog 获取慢查询日志
|
||||
func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, fmt.Sprintf("%s/server/mysql/mysql-slow.log", app.Root))
|
||||
service.Success(w, app.Root+"/server/mysql/mysql-slow.log")
|
||||
}
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
@@ -222,10 +232,10 @@ func (s *App) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
oldRootPassword, _ := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
mysql, err := db.NewMySQL(r.Context(), "root", oldRootPassword, s.getSock(), "unix")
|
||||
mysql, err := db.NewMySQL(r.Context(), "root", oldRootPassword, db.MySQLSocket(app.Root), "unix")
|
||||
if err != nil {
|
||||
// 尝试安全模式直接改密
|
||||
if err = db.MySQLResetRootPassword(req.Password); err != nil {
|
||||
if err = db.MySQLResetRootPassword(req.Password, app.Root); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -257,35 +267,35 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tune := ConfigTune{
|
||||
// 常规设置
|
||||
Port: s.getINIValue(config, "port"),
|
||||
MaxConnections: s.getINIValue(config, "max_connections"),
|
||||
MaxConnectErrors: s.getINIValue(config, "max_connect_errors"),
|
||||
DefaultStorageEngine: s.getINIValue(config, "default_storage_engine"),
|
||||
TableOpenCache: s.getINIValue(config, "table_open_cache"),
|
||||
MaxAllowedPacket: s.getINIValue(config, "max_allowed_packet"),
|
||||
OpenFilesLimit: s.getINIValue(config, "open_files_limit"),
|
||||
Port: confval.INI.Get(config, "port"),
|
||||
MaxConnections: confval.INI.Get(config, "max_connections"),
|
||||
MaxConnectErrors: confval.INI.Get(config, "max_connect_errors"),
|
||||
DefaultStorageEngine: confval.INI.Get(config, "default_storage_engine"),
|
||||
TableOpenCache: confval.INI.Get(config, "table_open_cache"),
|
||||
MaxAllowedPacket: confval.INI.Get(config, "max_allowed_packet"),
|
||||
OpenFilesLimit: confval.INI.Get(config, "open_files_limit"),
|
||||
// 性能调整
|
||||
KeyBufferSize: s.getINIValue(config, "key_buffer_size"),
|
||||
SortBufferSize: s.getINIValue(config, "sort_buffer_size"),
|
||||
ReadBufferSize: s.getINIValue(config, "read_buffer_size"),
|
||||
ReadRndBufferSize: s.getINIValue(config, "read_rnd_buffer_size"),
|
||||
JoinBufferSize: s.getINIValue(config, "join_buffer_size"),
|
||||
ThreadCacheSize: s.getINIValue(config, "thread_cache_size"),
|
||||
ThreadStack: s.getINIValue(config, "thread_stack"),
|
||||
TmpTableSize: s.getINIValue(config, "tmp_table_size"),
|
||||
MaxHeapTableSize: s.getINIValue(config, "max_heap_table_size"),
|
||||
MyisamSortBufferSize: s.getINIValue(config, "myisam_sort_buffer_size"),
|
||||
KeyBufferSize: confval.INI.Get(config, "key_buffer_size"),
|
||||
SortBufferSize: confval.INI.Get(config, "sort_buffer_size"),
|
||||
ReadBufferSize: confval.INI.Get(config, "read_buffer_size"),
|
||||
ReadRndBufferSize: confval.INI.Get(config, "read_rnd_buffer_size"),
|
||||
JoinBufferSize: confval.INI.Get(config, "join_buffer_size"),
|
||||
ThreadCacheSize: confval.INI.Get(config, "thread_cache_size"),
|
||||
ThreadStack: confval.INI.Get(config, "thread_stack"),
|
||||
TmpTableSize: confval.INI.Get(config, "tmp_table_size"),
|
||||
MaxHeapTableSize: confval.INI.Get(config, "max_heap_table_size"),
|
||||
MyisamSortBufferSize: confval.INI.Get(config, "myisam_sort_buffer_size"),
|
||||
// InnoDB
|
||||
InnodbBufferPoolSize: s.getINIValue(config, "innodb_buffer_pool_size"),
|
||||
InnodbLogBufferSize: s.getINIValue(config, "innodb_log_buffer_size"),
|
||||
InnodbFlushLogAtTrxCommit: s.getINIValue(config, "innodb_flush_log_at_trx_commit"),
|
||||
InnodbLockWaitTimeout: s.getINIValue(config, "innodb_lock_wait_timeout"),
|
||||
InnodbMaxDirtyPagesPct: s.getINIValue(config, "innodb_max_dirty_pages_pct"),
|
||||
InnodbReadIoThreads: s.getINIValue(config, "innodb_read_io_threads"),
|
||||
InnodbWriteIoThreads: s.getINIValue(config, "innodb_write_io_threads"),
|
||||
InnodbBufferPoolSize: confval.INI.Get(config, "innodb_buffer_pool_size"),
|
||||
InnodbLogBufferSize: confval.INI.Get(config, "innodb_log_buffer_size"),
|
||||
InnodbFlushLogAtTrxCommit: confval.INI.Get(config, "innodb_flush_log_at_trx_commit"),
|
||||
InnodbLockWaitTimeout: confval.INI.Get(config, "innodb_lock_wait_timeout"),
|
||||
InnodbMaxDirtyPagesPct: confval.INI.Get(config, "innodb_max_dirty_pages_pct"),
|
||||
InnodbReadIoThreads: confval.INI.Get(config, "innodb_read_io_threads"),
|
||||
InnodbWriteIoThreads: confval.INI.Get(config, "innodb_write_io_threads"),
|
||||
// 日志
|
||||
SlowQueryLog: s.getINIValue(config, "slow_query_log"),
|
||||
LongQueryTime: s.getINIValue(config, "long_query_time"),
|
||||
SlowQueryLog: confval.INI.Get(config, "slow_query_log"),
|
||||
LongQueryTime: confval.INI.Get(config, "long_query_time"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -307,35 +317,35 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 更新常规设置
|
||||
config = s.setINIValue(config, "port", req.Port)
|
||||
config = s.setINIValue(config, "max_connections", req.MaxConnections)
|
||||
config = s.setINIValue(config, "max_connect_errors", req.MaxConnectErrors)
|
||||
config = s.setINIValue(config, "default_storage_engine", req.DefaultStorageEngine)
|
||||
config = s.setINIValue(config, "table_open_cache", req.TableOpenCache)
|
||||
config = s.setINIValue(config, "max_allowed_packet", req.MaxAllowedPacket)
|
||||
config = s.setINIValue(config, "open_files_limit", req.OpenFilesLimit)
|
||||
config = confval.INI.Set(config, "port", req.Port)
|
||||
config = confval.INI.Set(config, "max_connections", req.MaxConnections)
|
||||
config = confval.INI.Set(config, "max_connect_errors", req.MaxConnectErrors)
|
||||
config = confval.INI.Set(config, "default_storage_engine", req.DefaultStorageEngine)
|
||||
config = confval.INI.Set(config, "table_open_cache", req.TableOpenCache)
|
||||
config = confval.INI.Set(config, "max_allowed_packet", req.MaxAllowedPacket)
|
||||
config = confval.INI.Set(config, "open_files_limit", req.OpenFilesLimit)
|
||||
// 更新性能调整
|
||||
config = s.setINIValue(config, "key_buffer_size", req.KeyBufferSize)
|
||||
config = s.setINIValue(config, "sort_buffer_size", req.SortBufferSize)
|
||||
config = s.setINIValue(config, "read_buffer_size", req.ReadBufferSize)
|
||||
config = s.setINIValue(config, "read_rnd_buffer_size", req.ReadRndBufferSize)
|
||||
config = s.setINIValue(config, "join_buffer_size", req.JoinBufferSize)
|
||||
config = s.setINIValue(config, "thread_cache_size", req.ThreadCacheSize)
|
||||
config = s.setINIValue(config, "thread_stack", req.ThreadStack)
|
||||
config = s.setINIValue(config, "tmp_table_size", req.TmpTableSize)
|
||||
config = s.setINIValue(config, "max_heap_table_size", req.MaxHeapTableSize)
|
||||
config = s.setINIValue(config, "myisam_sort_buffer_size", req.MyisamSortBufferSize)
|
||||
config = confval.INI.Set(config, "key_buffer_size", req.KeyBufferSize)
|
||||
config = confval.INI.Set(config, "sort_buffer_size", req.SortBufferSize)
|
||||
config = confval.INI.Set(config, "read_buffer_size", req.ReadBufferSize)
|
||||
config = confval.INI.Set(config, "read_rnd_buffer_size", req.ReadRndBufferSize)
|
||||
config = confval.INI.Set(config, "join_buffer_size", req.JoinBufferSize)
|
||||
config = confval.INI.Set(config, "thread_cache_size", req.ThreadCacheSize)
|
||||
config = confval.INI.Set(config, "thread_stack", req.ThreadStack)
|
||||
config = confval.INI.Set(config, "tmp_table_size", req.TmpTableSize)
|
||||
config = confval.INI.Set(config, "max_heap_table_size", req.MaxHeapTableSize)
|
||||
config = confval.INI.Set(config, "myisam_sort_buffer_size", req.MyisamSortBufferSize)
|
||||
// 更新 InnoDB
|
||||
config = s.setINIValue(config, "innodb_buffer_pool_size", req.InnodbBufferPoolSize)
|
||||
config = s.setINIValue(config, "innodb_log_buffer_size", req.InnodbLogBufferSize)
|
||||
config = s.setINIValue(config, "innodb_flush_log_at_trx_commit", req.InnodbFlushLogAtTrxCommit)
|
||||
config = s.setINIValue(config, "innodb_lock_wait_timeout", req.InnodbLockWaitTimeout)
|
||||
config = s.setINIValue(config, "innodb_max_dirty_pages_pct", req.InnodbMaxDirtyPagesPct)
|
||||
config = s.setINIValue(config, "innodb_read_io_threads", req.InnodbReadIoThreads)
|
||||
config = s.setINIValue(config, "innodb_write_io_threads", req.InnodbWriteIoThreads)
|
||||
config = confval.INI.Set(config, "innodb_buffer_pool_size", req.InnodbBufferPoolSize)
|
||||
config = confval.INI.Set(config, "innodb_log_buffer_size", req.InnodbLogBufferSize)
|
||||
config = confval.INI.Set(config, "innodb_flush_log_at_trx_commit", req.InnodbFlushLogAtTrxCommit)
|
||||
config = confval.INI.Set(config, "innodb_lock_wait_timeout", req.InnodbLockWaitTimeout)
|
||||
config = confval.INI.Set(config, "innodb_max_dirty_pages_pct", req.InnodbMaxDirtyPagesPct)
|
||||
config = confval.INI.Set(config, "innodb_read_io_threads", req.InnodbReadIoThreads)
|
||||
config = confval.INI.Set(config, "innodb_write_io_threads", req.InnodbWriteIoThreads)
|
||||
// 更新日志
|
||||
config = s.setINIValue(config, "slow_query_log", req.SlowQueryLog)
|
||||
config = s.setINIValue(config, "long_query_time", req.LongQueryTime)
|
||||
config = confval.INI.Set(config, "slow_query_log", req.SlowQueryLog)
|
||||
config = confval.INI.Set(config, "long_query_time", req.LongQueryTime)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -345,83 +355,481 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) getSock() string {
|
||||
if sock := db.MySQLSocket(app.Root+"/server/mysql/config/my.cnf", "/etc/my.cnf"); sock != "" {
|
||||
return sock
|
||||
// ProcessList 获取进程列表
|
||||
func (s *App) ProcessList(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
return "/tmp/mysql.sock"
|
||||
defer mysql.Close()
|
||||
|
||||
rows, err := mysql.Query(`
|
||||
SELECT ID, coalesce(USER,''), coalesce(HOST,''), coalesce(DB,''), coalesce(COMMAND,''),
|
||||
TIME, coalesce(STATE,''), coalesce(INFO,'')
|
||||
FROM information_schema.PROCESSLIST WHERE ID != CONNECTION_ID() ORDER BY TIME DESC`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
processes := make([]Process, 0)
|
||||
for rows.Next() {
|
||||
var item Process
|
||||
if err = rows.Scan(&item.ID, &item.User, &item.Host, &item.DB, &item.Command, &item.Time, &item.State, &item.Info); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
processes = append(processes, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, processes)
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 格式内容中获取指定键的值
|
||||
func (s *App) getINIValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, ";") || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "[") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
// KillProcess 终止进程
|
||||
func (s *App) KillProcess(w http.ResponseWriter, r *http.Request) {
|
||||
id := cast.ToInt64(chi.URLParam(r, "id"))
|
||||
if id <= 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid process id"))
|
||||
return
|
||||
}
|
||||
return ""
|
||||
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
// KILL 不支持预编译参数,id 已校验为正整数
|
||||
if _, err = mysql.Exec(fmt.Sprintf(`KILL %d`, id)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// setINIValue 在 INI 格式内容中设置指定键的值
|
||||
func (s *App) setINIValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
// TransactionList 获取事务及锁等待列表
|
||||
func (s *App) TransactionList(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "[") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
rows, err := mysql.Query(`
|
||||
SELECT trx_id, trx_mysql_thread_id, coalesce(trx_state,''), coalesce(trx_query,''),
|
||||
timestampdiff(SECOND, trx_started, now()), trx_rows_locked, trx_rows_modified
|
||||
FROM information_schema.INNODB_TRX ORDER BY trx_started`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
result := Transactions{Transactions: make([]Transaction, 0), LockWaits: make([]LockWait, 0)}
|
||||
for rows.Next() {
|
||||
var item Transaction
|
||||
if err = rows.Scan(&item.ID, &item.ThreadID, &item.State, &item.Query, &item.Seconds, &item.RowsLocked, &item.RowsModified); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, ";") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
} else if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
if found {
|
||||
continue
|
||||
result.Transactions = append(result.Transactions, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 锁等待对,MariaDB 与 MySQL 8 的表不同,查询失败时忽略
|
||||
lockSQL := `
|
||||
SELECT r.trx_mysql_thread_id, coalesce(r.trx_query,''), b.trx_mysql_thread_id, coalesce(b.trx_query,'')
|
||||
FROM performance_schema.data_lock_waits w
|
||||
JOIN information_schema.innodb_trx r ON r.trx_id = w.REQUESTING_ENGINE_TRANSACTION_ID
|
||||
JOIN information_schema.innodb_trx b ON b.trx_id = w.BLOCKING_ENGINE_TRANSACTION_ID`
|
||||
if s.isMariaDB(mysql) {
|
||||
lockSQL = `
|
||||
SELECT r.trx_mysql_thread_id, coalesce(r.trx_query,''), b.trx_mysql_thread_id, coalesce(b.trx_query,'')
|
||||
FROM information_schema.INNODB_LOCK_WAITS w
|
||||
JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id
|
||||
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id`
|
||||
}
|
||||
if lockRows, lockErr := mysql.Query(lockSQL); lockErr == nil {
|
||||
defer func() { _ = lockRows.Close() }()
|
||||
for lockRows.Next() {
|
||||
var item LockWait
|
||||
if err = lockRows.Scan(&item.WaitingThreadID, &item.WaitingQuery, &item.BlockingThreadID, &item.BlockingQuery); err != nil {
|
||||
break
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, ";") && !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" = "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
result.LockWaits = append(result.LockWaits, item)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
|
||||
service.Success(w, result)
|
||||
}
|
||||
|
||||
// TopSQL 获取 SQL 性能统计
|
||||
func (s *App) TopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
var enabled int
|
||||
if err = mysql.QueryRow(`SELECT @@performance_schema`).Scan(&enabled); err != nil {
|
||||
if strings.Contains(err.Error(), "Unknown system variable") {
|
||||
service.Success(w, TopSQL{Supported: false, Items: []TopSQLItem{}})
|
||||
return
|
||||
}
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if enabled == 0 {
|
||||
// 检查是否已配置等待重启
|
||||
config, _ := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
pending := strings.EqualFold(confval.SectionINI.GetIn(config, "mysqld", "performance_schema"), "on")
|
||||
service.Success(w, TopSQL{Supported: true, Enabled: false, PendingRestart: pending, Items: []TopSQLItem{}})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := mysql.Query(`
|
||||
SELECT coalesce(SCHEMA_NAME,''), COUNT_STAR, round(SUM_TIMER_WAIT/1e9),
|
||||
round(AVG_TIMER_WAIT/1e9,2), SUM_ROWS_SENT, SUM_ROWS_EXAMINED, coalesce(DIGEST_TEXT,'')
|
||||
FROM performance_schema.events_statements_summary_by_digest
|
||||
WHERE DIGEST_TEXT IS NOT NULL ORDER BY SUM_TIMER_WAIT DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]TopSQLItem, 0)
|
||||
for rows.Next() {
|
||||
var item TopSQLItem
|
||||
if err = rows.Scan(&item.Database, &item.Calls, &item.TotalMs, &item.MeanMs, &item.RowsSent, &item.RowsExamined, &item.Query); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, TopSQL{Supported: true, Enabled: true, Items: items})
|
||||
}
|
||||
|
||||
// EnableTopSQL 启用 performance_schema
|
||||
func (s *App) EnableTopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
// 实例不支持时禁止写入配置
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
var enabled int
|
||||
if err = mysql.QueryRow(`SELECT @@performance_schema`).Scan(&enabled); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("performance_schema is not supported by this instance"))
|
||||
return
|
||||
}
|
||||
|
||||
confPath := app.Root + "/server/mysql/conf/my.cnf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// my.cnf 分段,必须用 SectionINI 保证写入 [mysqld] 段内,重启后生效
|
||||
config = confval.SectionINI.SetIn(config, "mysqld", "performance_schema", "on")
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ResetTopSQL 重置 SQL 性能统计
|
||||
func (s *App) ResetTopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
if _, err = mysql.Exec(`TRUNCATE TABLE performance_schema.events_statements_summary_by_digest`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// TableList 获取表维护信息
|
||||
func (s *App) TableList(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
rows, err := mysql.Query(`
|
||||
SELECT table_schema, table_name, coalesce(engine,''), coalesce(table_rows,0),
|
||||
coalesce(data_length + index_length,0), coalesce(data_free,0)
|
||||
FROM information_schema.TABLES
|
||||
WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys')
|
||||
AND table_type = 'BASE TABLE'
|
||||
ORDER BY data_length + index_length DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]TableInfo, 0)
|
||||
for rows.Next() {
|
||||
var item TableInfo
|
||||
var size, free int64
|
||||
if err = rows.Scan(&item.Database, &item.Table, &item.Engine, &item.Rows, &size, &free); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
item.Size = tools.FormatBytes(float64(size))
|
||||
if size+free > 0 {
|
||||
item.FragmentRate = float64(free) * 100 / float64(size+free)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, items)
|
||||
}
|
||||
|
||||
// RunMaintenance 对表执行维护操作(异步任务)
|
||||
func (s *App) RunMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[MaintenanceRun](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !slices.Contains([]string{"optimize", "analyze"}, req.Operation) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid operation"))
|
||||
return
|
||||
}
|
||||
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
escaped := strings.ReplaceAll(rootPassword, `'`, `'\''`)
|
||||
cmd := fmt.Sprintf("MYSQL_PWD='%s' mysql -u root -e '%s TABLE `%s`.`%s`'", escaped, strings.ToUpper(req.Operation), req.Database, req.Table)
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = fmt.Sprintf("mysql:maintenance:%s.%s", req.Database, req.Table)
|
||||
task.Name = s.t.Get("Run %s on table %s.%s", req.Operation, req.Database, req.Table)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// BinlogList 获取 binlog 状态
|
||||
func (s *App) BinlogList(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
var enabled int
|
||||
if err = mysql.QueryRow(`SELECT @@log_bin`).Scan(&enabled); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if enabled == 0 {
|
||||
service.Success(w, Binlog{Enabled: false, Items: []BinlogFile{}})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := mysql.Query(`SHOW BINARY LOGS`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
// SHOW BINARY LOGS 列数两派不同(MySQL 8 多 Encrypted 列),动态取列
|
||||
maps, err := s.rowsToMaps(rows)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
binlog := Binlog{Enabled: true, Items: make([]BinlogFile, 0, len(maps))}
|
||||
var total int64
|
||||
for _, m := range maps {
|
||||
size := cast.ToInt64(m["File_size"])
|
||||
total += size
|
||||
binlog.Items = append(binlog.Items, BinlogFile{Name: m["Log_name"], Size: tools.FormatBytes(float64(size))})
|
||||
}
|
||||
binlog.TotalSize = tools.FormatBytes(float64(total))
|
||||
|
||||
service.Success(w, binlog)
|
||||
}
|
||||
|
||||
// PurgeBinlog 清理指定文件之前的 binlog
|
||||
func (s *App) PurgeBinlog(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[BinlogPurge](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
// 校验文件名存在于 binlog 列表,PURGE 不支持预编译参数
|
||||
rows, err := mysql.Query(`SHOW BINARY LOGS`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
maps, err := s.rowsToMaps(rows)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if !slices.ContainsFunc(maps, func(m map[string]string) bool { return m["Log_name"] == req.File }) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("binlog file %s does not exist", req.File))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = mysql.Exec(fmt.Sprintf(`PURGE BINARY LOGS TO '%s'`, req.File)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ReplicationStatus 获取复制状态
|
||||
func (s *App) ReplicationStatus(w http.ResponseWriter, r *http.Request) {
|
||||
mysql, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer mysql.Close()
|
||||
|
||||
rows, err := mysql.Query(`SHOW REPLICA STATUS`)
|
||||
if err != nil {
|
||||
// 老版本 fallback
|
||||
if rows, err = mysql.Query(`SHOW SLAVE STATUS`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
maps, err := s.rowsToMaps(rows)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if len(maps) == 0 {
|
||||
service.Success(w, Replication{Enabled: false})
|
||||
return
|
||||
}
|
||||
|
||||
// MySQL 8 与 MariaDB 的列名两派不同,按候选名取值
|
||||
pick := func(m map[string]string, keys ...string) string {
|
||||
for _, key := range keys {
|
||||
if v, ok := m[key]; ok && v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
m := maps[0]
|
||||
service.Success(w, Replication{
|
||||
Enabled: true,
|
||||
IORunning: pick(m, "Replica_IO_Running", "Slave_IO_Running"),
|
||||
SQLRunning: pick(m, "Replica_SQL_Running", "Slave_SQL_Running"),
|
||||
SecondsBehind: pick(m, "Seconds_Behind_Source", "Seconds_Behind_Master"),
|
||||
SourceHost: pick(m, "Source_Host", "Master_Host"),
|
||||
LastError: pick(m, "Last_Error"),
|
||||
})
|
||||
}
|
||||
|
||||
// connect 以 root 用户通过 unix socket 连接
|
||||
func (s *App) connect(ctx context.Context) (db.Operator, error) {
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.NewMySQL(ctx, "root", rootPassword, db.MySQLSocket(app.Root), "unix")
|
||||
}
|
||||
|
||||
// isMariaDB 判断当前实例是否为 MariaDB
|
||||
func (s *App) isMariaDB(op db.Operator) bool {
|
||||
var version string
|
||||
if err := op.QueryRow(`SELECT VERSION()`).Scan(&version); err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(strings.ToLower(version), "mariadb")
|
||||
}
|
||||
|
||||
// rowsToMaps 将查询结果按列名转为 map,用于列名/列数不固定的查询
|
||||
func (s *App) rowsToMaps(rows *sql.Rows) ([]map[string]string, error) {
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
columns, err := rows.Columns()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]map[string]string, 0)
|
||||
for rows.Next() {
|
||||
values := make([]sql.NullString, len(columns))
|
||||
scans := make([]any, len(columns))
|
||||
for i := range values {
|
||||
scans[i] = &values[i]
|
||||
}
|
||||
if err = rows.Scan(scans...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]string, len(columns))
|
||||
for i, column := range columns {
|
||||
m[column] = values[i].String
|
||||
}
|
||||
result = append(result, m)
|
||||
}
|
||||
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
@@ -41,3 +41,104 @@ type ConfigTune struct {
|
||||
SlowQueryLog string `form:"slow_query_log" json:"slow_query_log"`
|
||||
LongQueryTime string `form:"long_query_time" json:"long_query_time"`
|
||||
}
|
||||
|
||||
// MaintenanceRun 表维护操作请求
|
||||
type MaintenanceRun struct {
|
||||
Database string `form:"database" json:"database" validate:"required"`
|
||||
Table string `form:"table" json:"table" validate:"required"`
|
||||
Operation string `form:"operation" json:"operation" validate:"required"`
|
||||
}
|
||||
|
||||
// BinlogPurge binlog 清理请求
|
||||
type BinlogPurge struct {
|
||||
File string `form:"file" json:"file" validate:"required"`
|
||||
}
|
||||
|
||||
// Process 数据库进程信息
|
||||
type Process struct {
|
||||
ID int64 `json:"id"`
|
||||
User string `json:"user"`
|
||||
Host string `json:"host"`
|
||||
DB string `json:"db"`
|
||||
Command string `json:"command"`
|
||||
Time int64 `json:"time"`
|
||||
State string `json:"state"`
|
||||
Info string `json:"info"`
|
||||
}
|
||||
|
||||
// Transaction InnoDB 事务信息
|
||||
type Transaction struct {
|
||||
ID string `json:"id"`
|
||||
ThreadID int64 `json:"thread_id"`
|
||||
State string `json:"state"`
|
||||
Query string `json:"query"`
|
||||
Seconds int64 `json:"seconds"`
|
||||
RowsLocked int64 `json:"rows_locked"`
|
||||
RowsModified int64 `json:"rows_modified"`
|
||||
}
|
||||
|
||||
// LockWait 锁等待对
|
||||
type LockWait struct {
|
||||
WaitingThreadID int64 `json:"waiting_thread_id"`
|
||||
WaitingQuery string `json:"waiting_query"`
|
||||
BlockingThreadID int64 `json:"blocking_thread_id"`
|
||||
BlockingQuery string `json:"blocking_query"`
|
||||
}
|
||||
|
||||
// Transactions 事务与锁等待响应
|
||||
type Transactions struct {
|
||||
Transactions []Transaction `json:"transactions"`
|
||||
LockWaits []LockWait `json:"lock_waits"`
|
||||
}
|
||||
|
||||
// TopSQLItem Top SQL 统计项
|
||||
type TopSQLItem struct {
|
||||
Database string `json:"database"`
|
||||
Calls int64 `json:"calls"`
|
||||
TotalMs int64 `json:"total_ms"`
|
||||
MeanMs float64 `json:"mean_ms"`
|
||||
RowsSent int64 `json:"rows_sent"`
|
||||
RowsExamined int64 `json:"rows_examined"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// TopSQL Top SQL 响应
|
||||
type TopSQL struct {
|
||||
Supported bool `json:"supported"`
|
||||
Enabled bool `json:"enabled"`
|
||||
PendingRestart bool `json:"pending_restart"`
|
||||
Items []TopSQLItem `json:"items"`
|
||||
}
|
||||
|
||||
// TableInfo 表维护信息
|
||||
type TableInfo struct {
|
||||
Database string `json:"database"`
|
||||
Table string `json:"table"`
|
||||
Engine string `json:"engine"`
|
||||
Rows int64 `json:"rows"`
|
||||
Size string `json:"size"`
|
||||
FragmentRate float64 `json:"fragment_rate"`
|
||||
}
|
||||
|
||||
// BinlogFile binlog 文件信息
|
||||
type BinlogFile struct {
|
||||
Name string `json:"name"`
|
||||
Size string `json:"size"`
|
||||
}
|
||||
|
||||
// Binlog binlog 状态响应
|
||||
type Binlog struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
TotalSize string `json:"total_size"`
|
||||
Items []BinlogFile `json:"items"`
|
||||
}
|
||||
|
||||
// Replication 复制状态响应
|
||||
type Replication struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
IORunning string `json:"io_running"`
|
||||
SQLRunning string `json:"sql_running"`
|
||||
SecondsBehind string `json:"seconds_behind"`
|
||||
SourceHost string `json:"source_host"`
|
||||
LastError string `json:"last_error"`
|
||||
}
|
||||
|
||||
+55
-132
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
@@ -25,11 +25,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -57,7 +57,7 @@ func (s *App) Status() string {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/nginx/conf/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -73,7 +73,7 @@ func (s *App) SaveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root), req.Config, 0600); err != nil {
|
||||
if err = io.Write(app.Root+"/server/nginx/conf/nginx.conf", req.Config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -178,7 +178,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfigTune 获取 Nginx 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/nginx/conf/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -186,33 +186,33 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tune := ConfigTune{
|
||||
// 常规设置
|
||||
WorkerProcesses: s.getNginxValue(config, "worker_processes"),
|
||||
WorkerConnections: s.getNginxValue(config, "worker_connections"),
|
||||
KeepaliveTimeout: s.getNginxValue(config, "keepalive_timeout"),
|
||||
ClientMaxBodySize: s.getNginxValue(config, "client_max_body_size"),
|
||||
ClientBodyBufferSize: s.getNginxValue(config, "client_body_buffer_size"),
|
||||
ClientHeaderBufferSize: s.getNginxValue(config, "client_header_buffer_size"),
|
||||
ServerNamesHashBucketSize: s.getNginxValue(config, "server_names_hash_bucket_size"),
|
||||
ServerTokens: s.getNginxValue(config, "server_tokens"),
|
||||
WorkerProcesses: confval.Nginx.Get(config, "worker_processes"),
|
||||
WorkerConnections: confval.Nginx.Get(config, "worker_connections"),
|
||||
KeepaliveTimeout: confval.Nginx.Get(config, "keepalive_timeout"),
|
||||
ClientMaxBodySize: confval.Nginx.Get(config, "client_max_body_size"),
|
||||
ClientBodyBufferSize: confval.Nginx.Get(config, "client_body_buffer_size"),
|
||||
ClientHeaderBufferSize: confval.Nginx.Get(config, "client_header_buffer_size"),
|
||||
ServerNamesHashBucketSize: confval.Nginx.Get(config, "server_names_hash_bucket_size"),
|
||||
ServerTokens: confval.Nginx.Get(config, "server_tokens"),
|
||||
// Gzip 压缩
|
||||
Gzip: s.getNginxValue(config, "gzip"),
|
||||
GzipMinLength: s.getNginxValue(config, "gzip_min_length"),
|
||||
GzipCompLevel: s.getNginxValue(config, "gzip_comp_level"),
|
||||
GzipTypes: s.getNginxValue(config, "gzip_types"),
|
||||
GzipVary: s.getNginxValue(config, "gzip_vary"),
|
||||
GzipProxied: s.getNginxValue(config, "gzip_proxied"),
|
||||
Gzip: confval.Nginx.Get(config, "gzip"),
|
||||
GzipMinLength: confval.Nginx.Get(config, "gzip_min_length"),
|
||||
GzipCompLevel: confval.Nginx.Get(config, "gzip_comp_level"),
|
||||
GzipTypes: confval.Nginx.Get(config, "gzip_types"),
|
||||
GzipVary: confval.Nginx.Get(config, "gzip_vary"),
|
||||
GzipProxied: confval.Nginx.Get(config, "gzip_proxied"),
|
||||
// Brotli 压缩
|
||||
Brotli: s.getNginxValue(config, "brotli"),
|
||||
BrotliMinLength: s.getNginxValue(config, "brotli_min_length"),
|
||||
BrotliCompLevel: s.getNginxValue(config, "brotli_comp_level"),
|
||||
BrotliTypes: s.getNginxValue(config, "brotli_types"),
|
||||
BrotliStatic: s.getNginxValue(config, "brotli_static"),
|
||||
Brotli: confval.Nginx.Get(config, "brotli"),
|
||||
BrotliMinLength: confval.Nginx.Get(config, "brotli_min_length"),
|
||||
BrotliCompLevel: confval.Nginx.Get(config, "brotli_comp_level"),
|
||||
BrotliTypes: confval.Nginx.Get(config, "brotli_types"),
|
||||
BrotliStatic: confval.Nginx.Get(config, "brotli_static"),
|
||||
// Zstd 压缩
|
||||
Zstd: s.getNginxValue(config, "zstd"),
|
||||
ZstdMinLength: s.getNginxValue(config, "zstd_min_length"),
|
||||
ZstdCompLevel: s.getNginxValue(config, "zstd_comp_level"),
|
||||
ZstdTypes: s.getNginxValue(config, "zstd_types"),
|
||||
ZstdStatic: s.getNginxValue(config, "zstd_static"),
|
||||
Zstd: confval.Nginx.Get(config, "zstd"),
|
||||
ZstdMinLength: confval.Nginx.Get(config, "zstd_min_length"),
|
||||
ZstdCompLevel: confval.Nginx.Get(config, "zstd_comp_level"),
|
||||
ZstdTypes: confval.Nginx.Get(config, "zstd_types"),
|
||||
ZstdStatic: confval.Nginx.Get(config, "zstd_static"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -226,7 +226,7 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/nginx/conf/nginx.conf", app.Root)
|
||||
confPath := app.Root + "/server/nginx/conf/nginx.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -234,33 +234,33 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 更新常规设置
|
||||
config = s.setNginxValue(config, "worker_processes", req.WorkerProcesses)
|
||||
config = s.setNginxValue(config, "worker_connections", req.WorkerConnections)
|
||||
config = s.setNginxValue(config, "keepalive_timeout", req.KeepaliveTimeout)
|
||||
config = s.setNginxValue(config, "client_max_body_size", req.ClientMaxBodySize)
|
||||
config = s.setNginxValue(config, "client_body_buffer_size", req.ClientBodyBufferSize)
|
||||
config = s.setNginxValue(config, "client_header_buffer_size", req.ClientHeaderBufferSize)
|
||||
config = s.setNginxValue(config, "server_names_hash_bucket_size", req.ServerNamesHashBucketSize)
|
||||
config = s.setNginxValue(config, "server_tokens", req.ServerTokens)
|
||||
config = confval.Nginx.Set(config, "worker_processes", req.WorkerProcesses)
|
||||
config = confval.Nginx.Set(config, "worker_connections", req.WorkerConnections)
|
||||
config = confval.Nginx.Set(config, "keepalive_timeout", req.KeepaliveTimeout)
|
||||
config = confval.Nginx.Set(config, "client_max_body_size", req.ClientMaxBodySize)
|
||||
config = confval.Nginx.Set(config, "client_body_buffer_size", req.ClientBodyBufferSize)
|
||||
config = confval.Nginx.Set(config, "client_header_buffer_size", req.ClientHeaderBufferSize)
|
||||
config = confval.Nginx.Set(config, "server_names_hash_bucket_size", req.ServerNamesHashBucketSize)
|
||||
config = confval.Nginx.Set(config, "server_tokens", req.ServerTokens)
|
||||
// 更新 Gzip 压缩
|
||||
config = s.setNginxValue(config, "gzip", req.Gzip)
|
||||
config = s.setNginxValue(config, "gzip_min_length", req.GzipMinLength)
|
||||
config = s.setNginxValue(config, "gzip_comp_level", req.GzipCompLevel)
|
||||
config = s.setNginxValue(config, "gzip_types", req.GzipTypes)
|
||||
config = s.setNginxValue(config, "gzip_vary", req.GzipVary)
|
||||
config = s.setNginxValue(config, "gzip_proxied", req.GzipProxied)
|
||||
config = confval.Nginx.Set(config, "gzip", req.Gzip)
|
||||
config = confval.Nginx.Set(config, "gzip_min_length", req.GzipMinLength)
|
||||
config = confval.Nginx.Set(config, "gzip_comp_level", req.GzipCompLevel)
|
||||
config = confval.Nginx.Set(config, "gzip_types", req.GzipTypes)
|
||||
config = confval.Nginx.Set(config, "gzip_vary", req.GzipVary)
|
||||
config = confval.Nginx.Set(config, "gzip_proxied", req.GzipProxied)
|
||||
// 更新 Brotli 压缩
|
||||
config = s.setNginxValue(config, "brotli", req.Brotli)
|
||||
config = s.setNginxValue(config, "brotli_min_length", req.BrotliMinLength)
|
||||
config = s.setNginxValue(config, "brotli_comp_level", req.BrotliCompLevel)
|
||||
config = s.setNginxValue(config, "brotli_types", req.BrotliTypes)
|
||||
config = s.setNginxValue(config, "brotli_static", req.BrotliStatic)
|
||||
config = confval.Nginx.Set(config, "brotli", req.Brotli)
|
||||
config = confval.Nginx.Set(config, "brotli_min_length", req.BrotliMinLength)
|
||||
config = confval.Nginx.Set(config, "brotli_comp_level", req.BrotliCompLevel)
|
||||
config = confval.Nginx.Set(config, "brotli_types", req.BrotliTypes)
|
||||
config = confval.Nginx.Set(config, "brotli_static", req.BrotliStatic)
|
||||
// 更新 Zstd 压缩
|
||||
config = s.setNginxValue(config, "zstd", req.Zstd)
|
||||
config = s.setNginxValue(config, "zstd_min_length", req.ZstdMinLength)
|
||||
config = s.setNginxValue(config, "zstd_comp_level", req.ZstdCompLevel)
|
||||
config = s.setNginxValue(config, "zstd_types", req.ZstdTypes)
|
||||
config = s.setNginxValue(config, "zstd_static", req.ZstdStatic)
|
||||
config = confval.Nginx.Set(config, "zstd", req.Zstd)
|
||||
config = confval.Nginx.Set(config, "zstd_min_length", req.ZstdMinLength)
|
||||
config = confval.Nginx.Set(config, "zstd_comp_level", req.ZstdCompLevel)
|
||||
config = confval.Nginx.Set(config, "zstd_types", req.ZstdTypes)
|
||||
config = confval.Nginx.Set(config, "zstd_static", req.ZstdStatic)
|
||||
|
||||
if err = io.Write(confPath, config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -275,80 +275,3 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getNginxValue 从 Nginx 配置内容中获取指定指令的值
|
||||
func (s *App) getNginxValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(trimmed, ";") {
|
||||
continue
|
||||
}
|
||||
trimmed = strings.TrimSuffix(trimmed, ";")
|
||||
trimmed = strings.TrimSpace(trimmed)
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setNginxValue 在 Nginx 配置内容中设置指定指令的值
|
||||
func (s *App) setNginxValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || trimmed == "{" || trimmed == "}" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查指令(可能被注释)
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(checkLine, ";") {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine = strings.TrimSuffix(checkLine, ";")
|
||||
checkLine = strings.TrimSpace(checkLine)
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+"#"+strings.TrimLeft(line, " \t"))
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 保留原行缩进
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+key+" "+value+";")
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, " "+key+" "+value+";")
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -36,7 +37,7 @@ func (s *App) CreateStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", req.Name))
|
||||
configPath := filepath.Join(s.streamDir(), req.Name+".conf")
|
||||
if _, statErr := os.Stat(configPath); statErr == nil {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("stream server config already exists: %s", req.Name))
|
||||
return
|
||||
@@ -70,7 +71,7 @@ func (s *App) UpdateStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", name))
|
||||
configPath := filepath.Join(s.streamDir(), name+".conf")
|
||||
if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) {
|
||||
service.Error(w, http.StatusNotFound, s.t.Get("stream server not found: %s", name))
|
||||
return
|
||||
@@ -78,7 +79,7 @@ func (s *App) UpdateStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
newConfigPath := configPath
|
||||
if req.Name != name {
|
||||
newConfigPath = filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", req.Name))
|
||||
newConfigPath = filepath.Join(s.streamDir(), req.Name+".conf")
|
||||
if _, statErr := os.Stat(newConfigPath); statErr == nil {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("stream server config already exists: %s", req.Name))
|
||||
return
|
||||
@@ -110,7 +111,7 @@ func (s *App) DeleteStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", name))
|
||||
configPath := filepath.Join(s.streamDir(), name+".conf")
|
||||
if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) {
|
||||
service.Error(w, http.StatusNotFound, s.t.Get("stream server not found: %s", name))
|
||||
return
|
||||
@@ -411,24 +412,24 @@ func (s *App) parseStreamUpstreamFile(filePath string, expectedName string) (*St
|
||||
|
||||
cfg := p.Config()
|
||||
if cfg == nil || cfg.Block == nil {
|
||||
return nil, fmt.Errorf("invalid config")
|
||||
return nil, errors.New("invalid config")
|
||||
}
|
||||
|
||||
// 查找 upstream 块
|
||||
upstreamDirectives := cfg.Block.FindDirectives("upstream")
|
||||
if len(upstreamDirectives) == 0 {
|
||||
return nil, fmt.Errorf("no upstream block found")
|
||||
return nil, errors.New("no upstream block found")
|
||||
}
|
||||
|
||||
upstreamDir := upstreamDirectives[0]
|
||||
params := upstreamDir.GetParameters()
|
||||
if len(params) == 0 {
|
||||
return nil, fmt.Errorf("upstream name not found")
|
||||
return nil, errors.New("upstream name not found")
|
||||
}
|
||||
|
||||
name := params[0].Value
|
||||
if expectedName != "" && name != expectedName {
|
||||
return nil, fmt.Errorf("upstream name mismatch")
|
||||
return nil, errors.New("upstream name mismatch")
|
||||
}
|
||||
|
||||
upstream := &StreamUpstream{
|
||||
@@ -439,7 +440,7 @@ func (s *App) parseStreamUpstreamFile(filePath string, expectedName string) (*St
|
||||
|
||||
upstreamBlock := upstreamDir.GetBlock()
|
||||
if upstreamBlock == nil {
|
||||
return nil, fmt.Errorf("upstream block is empty")
|
||||
return nil, errors.New("upstream block is empty")
|
||||
}
|
||||
|
||||
// 解析 upstream 块中的指令
|
||||
|
||||
@@ -12,10 +12,10 @@ type App struct {
|
||||
nginx *nginx.App
|
||||
}
|
||||
|
||||
func NewApp(nginxApp *nginx.App) (*App, error) {
|
||||
func NewApp(nginxApp *nginx.App) *App {
|
||||
return &App{
|
||||
nginx: nginxApp,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -15,6 +14,8 @@ import (
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -25,9 +26,9 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{t: t}, nil
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -93,28 +94,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(s.configPath())
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, s.configPath())
|
||||
}
|
||||
|
||||
func (s *App) 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(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("opensearch"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.configPath(), "opensearch")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 OpenSearch 配置调整参数
|
||||
@@ -130,13 +114,13 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
heapInit, heapMax := s.parseJVMHeap(jvmRaw)
|
||||
|
||||
tune := ConfigTune{
|
||||
ClusterName: s.getYAMLValue(cfg, "cluster.name"),
|
||||
NodeName: s.getYAMLValue(cfg, "node.name"),
|
||||
NetworkHost: s.getYAMLValue(cfg, "network.host"),
|
||||
HTTPPort: s.getYAMLValue(cfg, "http.port"),
|
||||
DiscoveryType: s.getYAMLValue(cfg, "discovery.type"),
|
||||
PathData: s.getYAMLValue(cfg, "path.data"),
|
||||
PathLogs: s.getYAMLValue(cfg, "path.logs"),
|
||||
ClusterName: confval.GetYAML(cfg, "cluster.name"),
|
||||
NodeName: confval.GetYAML(cfg, "node.name"),
|
||||
NetworkHost: confval.GetYAML(cfg, "network.host"),
|
||||
HTTPPort: confval.GetYAML(cfg, "http.port"),
|
||||
DiscoveryType: confval.GetYAML(cfg, "discovery.type"),
|
||||
PathData: confval.GetYAML(cfg, "path.data"),
|
||||
PathLogs: confval.GetYAML(cfg, "path.logs"),
|
||||
HeapInitSize: heapInit,
|
||||
HeapMaxSize: heapMax,
|
||||
}
|
||||
@@ -158,13 +142,13 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
cfg = make(map[string]any)
|
||||
}
|
||||
|
||||
s.setYAMLValue(cfg, "cluster.name", req.ClusterName)
|
||||
s.setYAMLValue(cfg, "node.name", req.NodeName)
|
||||
s.setYAMLValue(cfg, "network.host", req.NetworkHost)
|
||||
s.setYAMLValue(cfg, "http.port", req.HTTPPort)
|
||||
s.setYAMLValue(cfg, "discovery.type", req.DiscoveryType)
|
||||
s.setYAMLValue(cfg, "path.data", req.PathData)
|
||||
s.setYAMLValue(cfg, "path.logs", req.PathLogs)
|
||||
confval.SetYAML(cfg, "cluster.name", req.ClusterName)
|
||||
confval.SetYAML(cfg, "node.name", req.NodeName)
|
||||
confval.SetYAML(cfg, "network.host", req.NetworkHost)
|
||||
confval.SetYAML(cfg, "http.port", req.HTTPPort)
|
||||
confval.SetYAML(cfg, "discovery.type", req.DiscoveryType)
|
||||
confval.SetYAML(cfg, "path.data", req.PathData)
|
||||
confval.SetYAML(cfg, "path.logs", req.PathLogs)
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
@@ -194,11 +178,11 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/opensearch/config/opensearch.yml", app.Root)
|
||||
return app.Root + "/server/opensearch/config/opensearch.yml"
|
||||
}
|
||||
|
||||
func (s *App) jvmOptionsPath() string {
|
||||
return fmt.Sprintf("%s/server/opensearch/config/jvm.options", app.Root)
|
||||
return app.Root + "/server/opensearch/config/jvm.options"
|
||||
}
|
||||
|
||||
func (s *App) getPort() string {
|
||||
@@ -206,53 +190,13 @@ func (s *App) getPort() string {
|
||||
var cfg map[string]any
|
||||
_ = yaml.Unmarshal([]byte(raw), &cfg)
|
||||
if cfg != nil {
|
||||
if v := s.getYAMLValue(cfg, "http.port"); v != "" {
|
||||
if v := confval.GetYAML(cfg, "http.port"); v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return "9200"
|
||||
}
|
||||
|
||||
// getYAMLValue 获取 YAML 值,优先匹配平铺键(如 "path.data"),回退到嵌套键(如 path -> data)
|
||||
func (s *App) getYAMLValue(cfg map[string]any, key string) string {
|
||||
// 优先匹配平铺键(安装脚本用 sed 生成的格式)
|
||||
if val, ok := cfg[key]; ok {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
// 回退到嵌套键
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
val, ok := cfg[parts[0]]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return cast.ToString(val)
|
||||
}
|
||||
nested, ok := val.(map[string]any)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return s.getYAMLValue(nested, parts[1])
|
||||
}
|
||||
|
||||
// setYAMLValue 设置 YAML 值
|
||||
func (s *App) setYAMLValue(cfg map[string]any, key string, value string) {
|
||||
if value == "" {
|
||||
return
|
||||
}
|
||||
// 使用平铺键,同时清理可能存在的嵌套键
|
||||
cfg[key] = value
|
||||
parts := strings.SplitN(key, ".", 2)
|
||||
if len(parts) == 2 {
|
||||
if nested, ok := cfg[parts[0]].(map[string]any); ok {
|
||||
delete(nested, parts[1])
|
||||
if len(nested) == 0 {
|
||||
delete(cfg, parts[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) parseJVMHeap(content string) (initSize, maxSize string) {
|
||||
reInit := regexp.MustCompile(`(?m)^-Xms(\S+)`)
|
||||
reMax := regexp.MustCompile(`(?m)^-Xmx(\S+)`)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
package opensearch
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune OpenSearch 配置调整
|
||||
type ConfigTune struct {
|
||||
// 集群
|
||||
|
||||
@@ -10,10 +10,10 @@ type App struct {
|
||||
mysql *mysql.App
|
||||
}
|
||||
|
||||
func NewApp(mysqlApp *mysql.App) (*App, error) {
|
||||
func NewApp(mysqlApp *mysql.App) *App {
|
||||
return &App{
|
||||
mysql: mysqlApp,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
@@ -38,12 +38,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) (*App, error) {
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
conf: conf,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// pgadminLanguage 面板语言映射为 pgAdmin 语言代码
|
||||
@@ -86,12 +86,12 @@ func (s *App) Status() string {
|
||||
}
|
||||
|
||||
func (s *App) path() string {
|
||||
return fmt.Sprintf("%s/server/pgadmin", app.Root)
|
||||
return app.Root + "/server/pgadmin"
|
||||
}
|
||||
|
||||
// port 从 systemd 环境文件中解析监听端口
|
||||
func (s *App) port() (uint, error) {
|
||||
conf, err := io.Read(fmt.Sprintf("%s/pgadmin.conf", s.path()))
|
||||
conf, err := io.Read(s.path() + "/pgadmin.conf")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -105,7 +105,7 @@ func (s *App) port() (uint, error) {
|
||||
|
||||
// credential 读取安装时生成的初始凭据(邮箱与密码)
|
||||
func (s *App) credential() (string, string) {
|
||||
raw, err := io.Read(fmt.Sprintf("%s/credential", s.path()))
|
||||
raw, err := io.Read(s.path() + "/credential")
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
conf := fmt.Sprintf("%s/pgadmin.conf", s.path())
|
||||
conf := s.path() + "/pgadmin.conf"
|
||||
content, err := io.Read(conf)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -507,7 +507,7 @@ func (s *App) UpdateUsername(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 更新凭据文件,下次登录按新账号重新同步
|
||||
if err = io.Write(fmt.Sprintf("%s/credential", s.path()), req.Username+"\n"+password+"\n", 0600); err != nil {
|
||||
if err = io.Write(s.path()+"/credential", req.Username+"\n"+password+"\n", 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -539,7 +539,7 @@ func (s *App) ResetPassword(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/credential", s.path()), email+"\n"+req.Password+"\n", 0600); err != nil {
|
||||
if err = io.Write(s.path()+"/credential", email+"\n"+req.Password+"\n", 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,12 +35,12 @@ type App struct {
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
}
|
||||
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) (*App, error) {
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
conf: conf,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -59,7 +59,7 @@ func (s *App) Status() string {
|
||||
|
||||
// info 获取 phpMyAdmin 的访问目录与端口
|
||||
func (s *App) info() (string, int, error) {
|
||||
files, err := os.ReadDir(fmt.Sprintf("%s/server/phpmyadmin", app.Root))
|
||||
files, err := os.ReadDir(app.Root + "/server/phpmyadmin")
|
||||
if err != nil {
|
||||
return "", 0, errors.New(s.t.Get("phpMyAdmin directory not found"))
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func (s *App) info() (string, int, error) {
|
||||
return "", 0, errors.New(s.t.Get("phpMyAdmin directory not found"))
|
||||
}
|
||||
|
||||
conf, err := io.Read(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root))
|
||||
conf, err := io.Read(app.Root + "/sites/phpmyadmin/config/nginx.conf")
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
@@ -248,13 +248,13 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
conf, err := io.Read(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root))
|
||||
conf, err := io.Read(app.Root + "/sites/phpmyadmin/config/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
conf = regexp.MustCompile(`listen\s+(\d+);`).ReplaceAllString(conf, "listen "+cast.ToString(req.Port)+";")
|
||||
if err = io.Write(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root), conf, 0600); err != nil {
|
||||
if err = io.Write(app.Root+"/sites/phpmyadmin/config/nginx.conf", conf, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -282,7 +282,7 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/sites/phpmyadmin/config/nginx.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -298,7 +298,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/sites/phpmyadmin/config/nginx.conf", app.Root), req.Config, 0600); err != nil {
|
||||
if err = io.Write(app.Root+"/sites/phpmyadmin/config/nginx.conf", req.Config, 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
|
||||
type App struct{}
|
||||
|
||||
func NewApp() (*App, error) {
|
||||
return &App{}, nil
|
||||
func NewApp() *App {
|
||||
return &App{}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
+606
-150
@@ -1,19 +1,25 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
@@ -21,21 +27,24 @@ import (
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
var defaultVersionRegexp = regexp.MustCompile(`default_version\s*=\s*'([^']+)'`)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
settingRepo biz.SettingRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo) (*App, error) {
|
||||
|
||||
setting := settingRepo
|
||||
databaseServer := databaseServerRepo
|
||||
func NewApp(t *gotext.Locale, conf *config.Config, databaseServerRepo biz.DatabaseServerRepo, settingRepo biz.SettingRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
settingRepo: setting,
|
||||
databaseServerRepo: databaseServer,
|
||||
}, nil
|
||||
conf: conf,
|
||||
settingRepo: settingRepo,
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
taskRepo: taskRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -49,6 +58,23 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/postgres_password", s.SetPostgresPassword)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 扩展管理
|
||||
r.Get("/extensions", s.ExtensionList)
|
||||
r.Post("/extensions", s.InstallExtension)
|
||||
r.Delete("/extensions", s.UninstallExtension)
|
||||
r.Post("/extensions/enable", s.EnableExtension)
|
||||
// 性能
|
||||
r.Get("/sessions", s.SessionList)
|
||||
r.Post("/sessions/{pid}/terminate", s.TerminateSession)
|
||||
r.Get("/top_sql", s.TopSQL)
|
||||
r.Post("/top_sql/enable", s.EnableTopSQL)
|
||||
r.Post("/top_sql/reset", s.ResetTopSQL)
|
||||
// 维护
|
||||
r.Get("/databases", s.DatabaseList)
|
||||
r.Get("/bloat", s.BloatList)
|
||||
r.Post("/maintenance", s.RunMaintenance)
|
||||
r.Get("/wal", s.WalStatus)
|
||||
r.Delete("/replication_slots/{slot}", s.DropReplicationSlot)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
@@ -76,7 +102,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
oldPort := s.getPort()
|
||||
oldPort := db.PostgresPort(app.Root)
|
||||
if err = io.Write(s.configPath(), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -93,7 +119,7 @@ func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// GetUserConfig 获取用户配置
|
||||
func (s *App) GetUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
// 获取配置
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/postgresql/data/pg_hba.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -110,7 +136,7 @@ func (s *App) UpdateUserConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/postgresql/data/pg_hba.conf", app.Root), req.Config, 0644); err != nil {
|
||||
if err = io.Write(app.Root+"/server/postgresql/data/pg_hba.conf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -137,19 +163,14 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = os.Setenv("PGPASSWORD", postgresPassword); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to set PGPASSWORD env: %v", err))
|
||||
return
|
||||
}
|
||||
defer func() { _ = os.Unsetenv("PGPASSWORD") }()
|
||||
|
||||
port := s.getPort()
|
||||
start, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_postmaster_start_time();" | head -1 | cut -d'.' -f1`, port)
|
||||
env := []string{"PGPASSWORD=" + postgresPassword}
|
||||
port := db.PostgresPort(app.Root)
|
||||
start, err := shell.ExecfWithEnv(env, `psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_postmaster_start_time();" | head -1 | cut -d'.' -f1`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL start time: %v", err))
|
||||
return
|
||||
}
|
||||
pid, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_backend_pid();"`, port)
|
||||
pid, err := shell.ExecfWithEnv(env, `psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_backend_pid();"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL backend pid: %v", err))
|
||||
return
|
||||
@@ -159,12 +180,12 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL process: %v", err))
|
||||
return
|
||||
}
|
||||
connections, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();"`, port)
|
||||
connections, err := shell.ExecfWithEnv(env, `psql -h 127.0.0.1 -p %d -U postgres -t -c "SELECT count(*) FROM pg_stat_activity WHERE NOT pid=pg_backend_pid();"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL connections: %v", err))
|
||||
return
|
||||
}
|
||||
storage, err := shell.Execf(`psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_size_pretty(pg_database_size('postgres'));"`, port)
|
||||
storage, err := shell.ExecfWithEnv(env, `psql -h 127.0.0.1 -p %d -U postgres -t -c "select pg_size_pretty(pg_database_size('postgres'));"`, port)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get PostgreSQL database size: %v", err))
|
||||
return
|
||||
@@ -183,7 +204,7 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Log 获取应用日志路径列表
|
||||
func (s *App) Log(w http.ResponseWriter, r *http.Request) {
|
||||
paths, err := filepath.Glob(fmt.Sprintf("%s/server/postgresql/logs/postgresql-*.log", app.Root))
|
||||
paths, err := filepath.Glob(app.Root + "/server/postgresql/logs/postgresql-*.log")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -212,7 +233,7 @@ func (s *App) SetPostgresPassword(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
oldPassword, _ := s.settingRepo.Get(biz.SettingKeyPostgresPassword)
|
||||
port := s.getPort()
|
||||
port := db.PostgresPort(app.Root)
|
||||
postgres, err := db.NewPostgres(r.Context(), "postgres", oldPassword, "127.0.0.1", port)
|
||||
if err != nil {
|
||||
// 直接修改密码
|
||||
@@ -248,32 +269,32 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tune := ConfigTune{
|
||||
// 连接设置
|
||||
ListenAddresses: s.getPGValue(config, "listen_addresses"),
|
||||
Port: s.getPGValue(config, "port"),
|
||||
MaxConnections: s.getPGValue(config, "max_connections"),
|
||||
SuperuserReservedConnections: s.getPGValue(config, "superuser_reserved_connections"),
|
||||
ListenAddresses: confval.Postgres.Get(config, "listen_addresses"),
|
||||
Port: confval.Postgres.Get(config, "port"),
|
||||
MaxConnections: confval.Postgres.Get(config, "max_connections"),
|
||||
SuperuserReservedConnections: confval.Postgres.Get(config, "superuser_reserved_connections"),
|
||||
// 内存设置
|
||||
SharedBuffers: s.getPGValue(config, "shared_buffers"),
|
||||
WorkMem: s.getPGValue(config, "work_mem"),
|
||||
MaintenanceWorkMem: s.getPGValue(config, "maintenance_work_mem"),
|
||||
EffectiveCacheSize: s.getPGValue(config, "effective_cache_size"),
|
||||
HugePages: s.getPGValue(config, "huge_pages"),
|
||||
SharedBuffers: confval.Postgres.Get(config, "shared_buffers"),
|
||||
WorkMem: confval.Postgres.Get(config, "work_mem"),
|
||||
MaintenanceWorkMem: confval.Postgres.Get(config, "maintenance_work_mem"),
|
||||
EffectiveCacheSize: confval.Postgres.Get(config, "effective_cache_size"),
|
||||
HugePages: confval.Postgres.Get(config, "huge_pages"),
|
||||
// WAL 设置
|
||||
WalLevel: s.getPGValue(config, "wal_level"),
|
||||
WalBuffers: s.getPGValue(config, "wal_buffers"),
|
||||
MaxWalSize: s.getPGValue(config, "max_wal_size"),
|
||||
MinWalSize: s.getPGValue(config, "min_wal_size"),
|
||||
CheckpointCompletionTarget: s.getPGValue(config, "checkpoint_completion_target"),
|
||||
WalLevel: confval.Postgres.Get(config, "wal_level"),
|
||||
WalBuffers: confval.Postgres.Get(config, "wal_buffers"),
|
||||
MaxWalSize: confval.Postgres.Get(config, "max_wal_size"),
|
||||
MinWalSize: confval.Postgres.Get(config, "min_wal_size"),
|
||||
CheckpointCompletionTarget: confval.Postgres.Get(config, "checkpoint_completion_target"),
|
||||
// 查询优化
|
||||
DefaultStatisticsTarget: s.getPGValue(config, "default_statistics_target"),
|
||||
RandomPageCost: s.getPGValue(config, "random_page_cost"),
|
||||
EffectiveIoConcurrency: s.getPGValue(config, "effective_io_concurrency"),
|
||||
DefaultStatisticsTarget: confval.Postgres.Get(config, "default_statistics_target"),
|
||||
RandomPageCost: confval.Postgres.Get(config, "random_page_cost"),
|
||||
EffectiveIoConcurrency: confval.Postgres.Get(config, "effective_io_concurrency"),
|
||||
// 日志设置
|
||||
LogDestination: s.getPGValue(config, "log_destination"),
|
||||
LogMinDurationStatement: s.getPGValue(config, "log_min_duration_statement"),
|
||||
LogTimezone: s.getPGValue(config, "log_timezone"),
|
||||
LogDestination: confval.Postgres.Get(config, "log_destination"),
|
||||
LogMinDurationStatement: confval.Postgres.Get(config, "log_min_duration_statement"),
|
||||
LogTimezone: confval.Postgres.Get(config, "log_timezone"),
|
||||
// IO 设置
|
||||
IoMethod: s.getPGValue(config, "io_method"),
|
||||
IoMethod: confval.Postgres.Get(config, "io_method"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -288,7 +309,7 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
confPath := s.configPath()
|
||||
oldPort := s.getPort()
|
||||
oldPort := db.PostgresPort(app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -296,32 +317,32 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 更新连接设置
|
||||
config = s.setPGValue(config, "listen_addresses", req.ListenAddresses)
|
||||
config = s.setPGValue(config, "port", req.Port)
|
||||
config = s.setPGValue(config, "max_connections", req.MaxConnections)
|
||||
config = s.setPGValue(config, "superuser_reserved_connections", req.SuperuserReservedConnections)
|
||||
config = confval.Postgres.Set(config, "listen_addresses", req.ListenAddresses)
|
||||
config = confval.Postgres.Set(config, "port", req.Port)
|
||||
config = confval.Postgres.Set(config, "max_connections", req.MaxConnections)
|
||||
config = confval.Postgres.Set(config, "superuser_reserved_connections", req.SuperuserReservedConnections)
|
||||
// 更新内存设置
|
||||
config = s.setPGValue(config, "shared_buffers", req.SharedBuffers)
|
||||
config = s.setPGValue(config, "work_mem", req.WorkMem)
|
||||
config = s.setPGValue(config, "maintenance_work_mem", req.MaintenanceWorkMem)
|
||||
config = s.setPGValue(config, "effective_cache_size", req.EffectiveCacheSize)
|
||||
config = s.setPGValue(config, "huge_pages", req.HugePages)
|
||||
config = confval.Postgres.Set(config, "shared_buffers", req.SharedBuffers)
|
||||
config = confval.Postgres.Set(config, "work_mem", req.WorkMem)
|
||||
config = confval.Postgres.Set(config, "maintenance_work_mem", req.MaintenanceWorkMem)
|
||||
config = confval.Postgres.Set(config, "effective_cache_size", req.EffectiveCacheSize)
|
||||
config = confval.Postgres.Set(config, "huge_pages", req.HugePages)
|
||||
// 更新 WAL 设置
|
||||
config = s.setPGValue(config, "wal_level", req.WalLevel)
|
||||
config = s.setPGValue(config, "wal_buffers", req.WalBuffers)
|
||||
config = s.setPGValue(config, "max_wal_size", req.MaxWalSize)
|
||||
config = s.setPGValue(config, "min_wal_size", req.MinWalSize)
|
||||
config = s.setPGValue(config, "checkpoint_completion_target", req.CheckpointCompletionTarget)
|
||||
config = confval.Postgres.Set(config, "wal_level", req.WalLevel)
|
||||
config = confval.Postgres.Set(config, "wal_buffers", req.WalBuffers)
|
||||
config = confval.Postgres.Set(config, "max_wal_size", req.MaxWalSize)
|
||||
config = confval.Postgres.Set(config, "min_wal_size", req.MinWalSize)
|
||||
config = confval.Postgres.Set(config, "checkpoint_completion_target", req.CheckpointCompletionTarget)
|
||||
// 更新查询优化
|
||||
config = s.setPGValue(config, "default_statistics_target", req.DefaultStatisticsTarget)
|
||||
config = s.setPGValue(config, "random_page_cost", req.RandomPageCost)
|
||||
config = s.setPGValue(config, "effective_io_concurrency", req.EffectiveIoConcurrency)
|
||||
config = confval.Postgres.Set(config, "default_statistics_target", req.DefaultStatisticsTarget)
|
||||
config = confval.Postgres.Set(config, "random_page_cost", req.RandomPageCost)
|
||||
config = confval.Postgres.Set(config, "effective_io_concurrency", req.EffectiveIoConcurrency)
|
||||
// 更新日志设置
|
||||
config = s.setPGValue(config, "log_destination", req.LogDestination)
|
||||
config = s.setPGValue(config, "log_min_duration_statement", req.LogMinDurationStatement)
|
||||
config = s.setPGValue(config, "log_timezone", req.LogTimezone)
|
||||
config = confval.Postgres.Set(config, "log_destination", req.LogDestination)
|
||||
config = confval.Postgres.Set(config, "log_min_duration_statement", req.LogMinDurationStatement)
|
||||
config = confval.Postgres.Set(config, "log_timezone", req.LogTimezone)
|
||||
// 更新 IO 设置
|
||||
config = s.setPGValue(config, "io_method", req.IoMethod)
|
||||
config = confval.Postgres.Set(config, "io_method", req.IoMethod)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -336,22 +357,533 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/postgresql/data/postgresql.conf", app.Root)
|
||||
// ExtensionList 获取扩展列表及安装状态
|
||||
func (s *App) ExtensionList(w http.ResponseWriter, r *http.Request) {
|
||||
extensions := s.getExtensions()
|
||||
for i := range extensions {
|
||||
controlPath := fmt.Sprintf("%s/server/postgresql/share/extension/%s.control", app.Root, extensions[i].ExtName)
|
||||
extensions[i].Installed = io.Exists(controlPath)
|
||||
if extensions[i].Installed {
|
||||
control, _ := io.Read(controlPath)
|
||||
if m := defaultVersionRegexp.FindStringSubmatch(control); len(m) > 1 {
|
||||
extensions[i].InstalledVersion = m[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, extensions)
|
||||
}
|
||||
|
||||
// getPort 读取 PostgreSQL 端口
|
||||
func (s *App) getPort() uint {
|
||||
// InstallExtension 安装扩展(异步任务)
|
||||
func (s *App) InstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExtensionSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExtension(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/postgresql/extensions/%s.sh' | bash -s -- 'install'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "postgresql:extension:" + req.Slug
|
||||
task.Name = s.t.Get("Install PostgreSQL extension %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UninstallExtension 卸载扩展(异步任务)
|
||||
func (s *App) UninstallExtension(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExtensionSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExtension(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/postgresql/extensions/%s.sh' | bash -s -- 'uninstall'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = "postgresql:extension:" + req.Slug
|
||||
task.Name = s.t.Get("Uninstall PostgreSQL extension %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// EnableExtension 在指定数据库启用扩展
|
||||
func (s *App) EnableExtension(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExtensionEnable](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ext, ok := lo.Find(s.getExtensions(), func(e Extension) bool {
|
||||
return e.Slug == req.Slug
|
||||
})
|
||||
if !ok {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("extension %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context(), req.Database)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(fmt.Sprintf(`CREATE EXTENSION IF NOT EXISTS "%s"`, ext.ExtName)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to enable extension: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// SessionList 获取会话列表
|
||||
func (s *App) SessionList(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
rows, err := postgres.Query(`
|
||||
SELECT a.pid, coalesce(a.datname,''), coalesce(a.usename,''), coalesce(a.client_addr::text,''),
|
||||
coalesce(a.state,''), coalesce(a.wait_event_type,''), coalesce(a.wait_event,''),
|
||||
coalesce(array_to_string(pg_blocking_pids(a.pid),','),''),
|
||||
coalesce(extract(epoch from (now()-a.xact_start))::bigint,0),
|
||||
coalesce(extract(epoch from (now()-a.query_start))::bigint,0),
|
||||
coalesce(a.query,'')
|
||||
FROM pg_stat_activity a
|
||||
WHERE a.pid != pg_backend_pid() AND a.backend_type = 'client backend'
|
||||
ORDER BY a.backend_start`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
sessions := make([]Session, 0)
|
||||
for rows.Next() {
|
||||
var item Session
|
||||
if err = rows.Scan(&item.PID, &item.Database, &item.User, &item.ClientAddr, &item.State,
|
||||
&item.WaitEventType, &item.WaitEvent, &item.BlockedBy, &item.XactSeconds, &item.QuerySeconds, &item.Query); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
sessions = append(sessions, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, sessions)
|
||||
}
|
||||
|
||||
// TerminateSession 终止会话
|
||||
func (s *App) TerminateSession(w http.ResponseWriter, r *http.Request) {
|
||||
pid := cast.ToInt64(chi.URLParam(r, "pid"))
|
||||
if pid <= 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid pid"))
|
||||
return
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(`SELECT pg_terminate_backend($1)`, pid); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// TopSQL 获取 SQL 性能统计
|
||||
func (s *App) TopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
rows, err := postgres.Query(`
|
||||
SELECT coalesce(d.datname,''), s.calls, round(s.total_exec_time)::bigint,
|
||||
round(s.mean_exec_time::numeric,2)::float8, s.rows,
|
||||
coalesce(round(100.0*s.shared_blks_hit/nullif(s.shared_blks_hit+s.shared_blks_read,0),1),0)::float8,
|
||||
s.query
|
||||
FROM pg_stat_statements s LEFT JOIN pg_database d ON d.oid = s.dbid
|
||||
ORDER BY s.total_exec_time DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
// pg_stat_statements 未启用时返回状态而非报错
|
||||
if strings.Contains(err.Error(), "shared_preload_libraries") {
|
||||
service.Success(w, TopSQL{Enabled: false, PendingRestart: true, Items: []TopSQLItem{}})
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "does not exist") {
|
||||
service.Success(w, TopSQL{Enabled: false, Items: []TopSQLItem{}})
|
||||
return
|
||||
}
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]TopSQLItem, 0)
|
||||
for rows.Next() {
|
||||
var item TopSQLItem
|
||||
if err = rows.Scan(&item.Database, &item.Calls, &item.TotalMs, &item.MeanMs, &item.Rows, &item.HitRate, &item.Query); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, TopSQL{Enabled: true, Items: items})
|
||||
}
|
||||
|
||||
// EnableTopSQL 启用 pg_stat_statements
|
||||
func (s *App) EnableTopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(s.configPath())
|
||||
if err != nil {
|
||||
return 5432
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
return s.parsePort(config)
|
||||
|
||||
// 将 pg_stat_statements 加入 shared_preload_libraries,重启后生效
|
||||
var libs []string
|
||||
for lib := range strings.SplitSeq(confval.Postgres.Get(config, "shared_preload_libraries"), ",") {
|
||||
if lib = strings.TrimSpace(lib); lib != "" {
|
||||
libs = append(libs, lib)
|
||||
}
|
||||
}
|
||||
if !slices.Contains(libs, "pg_stat_statements") {
|
||||
libs = append(libs, "pg_stat_statements")
|
||||
config = confval.Postgres.Set(config, "shared_preload_libraries", strings.Join(libs, ","))
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(`CREATE EXTENSION IF NOT EXISTS pg_stat_statements`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ResetTopSQL 重置 SQL 性能统计
|
||||
func (s *App) ResetTopSQL(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(`SELECT pg_stat_statements_reset()`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DatabaseList 获取可连接的数据库列表
|
||||
func (s *App) DatabaseList(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
rows, err := postgres.Query(`SELECT datname FROM pg_database WHERE datallowconn ORDER BY datname`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
databases := make([]string, 0)
|
||||
for rows.Next() {
|
||||
var name string
|
||||
if err = rows.Scan(&name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
databases = append(databases, name)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, databases)
|
||||
}
|
||||
|
||||
// BloatList 获取指定数据库的表膨胀情况
|
||||
func (s *App) BloatList(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[BloatQuery](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context(), req.Database)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
rows, err := postgres.Query(`
|
||||
SELECT schemaname, relname, pg_size_pretty(pg_total_relation_size(relid)),
|
||||
n_live_tup, n_dead_tup,
|
||||
coalesce(round(100.0*n_dead_tup/nullif(n_live_tup+n_dead_tup,0),1),0)::float8,
|
||||
coalesce(to_char(last_vacuum,'YYYY-MM-DD HH24:MI'),''), coalesce(to_char(last_autovacuum,'YYYY-MM-DD HH24:MI'),''),
|
||||
coalesce(to_char(last_analyze,'YYYY-MM-DD HH24:MI'),''), coalesce(to_char(last_autoanalyze,'YYYY-MM-DD HH24:MI'),'')
|
||||
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 50`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = rows.Close() }()
|
||||
|
||||
items := make([]BloatItem, 0)
|
||||
for rows.Next() {
|
||||
var item BloatItem
|
||||
if err = rows.Scan(&item.Schema, &item.Table, &item.Size, &item.LiveTuples, &item.DeadTuples,
|
||||
&item.DeadRate, &item.LastVacuum, &item.LastAutovacuum, &item.LastAnalyze, &item.LastAutoanalyz); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, Bloat{
|
||||
RepackInstalled: io.Exists(app.Root + "/server/postgresql/share/extension/pg_repack.control"),
|
||||
Items: items,
|
||||
})
|
||||
}
|
||||
|
||||
// RunMaintenance 对表执行维护操作(异步任务)
|
||||
func (s *App) RunMaintenance(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[MaintenanceRun](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !slices.Contains([]string{"vacuum", "analyze", "repack"}, req.Operation) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid operation"))
|
||||
return
|
||||
}
|
||||
|
||||
var cmd string
|
||||
switch req.Operation {
|
||||
case "vacuum":
|
||||
cmd = fmt.Sprintf(`su - postgres -c 'psql -d "%s" -c "VACUUM \"%s\".\"%s\""'`, req.Database, req.Schema, req.Table)
|
||||
case "analyze":
|
||||
cmd = fmt.Sprintf(`su - postgres -c 'psql -d "%s" -c "ANALYZE \"%s\".\"%s\""'`, req.Database, req.Schema, req.Table)
|
||||
case "repack":
|
||||
if !io.Exists(app.Root + "/server/postgresql/share/extension/pg_repack.control") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("pg_repack is not installed, please install it in the extensions tab first"))
|
||||
return
|
||||
}
|
||||
cmd = fmt.Sprintf(`su - postgres -c 'pg_repack -d "%s" -t "%s.%s"'`, req.Database, req.Schema, req.Table)
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = fmt.Sprintf("postgresql:maintenance:%s:%s.%s", req.Database, req.Schema, req.Table)
|
||||
task.Name = s.t.Get("Run %s on table %s.%s of database %s", req.Operation, req.Schema, req.Table, req.Database)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// WalStatus 获取 WAL 及复制状态
|
||||
func (s *App) WalStatus(w http.ResponseWriter, r *http.Request) {
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
var wal Wal
|
||||
if err = postgres.QueryRow(`SELECT pg_size_pretty(coalesce(sum(size),0)) FROM pg_ls_waldir()`).Scan(&wal.WalSize); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = postgres.QueryRow(`SELECT archived_count, failed_count, coalesce(last_archived_wal,''), coalesce(last_failed_wal,'') FROM pg_stat_archiver`).Scan(
|
||||
&wal.Archiver.ArchivedCount, &wal.Archiver.FailedCount, &wal.Archiver.LastArchivedWal, &wal.Archiver.LastFailedWal); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
slotRows, err := postgres.Query(`SELECT slot_name, slot_type, active, coalesce(pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)),'') FROM pg_replication_slots`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = slotRows.Close() }()
|
||||
wal.Slots = make([]ReplicationSlot, 0)
|
||||
for slotRows.Next() {
|
||||
var slot ReplicationSlot
|
||||
if err = slotRows.Scan(&slot.Name, &slot.Type, &slot.Active, &slot.RetainedWal); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
wal.Slots = append(wal.Slots, slot)
|
||||
}
|
||||
if err = slotRows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
replRows, err := postgres.Query(`SELECT coalesce(client_addr::text,''), coalesce(state,''), coalesce(sync_state,''), coalesce(pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)),'') FROM pg_stat_replication`)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer func() { _ = replRows.Close() }()
|
||||
wal.Replications = make([]Replication, 0)
|
||||
for replRows.Next() {
|
||||
var repl Replication
|
||||
if err = replRows.Scan(&repl.ClientAddr, &repl.State, &repl.SyncState, &repl.Lag); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
wal.Replications = append(wal.Replications, repl)
|
||||
}
|
||||
if err = replRows.Err(); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, wal)
|
||||
}
|
||||
|
||||
// DropReplicationSlot 删除复制槽
|
||||
func (s *App) DropReplicationSlot(w http.ResponseWriter, r *http.Request) {
|
||||
slot := chi.URLParam(r, "slot")
|
||||
if slot == "" {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("invalid slot name"))
|
||||
return
|
||||
}
|
||||
|
||||
postgres, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer postgres.Close()
|
||||
|
||||
if _, err = postgres.Exec(`SELECT pg_drop_replication_slot($1)`, slot); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getExtensions 返回所有扩展定义
|
||||
func (s *App) getExtensions() []Extension {
|
||||
return []Extension{
|
||||
{Name: "pgvector", Slug: "pgvector", ExtName: "vector", Description: s.t.Get("Vector similarity search")},
|
||||
{Name: "PostGIS", Slug: "postgis", ExtName: "postgis", Description: s.t.Get("Spatial and geographic objects support")},
|
||||
{Name: "TimescaleDB", Slug: "timescaledb", ExtName: "timescaledb", Description: s.t.Get("Time-series database engine, requires restarting PostgreSQL after installation")},
|
||||
{Name: "zhparser", Slug: "zhparser", ExtName: "zhparser", Description: s.t.Get("Chinese full-text search parser based on SCWS")},
|
||||
{Name: "pg_repack", Slug: "pg_repack", ExtName: "pg_repack", Description: s.t.Get("Reorganize tables online to remove bloat")},
|
||||
{Name: "pg_cron", Slug: "pg_cron", ExtName: "pg_cron", Description: s.t.Get("Run periodic jobs inside the database, requires restarting PostgreSQL after installation")},
|
||||
{Name: "pg_partman", Slug: "pg_partman", ExtName: "pg_partman", Description: s.t.Get("Automated partition management")},
|
||||
{Name: "pgaudit", Slug: "pgaudit", ExtName: "pgaudit", Description: s.t.Get("Session and object audit logging, requires restarting PostgreSQL after installation")},
|
||||
{Name: "pg_hint_plan", Slug: "pg_hint_plan", ExtName: "pg_hint_plan", Description: s.t.Get("Control execution plans with hints in SQL comments, requires restarting PostgreSQL after installation")},
|
||||
{Name: "pg_stat_monitor", Slug: "pg_stat_monitor", ExtName: "pg_stat_monitor", Description: s.t.Get("Advanced query performance monitoring, requires restarting PostgreSQL after installation")},
|
||||
{Name: "pg_ivm", Slug: "pg_ivm", ExtName: "pg_ivm", Description: s.t.Get("Incremental view maintenance for materialized views")},
|
||||
{Name: "hypopg", Slug: "hypopg", ExtName: "hypopg", Description: s.t.Get("Hypothetical indexes for query plan testing")},
|
||||
{Name: "pgmq", Slug: "pgmq", ExtName: "pgmq", Description: s.t.Get("Lightweight message queue")},
|
||||
{Name: "orafce", Slug: "orafce", ExtName: "orafce", Description: s.t.Get("Oracle compatibility functions")},
|
||||
{Name: "http", Slug: "http", ExtName: "http", Description: s.t.Get("HTTP client for SQL, send requests from the database")},
|
||||
}
|
||||
}
|
||||
|
||||
// checkExtension 检查 slug 是否有效
|
||||
func (s *App) checkExtension(slug string) bool {
|
||||
return lo.ContainsBy(s.getExtensions(), func(e Extension) bool {
|
||||
return e.Slug == slug
|
||||
})
|
||||
}
|
||||
|
||||
// connect 以 postgres 超级用户连接指定数据库,默认 postgres 库
|
||||
func (s *App) connect(ctx context.Context, database ...string) (db.Operator, error) {
|
||||
password, err := s.settingRepo.Get(biz.SettingKeyPostgresPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return db.NewPostgres(ctx, "postgres", password, "127.0.0.1", db.PostgresPort(app.Root), database...)
|
||||
}
|
||||
|
||||
func (s *App) configPath() string {
|
||||
return app.Root + "/server/postgresql/data/postgresql.conf"
|
||||
}
|
||||
|
||||
// parsePort 从 config 内容解析端口,未配置时返回默认值
|
||||
func (s *App) parsePort(config string) uint {
|
||||
port := cast.ToUint(s.getPGValue(config, "port"))
|
||||
port := cast.ToUint(confval.Postgres.Get(config, "port"))
|
||||
if port == 0 {
|
||||
return 5432
|
||||
}
|
||||
@@ -369,79 +901,3 @@ func (s *App) applyConfig(newConfig string, oldPort uint) error {
|
||||
}
|
||||
return s.databaseServerRepo.UpdatePort("local_postgresql", newPort)
|
||||
}
|
||||
|
||||
// getPGValue 从 PostgreSQL 配置内容中获取指定键的值
|
||||
func (s *App) getPGValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
v := strings.TrimSpace(parts[1])
|
||||
// 去除行尾注释
|
||||
if idx := strings.Index(v, "#"); idx >= 0 {
|
||||
v = strings.TrimSpace(v[:idx])
|
||||
}
|
||||
// 去除引号
|
||||
v = strings.Trim(v, "'\"")
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setPGValue 在 PostgreSQL 配置内容中设置指定键的值
|
||||
func (s *App) setPGValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
k := strings.TrimSpace(parts[0])
|
||||
if k == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" = '"+value+"'")
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" = '"+value+"'")
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,125 @@ type SetPostgresPassword struct {
|
||||
Password string `form:"password" json:"password" validate:"required && password"`
|
||||
}
|
||||
|
||||
// Extension PostgreSQL 扩展信息
|
||||
type Extension struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"` // 下载脚本名,如 pgvector
|
||||
ExtName string `json:"ext_name"` // CREATE EXTENSION 使用的扩展名,如 vector
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
InstalledVersion string `json:"installed_version"` // control 文件中的 default_version,仅展示
|
||||
}
|
||||
|
||||
// ExtensionSlug 扩展操作请求
|
||||
type ExtensionSlug struct {
|
||||
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||
}
|
||||
|
||||
// ExtensionEnable 在指定数据库启用扩展请求
|
||||
type ExtensionEnable struct {
|
||||
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||
Database string `form:"database" json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
// Session 数据库会话信息
|
||||
type Session struct {
|
||||
PID int64 `json:"pid"`
|
||||
Database string `json:"database"`
|
||||
User string `json:"user"`
|
||||
ClientAddr string `json:"client_addr"`
|
||||
State string `json:"state"`
|
||||
WaitEventType string `json:"wait_event_type"`
|
||||
WaitEvent string `json:"wait_event"`
|
||||
BlockedBy string `json:"blocked_by"`
|
||||
XactSeconds int64 `json:"xact_seconds"`
|
||||
QuerySeconds int64 `json:"query_seconds"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// TopSQLItem Top SQL 统计项
|
||||
type TopSQLItem struct {
|
||||
Database string `json:"database"`
|
||||
Calls int64 `json:"calls"`
|
||||
TotalMs int64 `json:"total_ms"`
|
||||
MeanMs float64 `json:"mean_ms"`
|
||||
Rows int64 `json:"rows"`
|
||||
HitRate float64 `json:"hit_rate"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// TopSQL Top SQL 响应
|
||||
type TopSQL struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
PendingRestart bool `json:"pending_restart"` // 已配置 preload,等待重启生效
|
||||
Items []TopSQLItem `json:"items"`
|
||||
}
|
||||
|
||||
// BloatQuery 表膨胀查询请求
|
||||
type BloatQuery struct {
|
||||
Database string `form:"database" json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
// BloatItem 表膨胀信息
|
||||
type BloatItem struct {
|
||||
Schema string `json:"schema"`
|
||||
Table string `json:"table"`
|
||||
Size string `json:"size"`
|
||||
LiveTuples int64 `json:"live_tuples"`
|
||||
DeadTuples int64 `json:"dead_tuples"`
|
||||
DeadRate float64 `json:"dead_rate"`
|
||||
LastVacuum string `json:"last_vacuum"`
|
||||
LastAutovacuum string `json:"last_autovacuum"`
|
||||
LastAnalyze string `json:"last_analyze"`
|
||||
LastAutoanalyz string `json:"last_autoanalyze"`
|
||||
}
|
||||
|
||||
// Bloat 表膨胀响应
|
||||
type Bloat struct {
|
||||
RepackInstalled bool `json:"repack_installed"`
|
||||
Items []BloatItem `json:"items"`
|
||||
}
|
||||
|
||||
// MaintenanceRun 表维护操作请求
|
||||
type MaintenanceRun struct {
|
||||
Database string `form:"database" json:"database" validate:"required"`
|
||||
Schema string `form:"schema" json:"schema" validate:"required"`
|
||||
Table string `form:"table" json:"table" validate:"required"`
|
||||
Operation string `form:"operation" json:"operation" validate:"required"`
|
||||
}
|
||||
|
||||
// ReplicationSlot 复制槽信息
|
||||
type ReplicationSlot struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Active bool `json:"active"`
|
||||
RetainedWal string `json:"retained_wal"`
|
||||
}
|
||||
|
||||
// Replication 流复制连接信息
|
||||
type Replication struct {
|
||||
ClientAddr string `json:"client_addr"`
|
||||
State string `json:"state"`
|
||||
SyncState string `json:"sync_state"`
|
||||
Lag string `json:"lag"`
|
||||
}
|
||||
|
||||
// WalArchiver WAL 归档统计
|
||||
type WalArchiver struct {
|
||||
ArchivedCount int64 `json:"archived_count"`
|
||||
FailedCount int64 `json:"failed_count"`
|
||||
LastArchivedWal string `json:"last_archived_wal"`
|
||||
LastFailedWal string `json:"last_failed_wal"`
|
||||
}
|
||||
|
||||
// Wal WAL 状态响应
|
||||
type Wal struct {
|
||||
WalSize string `json:"wal_size"`
|
||||
Archiver WalArchiver `json:"archiver"`
|
||||
Slots []ReplicationSlot `json:"slots"`
|
||||
Replications []Replication `json:"replications"`
|
||||
}
|
||||
|
||||
// ConfigTune PostgreSQL 配置调整
|
||||
type ConfigTune struct {
|
||||
// 连接设置
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
@@ -29,9 +30,9 @@ type App struct {
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, taskRepo biz.TaskRepo) (*App, error) {
|
||||
func NewApp(conf *config.Config, t *gotext.Locale, taskRepo biz.TaskRepo) *App {
|
||||
|
||||
return &App{t: t, conf: conf, taskRepo: taskRepo}, nil
|
||||
return &App{t: t, conf: conf, taskRepo: taskRepo}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -105,33 +106,16 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root))
|
||||
service.Success(w, conf)
|
||||
common.ServeConfig(w, app.Root+"/server/prometheus/prometheus.yml")
|
||||
}
|
||||
|
||||
func (s *App) 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(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, app.Root+"/server/prometheus/prometheus.yml", "prometheus")
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Prometheus 全局配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root))
|
||||
conf, _ := io.Read(app.Root + "/server/prometheus/prometheus.yml")
|
||||
|
||||
var cfg struct {
|
||||
Global struct {
|
||||
@@ -159,7 +143,7 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root)
|
||||
confPath := app.Root + "/server/prometheus/prometheus.yml"
|
||||
raw, _ := io.Read(confPath)
|
||||
|
||||
var cfg map[string]any
|
||||
@@ -205,7 +189,7 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetAlertmanagerConfig 获取 Alertmanager 配置
|
||||
func (s *App) GetAlertmanagerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/alertmanager/alertmanager.yml", app.Root))
|
||||
conf, _ := io.Read(app.Root + "/server/prometheus/alertmanager/alertmanager.yml")
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
@@ -217,7 +201,7 @@ func (s *App) UpdateAlertmanagerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/prometheus/alertmanager/alertmanager.yml", app.Root), req.Config, 0644); err != nil {
|
||||
if err = io.Write(app.Root+"/server/prometheus/alertmanager/alertmanager.yml", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/firewall"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
@@ -25,11 +26,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -148,13 +149,13 @@ func (s *App) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetPort 获取端口
|
||||
func (s *App) GetPort(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/pure-ftpd/etc/pure-ftpd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get port: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
bind := strings.Trim(s.getFTPValue(config, "Bind"), `"'`)
|
||||
bind := strings.Trim(confval.FTP.Get(config, "Bind"), `"'`)
|
||||
port := 21 // 默认端口
|
||||
if parts := strings.SplitN(bind, ",", 2); len(parts) == 2 {
|
||||
port = cast.ToInt(strings.TrimSpace(parts[1]))
|
||||
@@ -171,13 +172,13 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root)
|
||||
confPath := app.Root + "/server/pure-ftpd/etc/pure-ftpd.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
config = s.setFTPValue(config, "Bind", fmt.Sprintf(`"0.0.0.0,%d"`, req.Port))
|
||||
config = confval.FTP.Set(config, "Bind", fmt.Sprintf(`"0.0.0.0,%d"`, req.Port))
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -206,21 +207,21 @@ func (s *App) UpdatePort(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetConfigTune 获取 Pure-FTPd 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root))
|
||||
config, err := io.Read(app.Root + "/server/pure-ftpd/etc/pure-ftpd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
MaxClientsNumber: s.getFTPValue(config, "MaxClientsNumber"),
|
||||
MaxClientsPerIP: s.getFTPValue(config, "MaxClientsPerIP"),
|
||||
MaxIdleTime: s.getFTPValue(config, "MaxIdleTime"),
|
||||
MaxLoad: s.getFTPValue(config, "MaxLoad"),
|
||||
PassivePortRange: s.getFTPValue(config, "PassivePortRange"),
|
||||
AnonymousOnly: s.getFTPValue(config, "AnonymousOnly"),
|
||||
NoAnonymous: s.getFTPValue(config, "NoAnonymous"),
|
||||
MaxDiskUsage: s.getFTPValue(config, "MaxDiskUsage"),
|
||||
MaxClientsNumber: confval.FTP.Get(config, "MaxClientsNumber"),
|
||||
MaxClientsPerIP: confval.FTP.Get(config, "MaxClientsPerIP"),
|
||||
MaxIdleTime: confval.FTP.Get(config, "MaxIdleTime"),
|
||||
MaxLoad: confval.FTP.Get(config, "MaxLoad"),
|
||||
PassivePortRange: confval.FTP.Get(config, "PassivePortRange"),
|
||||
AnonymousOnly: confval.FTP.Get(config, "AnonymousOnly"),
|
||||
NoAnonymous: confval.FTP.Get(config, "NoAnonymous"),
|
||||
MaxDiskUsage: confval.FTP.Get(config, "MaxDiskUsage"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
@@ -234,21 +235,21 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/pure-ftpd/etc/pure-ftpd.conf", app.Root)
|
||||
confPath := app.Root + "/server/pure-ftpd/etc/pure-ftpd.conf"
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setFTPValue(config, "MaxClientsNumber", req.MaxClientsNumber)
|
||||
config = s.setFTPValue(config, "MaxClientsPerIP", req.MaxClientsPerIP)
|
||||
config = s.setFTPValue(config, "MaxIdleTime", req.MaxIdleTime)
|
||||
config = s.setFTPValue(config, "MaxLoad", req.MaxLoad)
|
||||
config = s.setFTPValue(config, "PassivePortRange", req.PassivePortRange)
|
||||
config = s.setFTPValue(config, "AnonymousOnly", req.AnonymousOnly)
|
||||
config = s.setFTPValue(config, "NoAnonymous", req.NoAnonymous)
|
||||
config = s.setFTPValue(config, "MaxDiskUsage", req.MaxDiskUsage)
|
||||
config = confval.FTP.Set(config, "MaxClientsNumber", req.MaxClientsNumber)
|
||||
config = confval.FTP.Set(config, "MaxClientsPerIP", req.MaxClientsPerIP)
|
||||
config = confval.FTP.Set(config, "MaxIdleTime", req.MaxIdleTime)
|
||||
config = confval.FTP.Set(config, "MaxLoad", req.MaxLoad)
|
||||
config = confval.FTP.Set(config, "PassivePortRange", req.PassivePortRange)
|
||||
config = confval.FTP.Set(config, "AnonymousOnly", req.AnonymousOnly)
|
||||
config = confval.FTP.Set(config, "NoAnonymous", req.NoAnonymous)
|
||||
config = confval.FTP.Set(config, "MaxDiskUsage", req.MaxDiskUsage)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -262,65 +263,3 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getFTPValue 从 Pure-FTPd 配置内容中获取指定键的值
|
||||
func (s *App) getFTPValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setFTPValue 在 Pure-FTPd 配置内容中设置指定键的值
|
||||
func (s *App) setFTPValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
found := false
|
||||
result := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 保留原行格式
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
result = append(result, indent+key+" "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
|
||||
+298
-107
@@ -1,36 +1,53 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/db"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/tools"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
taskRepo biz.TaskRepo
|
||||
slug string // 服务名与配置目录名,如 redis、valkey
|
||||
name string // 展示名,如 Redis、Valkey
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) (*App, error) {
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return New("redis", "Redis", t, databaseServerRepo, taskRepo)
|
||||
}
|
||||
|
||||
databaseServer := databaseServerRepo
|
||||
func New(slug, name string, t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
databaseServerRepo: databaseServer,
|
||||
}, nil
|
||||
databaseServerRepo: databaseServerRepo,
|
||||
taskRepo: taskRepo,
|
||||
slug: slug,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -39,17 +56,24 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// 性能诊断
|
||||
r.Get("/slow_log", s.SlowLog)
|
||||
r.Post("/slow_log/reset", s.ResetSlowLog)
|
||||
r.Get("/clients", s.ClientList)
|
||||
r.Post("/clients/kill", s.KillClient)
|
||||
r.Get("/memory", s.MemoryStatus)
|
||||
r.Post("/bigkeys", s.ScanBigKeys)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("redis")
|
||||
ok, _ := systemctl.Status(s.slug)
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("redis")
|
||||
status, err := systemctl.Status(s.slug)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get redis status: %v", err))
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get %s status: %v", s.name, err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
@@ -57,9 +81,9 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 Redis 密码
|
||||
// 检查密码
|
||||
withPassword := ""
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/redis/redis.conf", app.Root))
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -70,9 +94,9 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
withPassword = " -a " + matches[1]
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("redis-cli%s info", withPassword)
|
||||
raw, err := shell.Execf("%s%s info", s.slug+"-cli", withPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get redis info: %v", err))
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get %s info: %v", s.name, err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -104,60 +128,38 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/redis/redis.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, s.confPath())
|
||||
}
|
||||
|
||||
func (s *App) 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(fmt.Sprintf("%s/server/redis/redis.conf", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("redis"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, s.confPath(), s.slug)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Redis 配置调整参数
|
||||
// GetConfigTune 获取配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/redis/redis.conf", app.Root))
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
Bind: s.getRedisValue(config, "bind"),
|
||||
Port: s.getRedisValue(config, "port"),
|
||||
Databases: s.getRedisValue(config, "databases"),
|
||||
Requirepass: s.getRedisValue(config, "requirepass"),
|
||||
Timeout: s.getRedisValue(config, "timeout"),
|
||||
TCPKeepalive: s.getRedisValue(config, "tcp-keepalive"),
|
||||
Maxmemory: s.getRedisValue(config, "maxmemory"),
|
||||
MaxmemoryPolicy: s.getRedisValue(config, "maxmemory-policy"),
|
||||
Appendonly: s.getRedisValue(config, "appendonly"),
|
||||
Appendfsync: s.getRedisValue(config, "appendfsync"),
|
||||
Bind: confval.Directive.Get(config, "bind"),
|
||||
Port: confval.Directive.Get(config, "port"),
|
||||
Databases: confval.Directive.Get(config, "databases"),
|
||||
Requirepass: confval.Directive.Get(config, "requirepass"),
|
||||
Timeout: confval.Directive.Get(config, "timeout"),
|
||||
TCPKeepalive: confval.Directive.Get(config, "tcp-keepalive"),
|
||||
Maxmemory: confval.Directive.Get(config, "maxmemory"),
|
||||
MaxmemoryPolicy: confval.Directive.Get(config, "maxmemory-policy"),
|
||||
Appendonly: confval.Directive.Get(config, "appendonly"),
|
||||
Appendfsync: confval.Directive.Get(config, "appendfsync"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Redis 配置调整参数
|
||||
// UpdateConfigTune 更新配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
@@ -165,96 +167,285 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/redis/redis.conf", app.Root)
|
||||
confPath := s.confPath()
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setRedisValue(config, "bind", req.Bind)
|
||||
config = s.setRedisValue(config, "port", req.Port)
|
||||
config = s.setRedisValue(config, "databases", req.Databases)
|
||||
config = s.setRedisValue(config, "requirepass", req.Requirepass)
|
||||
config = s.setRedisValue(config, "timeout", req.Timeout)
|
||||
config = s.setRedisValue(config, "tcp-keepalive", req.TCPKeepalive)
|
||||
config = s.setRedisValue(config, "maxmemory", req.Maxmemory)
|
||||
config = s.setRedisValue(config, "maxmemory-policy", req.MaxmemoryPolicy)
|
||||
config = s.setRedisValue(config, "appendonly", req.Appendonly)
|
||||
config = s.setRedisValue(config, "appendfsync", req.Appendfsync)
|
||||
config = confval.Directive.Set(config, "bind", req.Bind)
|
||||
config = confval.Directive.Set(config, "port", req.Port)
|
||||
config = confval.Directive.Set(config, "databases", req.Databases)
|
||||
config = confval.Directive.Set(config, "requirepass", req.Requirepass)
|
||||
config = confval.Directive.Set(config, "timeout", req.Timeout)
|
||||
config = confval.Directive.Set(config, "tcp-keepalive", req.TCPKeepalive)
|
||||
config = confval.Directive.Set(config, "maxmemory", req.Maxmemory)
|
||||
config = confval.Directive.Set(config, "maxmemory-policy", req.MaxmemoryPolicy)
|
||||
config = confval.Directive.Set(config, "appendonly", req.Appendonly)
|
||||
config = confval.Directive.Set(config, "appendfsync", req.Appendfsync)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("redis"); err != nil {
|
||||
if err = systemctl.Restart(s.slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 同步密码到数据库服务器记录
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_redis", req.Requirepass)
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_"+s.slug, req.Requirepass)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getRedisValue 从 Redis 配置内容中获取指定键的值
|
||||
func (s *App) getRedisValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
// SlowLog 获取慢日志
|
||||
func (s *App) SlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
reply, err := conn.Exec("SLOWLOG", "GET", 50)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
rows, err := redigo.Values(reply, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
entries := make([]SlowLogEntry, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item, itemErr := redigo.Values(row, nil)
|
||||
if itemErr != nil || len(item) < 4 {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
entry := SlowLogEntry{}
|
||||
entry.ID, _ = redigo.Int64(item[0], nil)
|
||||
if ts, tsErr := redigo.Int64(item[1], nil); tsErr == nil {
|
||||
entry.Time = time.Unix(ts, 0).Format(time.DateTime)
|
||||
}
|
||||
entry.DurationUs, _ = redigo.Int64(item[2], nil)
|
||||
if cmd, cmdErr := redigo.Strings(item[3], nil); cmdErr == nil {
|
||||
entry.Command = strings.Join(cmd, " ")
|
||||
}
|
||||
if len(item) > 4 {
|
||||
entry.Client, _ = redigo.String(item[4], nil)
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return ""
|
||||
|
||||
service.Success(w, entries)
|
||||
}
|
||||
|
||||
// setRedisValue 在 Redis 配置内容中设置指定键的值
|
||||
func (s *App) setRedisValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
// ResetSlowLog 重置慢日志
|
||||
func (s *App) ResetSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
if _, err = conn.Exec("SLOWLOG", "RESET"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ClientList 获取客户端连接列表
|
||||
func (s *App) ClientList(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
raw, err := redigo.String(conn.Exec("CLIENT", "LIST"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
clients := make([]Client, 0)
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(raw), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 1 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
fields := map[string]string{}
|
||||
for kv := range strings.FieldsSeq(line) {
|
||||
if key, value, found := strings.Cut(kv, "="); found {
|
||||
fields[key] = value
|
||||
}
|
||||
found = true
|
||||
// 值为空时注释掉该配置项
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "# "+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
clients = append(clients, Client{
|
||||
ID: fields["id"],
|
||||
Addr: fields["addr"],
|
||||
Name: fields["name"],
|
||||
DB: fields["db"],
|
||||
Age: fields["age"],
|
||||
Idle: fields["idle"],
|
||||
Cmd: fields["cmd"],
|
||||
})
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
|
||||
service.Success(w, clients)
|
||||
}
|
||||
|
||||
// KillClient 踢除客户端连接
|
||||
func (s *App) KillClient(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ClientKill](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
killed, err := redigo.Int64(conn.Exec("CLIENT", "KILL", "ID", req.ID))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if killed == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("client %d not found", req.ID))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// MemoryStatus 获取内存诊断信息
|
||||
func (s *App) MemoryStatus(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.connect(r.Context())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
memory := Memory{Items: make([]types.NV, 0)}
|
||||
memory.Doctor, err = redigo.String(conn.Exec("MEMORY", "DOCTOR"))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
// 空库与无问题时返回的是电影梗彩蛋文案,无展示价值
|
||||
if strings.HasPrefix(memory.Doctor, "Hi Sam,") || strings.Contains(memory.Doctor, "can't find any memory issue") {
|
||||
memory.Doctor = ""
|
||||
}
|
||||
|
||||
reply, err := conn.Exec("MEMORY", "STATS")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
values, err := redigo.Values(reply, nil)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
stats := map[string]string{}
|
||||
for i := 0; i+1 < len(values); i += 2 {
|
||||
key, keyErr := redigo.String(values[i], nil)
|
||||
if keyErr != nil {
|
||||
continue
|
||||
}
|
||||
// 值可能为整数或字符串,嵌套数组(如 db.0)跳过
|
||||
if v, vErr := redigo.Int64(values[i+1], nil); vErr == nil {
|
||||
stats[key] = cast.ToString(v)
|
||||
continue
|
||||
}
|
||||
if v, vErr := redigo.String(values[i+1], nil); vErr == nil {
|
||||
stats[key] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 各版本键名存在差异,仅展示存在的指标
|
||||
items := []struct {
|
||||
key string
|
||||
name string
|
||||
bytes bool
|
||||
}{
|
||||
{"peak.allocated", s.t.Get("Peak Allocated"), true},
|
||||
{"total.allocated", s.t.Get("Total Allocated"), true},
|
||||
{"startup.allocated", s.t.Get("Startup Allocated"), true},
|
||||
{"dataset.bytes", s.t.Get("Dataset Size"), true},
|
||||
{"dataset.percentage", s.t.Get("Dataset Percentage"), false},
|
||||
{"keys.count", s.t.Get("Keys Count"), false},
|
||||
{"keys.bytes-per-key", s.t.Get("Bytes Per Key"), true},
|
||||
{"allocator-fragmentation.ratio", s.t.Get("Allocator Fragmentation Ratio"), false},
|
||||
{"fragmentation", s.t.Get("Fragmentation Ratio"), false},
|
||||
}
|
||||
for _, item := range items {
|
||||
value, ok := stats[item.key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if item.bytes {
|
||||
value = tools.FormatBytes(cast.ToFloat64(value))
|
||||
}
|
||||
memory.Items = append(memory.Items, types.NV{Name: item.name, Value: value})
|
||||
}
|
||||
|
||||
service.Success(w, memory)
|
||||
}
|
||||
|
||||
// ScanBigKeys 扫描大 Key(异步任务)
|
||||
func (s *App) ScanBigKeys(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
withPassword := ""
|
||||
if password := confval.Directive.Get(config, "requirepass"); password != "" {
|
||||
withPassword = " -a " + password
|
||||
}
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Key = s.slug + ":bigkeys"
|
||||
task.Name = s.t.Get("Scan %s big keys", s.name)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf("%s-cli%s --bigkeys", s.slug, withPassword)
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// connect 从配置文件读取端口与密码建立连接
|
||||
func (s *App) connect(ctx context.Context) (*db.Redis, error) {
|
||||
config, err := io.Read(s.confPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port := confval.Directive.Get(config, "port")
|
||||
if port == "" {
|
||||
port = "6379"
|
||||
}
|
||||
password := confval.Directive.Get(config, "requirepass")
|
||||
|
||||
return db.NewRedis(ctx, "", password, "127.0.0.1:"+port)
|
||||
}
|
||||
|
||||
func (s *App) confPath() string {
|
||||
return filepath.Join(app.Root, "server", s.slug, s.slug+".conf")
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package redis
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
import "github.com/acepanel/panel/v3/pkg/types"
|
||||
|
||||
// ConfigTune Redis 配置调整
|
||||
// ConfigTune Redis 协议兼容服务的配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Bind string `form:"bind" json:"bind"`
|
||||
@@ -20,3 +18,34 @@ type ConfigTune struct {
|
||||
Appendonly string `form:"appendonly" json:"appendonly" validate:"in:yes,no"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync" validate:"in:always,everysec,no"`
|
||||
}
|
||||
|
||||
// ClientKill 踢除客户端连接请求
|
||||
type ClientKill struct {
|
||||
ID int64 `form:"id" json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
// SlowLogEntry 慢日志条目
|
||||
type SlowLogEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
Time string `json:"time"`
|
||||
DurationUs int64 `json:"duration_us"`
|
||||
Command string `json:"command"`
|
||||
Client string `json:"client"`
|
||||
}
|
||||
|
||||
// Client 客户端连接信息
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Addr string `json:"addr"`
|
||||
Name string `json:"name"`
|
||||
DB string `json:"db"`
|
||||
Age string `json:"age"`
|
||||
Idle string `json:"idle"`
|
||||
Cmd string `json:"cmd"`
|
||||
}
|
||||
|
||||
// Memory 内存诊断信息
|
||||
type Memory struct {
|
||||
Doctor string `json:"doctor"`
|
||||
Items []types.NV `json:"items"`
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
@@ -20,9 +20,9 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{t: t}, nil
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -62,11 +62,11 @@ func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("NameServer Status"), Value: namesrvStr},
|
||||
{Name: s.t.Get("Broker Status"), Value: brokerStr},
|
||||
{Name: s.t.Get("Broker Name"), Value: s.getPropertiesValue(config, "brokerName")},
|
||||
{Name: s.t.Get("Listen Port"), Value: s.getPropertiesValue(config, "listenPort")},
|
||||
{Name: s.t.Get("NameServer Address"), Value: s.getPropertiesValue(config, "namesrvAddr")},
|
||||
{Name: s.t.Get("Broker Role"), Value: s.getPropertiesValue(config, "brokerRole")},
|
||||
{Name: s.t.Get("Flush Disk Type"), Value: s.getPropertiesValue(config, "flushDiskType")},
|
||||
{Name: s.t.Get("Broker Name"), Value: confval.Properties.Get(config, "brokerName")},
|
||||
{Name: s.t.Get("Listen Port"), Value: confval.Properties.Get(config, "listenPort")},
|
||||
{Name: s.t.Get("NameServer Address"), Value: confval.Properties.Get(config, "namesrvAddr")},
|
||||
{Name: s.t.Get("Broker Role"), Value: confval.Properties.Get(config, "brokerRole")},
|
||||
{Name: s.t.Get("Flush Disk Type"), Value: confval.Properties.Get(config, "flushDiskType")},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
@@ -106,14 +106,14 @@ func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
brokerInit, brokerMax := s.parseHeapLine(heapRaw, "ROCKETMQ_BROKER_HEAP")
|
||||
|
||||
tune := ConfigTune{
|
||||
BrokerName: s.getPropertiesValue(config, "brokerName"),
|
||||
ListenPort: s.getPropertiesValue(config, "listenPort"),
|
||||
NamesrvAddr: s.getPropertiesValue(config, "namesrvAddr"),
|
||||
BrokerRole: s.getPropertiesValue(config, "brokerRole"),
|
||||
FlushDiskType: s.getPropertiesValue(config, "flushDiskType"),
|
||||
StorePathRootDir: s.getPropertiesValue(config, "storePathRootDir"),
|
||||
StorePathCommitLog: s.getPropertiesValue(config, "storePathCommitLog"),
|
||||
MaxMessageSize: s.getPropertiesValue(config, "maxMessageSize"),
|
||||
BrokerName: confval.Properties.Get(config, "brokerName"),
|
||||
ListenPort: confval.Properties.Get(config, "listenPort"),
|
||||
NamesrvAddr: confval.Properties.Get(config, "namesrvAddr"),
|
||||
BrokerRole: confval.Properties.Get(config, "brokerRole"),
|
||||
FlushDiskType: confval.Properties.Get(config, "flushDiskType"),
|
||||
StorePathRootDir: confval.Properties.Get(config, "storePathRootDir"),
|
||||
StorePathCommitLog: confval.Properties.Get(config, "storePathCommitLog"),
|
||||
MaxMessageSize: confval.Properties.Get(config, "maxMessageSize"),
|
||||
NamesrvHeapInitSize: namesrvInit,
|
||||
NamesrvHeapMaxSize: namesrvMax,
|
||||
BrokerHeapInitSize: brokerInit,
|
||||
@@ -133,14 +133,14 @@ func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
config, _ := io.Read(s.configPath())
|
||||
|
||||
config = s.setPropertiesValue(config, "brokerName", req.BrokerName)
|
||||
config = s.setPropertiesValue(config, "listenPort", req.ListenPort)
|
||||
config = s.setPropertiesValue(config, "namesrvAddr", req.NamesrvAddr)
|
||||
config = s.setPropertiesValue(config, "brokerRole", req.BrokerRole)
|
||||
config = s.setPropertiesValue(config, "flushDiskType", req.FlushDiskType)
|
||||
config = s.setPropertiesValue(config, "storePathRootDir", req.StorePathRootDir)
|
||||
config = s.setPropertiesValue(config, "storePathCommitLog", req.StorePathCommitLog)
|
||||
config = s.setPropertiesValue(config, "maxMessageSize", req.MaxMessageSize)
|
||||
config = confval.Properties.Set(config, "brokerName", req.BrokerName)
|
||||
config = confval.Properties.Set(config, "listenPort", req.ListenPort)
|
||||
config = confval.Properties.Set(config, "namesrvAddr", req.NamesrvAddr)
|
||||
config = confval.Properties.Set(config, "brokerRole", req.BrokerRole)
|
||||
config = confval.Properties.Set(config, "flushDiskType", req.FlushDiskType)
|
||||
config = confval.Properties.Set(config, "storePathRootDir", req.StorePathRootDir)
|
||||
config = confval.Properties.Set(config, "storePathCommitLog", req.StorePathCommitLog)
|
||||
config = confval.Properties.Set(config, "maxMessageSize", req.MaxMessageSize)
|
||||
|
||||
if err = io.Write(s.configPath(), config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
@@ -175,70 +175,12 @@ func (s *App) restartServices() error {
|
||||
|
||||
// configPath 返回 broker 配置文件路径
|
||||
func (s *App) configPath() string {
|
||||
return fmt.Sprintf("%s/server/rocketmq/conf/broker.conf", app.Root)
|
||||
return app.Root + "/server/rocketmq/conf/broker.conf"
|
||||
}
|
||||
|
||||
// heapEnvPath 返回 JVM 堆内存配置文件路径
|
||||
func (s *App) heapEnvPath() string {
|
||||
return fmt.Sprintf("%s/server/rocketmq/conf/heap.env", app.Root)
|
||||
}
|
||||
|
||||
// getPropertiesValue 从 properties 内容中获取指定键的值
|
||||
func (s *App) getPropertiesValue(content string, key string) string {
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(trimmed, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setPropertiesValue 在 properties 内容中设置指定键的值
|
||||
func (s *App) setPropertiesValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
k, _, ok := strings.Cut(checkLine, "=")
|
||||
if ok && strings.TrimSpace(k) == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "#"+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+"="+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+"="+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
return app.Root + "/server/rocketmq/conf/heap.env"
|
||||
}
|
||||
|
||||
// parseHeapLine 从 heap.env 中提取指定环境变量的堆内存配置
|
||||
|
||||
+24
-175
@@ -1,19 +1,15 @@
|
||||
package rsync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/libtnb/utils/str"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
@@ -22,11 +18,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -44,62 +40,12 @@ func (s *App) Status() string {
|
||||
}
|
||||
|
||||
func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
modules, err := listModules()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var modules []Module
|
||||
lines := strings.Split(config, "\n")
|
||||
var currentModule *Module
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
moduleName := line[1 : len(line)-1]
|
||||
currentModule = &Module{
|
||||
Name: moduleName,
|
||||
}
|
||||
} else if currentModule != nil {
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
|
||||
switch key {
|
||||
case "path":
|
||||
currentModule.Path = value
|
||||
case "comment":
|
||||
currentModule.Comment = value
|
||||
case "read only":
|
||||
currentModule.ReadOnly = value == "yes" || value == "true"
|
||||
case "auth users":
|
||||
currentModule.AuthUser = value
|
||||
currentModule.Secret, err = shell.Execf(`grep -E '^%s:.*$' /etc/rsyncd.secrets | awk -F ':' '{print $2}'`, currentModule.AuthUser)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get the secret key for module %s", currentModule.AuthUser))
|
||||
return
|
||||
}
|
||||
case "hosts allow":
|
||||
currentModule.HostsAllow = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if currentModule != nil {
|
||||
modules = append(modules, *currentModule)
|
||||
}
|
||||
|
||||
paged, total := service.Paginate(r, modules)
|
||||
|
||||
service.Success(w, chix.M{
|
||||
@@ -109,136 +55,47 @@ func (s *App) List(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *App) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Create](r)
|
||||
req, err := service.Bind[Module](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if strings.Contains(config, "["+req.Name+"]") {
|
||||
if io.Exists(modulePath(req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
conf := `# ` + req.Name + `-START
|
||||
[` + req.Name + `]
|
||||
path = ` + req.Path + `
|
||||
comment = ` + req.Comment + `
|
||||
read only = no
|
||||
auth users = ` + req.AuthUser + `
|
||||
hosts allow = ` + req.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + req.Name + `-END
|
||||
`
|
||||
|
||||
if err = io.WriteAppend("/etc/rsyncd.conf", conf, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.WriteAppend("/etc/rsyncd.secrets", fmt.Sprintf("%s:%s\n", req.AuthUser, req.Secret), 0600); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Delete](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(config, "["+req.Name+"]") {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
module := str.Cut(config, "# "+req.Name+"-START", "# "+req.Name+"-END")
|
||||
config = strings.ReplaceAll(config, "\n# "+req.Name+"-START"+module+"# "+req.Name+"-END", "")
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if _, err = shell.Execf(`sed -i '/^%s:.*$/d' /etc/rsyncd.secrets`, authUser); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
s.save(w, req)
|
||||
}
|
||||
|
||||
func (s *App) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[Update](r)
|
||||
req, err := service.Bind[Module](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(config, "["+req.Name+"]") {
|
||||
if !io.Exists(modulePath(req.Name)) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("module %s does not exist", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
newConf := `# ` + req.Name + `-START
|
||||
[` + req.Name + `]
|
||||
path = ` + req.Path + `
|
||||
comment = ` + req.Comment + `
|
||||
read only = no
|
||||
auth users = ` + req.AuthUser + `
|
||||
hosts allow = ` + req.HostsAllow + `
|
||||
secrets file = /etc/rsyncd.secrets
|
||||
# ` + req.Name + `-END`
|
||||
s.save(w, req)
|
||||
}
|
||||
|
||||
module := str.Cut(config, "# "+req.Name+"-START", "# "+req.Name+"-END")
|
||||
config = strings.ReplaceAll(config, "# "+req.Name+"-START"+module+"# "+req.Name+"-END", newConf)
|
||||
|
||||
match := regexp.MustCompile(`auth users = ([^\n]+)`).FindStringSubmatch(module)
|
||||
if len(match) == 2 {
|
||||
authUser := match[1]
|
||||
if _, err = shell.Execf(`sed -i '/^%s:.*$/d' /etc/rsyncd.secrets`, authUser); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
func (s *App) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ModuleName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", config, 0644); err != nil {
|
||||
if err = io.Remove(modulePath(req.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.WriteAppend("/etc/rsyncd.secrets", fmt.Sprintf("%s:%s\n", req.AuthUser, req.Secret), 0600); err != nil {
|
||||
if err = io.Remove(secretsPath(req.Name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -252,28 +109,20 @@ secrets file = /etc/rsyncd.secrets
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read("/etc/rsyncd.conf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, rsyncdConf)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
common.SaveConfig(w, r, rsyncdConf, "rsyncd")
|
||||
}
|
||||
|
||||
if err = io.Write("/etc/rsyncd.conf", req.Config, 0644); err != nil {
|
||||
func (s *App) save(w http.ResponseWriter, req *Module) {
|
||||
if err := writeModule(*req); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("rsyncd"); err != nil {
|
||||
if err := systemctl.Restart("rsyncd"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,27 +1,5 @@
|
||||
package rsync
|
||||
|
||||
type Create struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
|
||||
Secret string `form:"secret" json:"secret" validate:"required"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
type Delete struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
Path string `form:"path" json:"path" validate:"required"`
|
||||
Comment string `form:"comment" json:"comment"`
|
||||
AuthUser string `form:"auth_user" json:"auth_user" validate:"required"`
|
||||
Secret string `form:"secret" json:"secret" validate:"required"`
|
||||
HostsAllow string `form:"hosts_allow" json:"hosts_allow"`
|
||||
}
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
type ModuleName struct {
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package rsync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
)
|
||||
|
||||
const (
|
||||
rsyncdConf = "/etc/rsyncd.conf"
|
||||
rsyncdDir = "/etc/rsyncd.d"
|
||||
)
|
||||
|
||||
func modulePath(name string) string {
|
||||
return filepath.Join(rsyncdDir, name+".conf")
|
||||
}
|
||||
|
||||
// secretsPath 一个模块独立的密钥文件,.secrets 后缀不会被 rsyncd 当作模块读入
|
||||
func secretsPath(name string) string {
|
||||
return filepath.Join(rsyncdDir, name+".secrets")
|
||||
}
|
||||
|
||||
func readModule(name string) (Module, error) {
|
||||
config, err := io.Read(modulePath(name))
|
||||
if err != nil {
|
||||
return Module{}, err
|
||||
}
|
||||
|
||||
secret, _ := io.Read(secretsPath(name))
|
||||
_, secret, _ = strings.Cut(strings.TrimSpace(secret), ":")
|
||||
|
||||
return Module{
|
||||
Name: name,
|
||||
Path: confval.SectionINI.GetIn(config, name, "path"),
|
||||
Comment: confval.SectionINI.GetIn(config, name, "comment"),
|
||||
ReadOnly: confval.SectionINI.GetIn(config, name, "read only") == "yes",
|
||||
AuthUser: confval.SectionINI.GetIn(config, name, "auth users"),
|
||||
HostsAllow: confval.SectionINI.GetIn(config, name, "hosts allow"),
|
||||
Secret: secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func listModules() ([]Module, error) {
|
||||
// Glob 在目录不存在时返回空,无需额外判断
|
||||
paths, err := filepath.Glob(modulePath("*"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modules := make([]Module, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
module, err := readModule(strings.TrimSuffix(filepath.Base(path), ".conf"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
modules = append(modules, module)
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
func writeModule(module Module) error {
|
||||
if err := os.MkdirAll(rsyncdDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := fmt.Sprintf(
|
||||
"[%s]\npath = %s\ncomment = %s\nread only = %s\nauth users = %s\nhosts allow = %s\nsecrets file = %s\n",
|
||||
module.Name, module.Path, module.Comment, lo.Ternary(module.ReadOnly, "yes", "no"),
|
||||
module.AuthUser, module.HostsAllow, secretsPath(module.Name),
|
||||
)
|
||||
if err := io.Write(modulePath(module.Name), config, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return io.Write(secretsPath(module.Name), module.AuthUser+":"+module.Secret+"\n", 0600)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package rsync
|
||||
|
||||
type Module struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Name string `form:"name" json:"name" validate:"required && regex:\"^[a-zA-Z0-9_.-]+$\""`
|
||||
Path string `json:"path" validate:"required && unix_path"`
|
||||
Comment string `json:"comment"`
|
||||
ReadOnly bool `json:"read_only"`
|
||||
AuthUser string `json:"auth_user"`
|
||||
Secret string `json:"secret"`
|
||||
AuthUser string `json:"auth_user" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
HostsAllow string `json:"hosts_allow"`
|
||||
}
|
||||
|
||||
@@ -23,11 +23,11 @@ type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
|
||||
return &App{
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
|
||||
+92
-114
@@ -7,8 +7,11 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/chix/v2"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/common"
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/os"
|
||||
@@ -22,19 +25,11 @@ type App struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) (*App, error) {
|
||||
|
||||
var name string
|
||||
if os.IsRHEL() {
|
||||
name = "supervisord"
|
||||
} else {
|
||||
name = "supervisor"
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
name: name,
|
||||
}, nil
|
||||
name: lo.Ternary(os.IsRHEL(), "supervisord", "supervisor"),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
@@ -48,8 +43,10 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Get("/processes/{process}/log", s.ProcessLog)
|
||||
r.Get("/processes/{process}", s.ProcessConfig)
|
||||
r.Post("/processes/{process}", s.UpdateProcessConfig)
|
||||
r.Delete("/processes/{process}", s.DeleteProcess)
|
||||
r.Get("/processes/{process}/setting", s.GetProcessSetting)
|
||||
r.Post("/processes/{process}/setting", s.UpdateProcessSetting)
|
||||
r.Post("/processes", s.CreateProcess)
|
||||
r.Delete("/processes/{process}", s.DeleteProcess)
|
||||
}
|
||||
|
||||
// Service 获取服务名称
|
||||
@@ -64,47 +61,12 @@ func (s *App) Status() string {
|
||||
|
||||
// GetConfig 获取配置
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var config string
|
||||
var err error
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/supervisord.conf`)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
common.ServeConfig(w, mainConfPath())
|
||||
}
|
||||
|
||||
// UpdateConfig 保存配置
|
||||
func (s *App) 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 os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.conf`, req.Config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/supervisord.conf`, req.Config, 0644)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart(s.name); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to restart %s: %v", s.name, err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
common.SaveConfig(w, r, mainConfPath(), s.name)
|
||||
}
|
||||
|
||||
// Processes 进程列表
|
||||
@@ -206,14 +168,7 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
}
|
||||
|
||||
logPath, err := s.processLog(req.Process)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
@@ -222,7 +177,7 @@ func (s *App) ProcessLog(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, logPath)
|
||||
}
|
||||
|
||||
// ProcessConfig 获取进程配置
|
||||
// ProcessConfig 获取进程配置原文
|
||||
func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
@@ -230,14 +185,7 @@ func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
var config string
|
||||
if os.IsRHEL() {
|
||||
config, err = io.Read(`/etc/supervisord.d/` + name + `.conf`)
|
||||
} else {
|
||||
config, err = io.Read(`/etc/supervisor/conf.d/` + name + `.conf`)
|
||||
}
|
||||
|
||||
config, err := io.Read(confPath(programName(req.Process)))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
@@ -246,7 +194,7 @@ func (s *App) ProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
// UpdateProcessConfig 保存进程配置
|
||||
// UpdateProcessConfig 保存进程配置原文
|
||||
func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateProcessConfig](r)
|
||||
if err != nil {
|
||||
@@ -255,22 +203,56 @@ func (s *App) UpdateProcessConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
if os.IsRHEL() {
|
||||
err = io.Write(`/etc/supervisord.d/`+name+`.conf`, req.Config, 0644)
|
||||
} else {
|
||||
err = io.Write(`/etc/supervisor/conf.d/`+name+`.conf`, req.Config, 0644)
|
||||
if err = io.Write(confPath(name), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.reload(w, name)
|
||||
}
|
||||
|
||||
// GetProcessSetting 获取进程可视化参数
|
||||
func (s *App) GetProcessSetting(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ProcessName](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s:'`, name)
|
||||
config, err := io.Read(confPath(programName(req.Process)))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
setting := new(ProcessSetting)
|
||||
readSetting(config, setting)
|
||||
|
||||
service.Success(w, setting)
|
||||
}
|
||||
|
||||
// UpdateProcessSetting 保存进程可视化参数
|
||||
func (s *App) UpdateProcessSetting(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateProcessSetting](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
name := programName(req.Process)
|
||||
path := confPath(name)
|
||||
config, err := io.Read(path)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(path, writeSetting(config, &req.ProcessSetting), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.reload(w, name)
|
||||
}
|
||||
|
||||
// CreateProcess 添加进程
|
||||
@@ -281,37 +263,27 @@ func (s *App) CreateProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
processName := `%(program_name)s`
|
||||
if req.Num > 1 {
|
||||
processName = `%(program_name)s_%(process_num)02d`
|
||||
path := confPath(req.Name)
|
||||
if io.Exists(path) {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("process %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
num := cast.ToString(req.Num)
|
||||
config := `[program:` + req.Name + `]
|
||||
command=` + req.Command + `
|
||||
process_name=` + processName + `
|
||||
process_name=` + processNameExpr(num) + `
|
||||
directory=` + req.Path + `
|
||||
autostart=true
|
||||
autorestart=true
|
||||
user=` + req.User + `
|
||||
numprocs=` + cast.ToString(req.Num) + `
|
||||
numprocs=` + num + `
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/` + req.Name + `.log
|
||||
stdout_logfile_maxbytes=2MB
|
||||
`
|
||||
|
||||
var confPath string
|
||||
if os.IsRHEL() {
|
||||
confPath = `/etc/supervisord.d/` + req.Name + `.conf`
|
||||
} else {
|
||||
confPath = `/etc/supervisor/conf.d/` + req.Name + `.conf`
|
||||
}
|
||||
|
||||
if io.Exists(confPath) {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("process %s already exists", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
if err = io.Write(path, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
@@ -337,39 +309,45 @@ func (s *App) DeleteProcess(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var logPath string
|
||||
if os.IsRHEL() {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisord.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
if err = io.Remove(`/etc/supervisord.d/` + name + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logPath, err = shell.Execf(`cat '/etc/supervisor/conf.d/%s.conf' | grep stdout_logfile= | awk -F "=" '{print $2}'`, name)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
if err = io.Remove(`/etc/supervisor/conf.d/` + name + `.conf`); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
logPath, err := s.processLog(req.Process)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get log path for process %s: %v", req.Process, err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Remove(confPath(name)); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
if err = io.Remove(logPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *App) processLog(process string) (string, error) {
|
||||
config, err := io.Read(confPath(programName(process)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return confval.Supervisor.Get(config, "stdout_logfile"), nil
|
||||
}
|
||||
|
||||
func (s *App) reload(w http.ResponseWriter, name string) {
|
||||
_, _ = shell.Execf(`supervisorctl reread`)
|
||||
_, _ = shell.Execf(`supervisorctl update`)
|
||||
_, _ = shell.Execf(`supervisorctl restart '%s:'`, name)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// programName 从 supervisorctl 的显示名还原出程序名,program 名本身不允许含 ":",切分无歧义
|
||||
func programName(process string) string {
|
||||
name, _, _ := strings.Cut(process, ":")
|
||||
return name
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/apps/confval"
|
||||
"github.com/acepanel/panel/v3/pkg/os"
|
||||
)
|
||||
|
||||
// mainConfPath supervisor 主配置,RHEL 系与 Debian 系的路径不同
|
||||
func mainConfPath() string {
|
||||
return lo.Ternary(os.IsRHEL(), "/etc/supervisord.conf", "/etc/supervisor/supervisord.conf")
|
||||
}
|
||||
|
||||
func confPath(name string) string {
|
||||
return lo.Ternary(os.IsRHEL(), "/etc/supervisord.d/", "/etc/supervisor/conf.d/") + name + ".conf"
|
||||
}
|
||||
|
||||
type settingField struct {
|
||||
key string
|
||||
value *string
|
||||
}
|
||||
|
||||
func settingFields(s *ProcessSetting) []settingField {
|
||||
return []settingField{
|
||||
{"command", &s.Command},
|
||||
{"directory", &s.Directory},
|
||||
{"user", &s.User},
|
||||
{"numprocs", &s.NumProcs},
|
||||
{"priority", &s.Priority},
|
||||
{"autostart", &s.AutoStart},
|
||||
{"autorestart", &s.AutoRestart},
|
||||
{"startsecs", &s.StartSecs},
|
||||
{"startretries", &s.StartRetries},
|
||||
{"stopwaitsecs", &s.StopWaitSecs},
|
||||
{"stopasgroup", &s.StopAsGroup},
|
||||
{"killasgroup", &s.KillAsGroup},
|
||||
{"redirect_stderr", &s.RedirectStderr},
|
||||
{"stdout_logfile", &s.StdoutLogfile},
|
||||
{"stdout_logfile_maxbytes", &s.StdoutLogfileMaxBytes},
|
||||
{"stdout_logfile_backups", &s.StdoutLogfileBackups},
|
||||
{"environment", &s.Environment},
|
||||
}
|
||||
}
|
||||
|
||||
func readSetting(config string, setting *ProcessSetting) {
|
||||
for _, f := range settingFields(setting) {
|
||||
*f.value = confval.Supervisor.Get(config, f.key)
|
||||
}
|
||||
}
|
||||
|
||||
// writeSetting numprocs 大于 1 时 process_name 必须带上 process_num,否则 supervisor 会拒绝加载
|
||||
func writeSetting(config string, setting *ProcessSetting) string {
|
||||
for _, f := range settingFields(setting) {
|
||||
config = confval.Supervisor.Set(config, f.key, *f.value)
|
||||
}
|
||||
|
||||
return confval.Supervisor.Set(config, "process_name", processNameExpr(setting.NumProcs))
|
||||
}
|
||||
|
||||
func processNameExpr(numProcs string) string {
|
||||
return lo.Ternary(numProcs == "" || numProcs == "1", `%(program_name)s`, `%(program_name)s_%(process_num)02d`)
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
package supervisor
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateProcessConfig struct {
|
||||
Process string `form:"process" json:"process" validate:"required"`
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
@@ -13,6 +9,11 @@ type ProcessName struct {
|
||||
Process string `form:"process" json:"process" validate:"required"`
|
||||
}
|
||||
|
||||
type UpdateProcessSetting struct {
|
||||
Process string `form:"process" json:"process" validate:"required"`
|
||||
ProcessSetting
|
||||
}
|
||||
|
||||
type CreateProcess struct {
|
||||
Name string `form:"name" json:"name" validate:"required"`
|
||||
User string `form:"user" json:"user" validate:"required"`
|
||||
|
||||
@@ -6,3 +6,24 @@ type Process struct {
|
||||
Pid string `json:"pid"`
|
||||
Uptime string `json:"uptime"`
|
||||
}
|
||||
|
||||
// ProcessSetting 进程的可视化参数,留空表示注释掉该项、走 supervisor 默认值
|
||||
type ProcessSetting struct {
|
||||
Command string `form:"command" json:"command" validate:"required"`
|
||||
Directory string `form:"directory" json:"directory"`
|
||||
User string `form:"user" json:"user"`
|
||||
NumProcs string `form:"numprocs" json:"numprocs"`
|
||||
Priority string `form:"priority" json:"priority"`
|
||||
AutoStart string `form:"autostart" json:"autostart"`
|
||||
AutoRestart string `form:"autorestart" json:"autorestart"`
|
||||
StartSecs string `form:"startsecs" json:"startsecs"`
|
||||
StartRetries string `form:"startretries" json:"startretries"`
|
||||
StopWaitSecs string `form:"stopwaitsecs" json:"stopwaitsecs"`
|
||||
StopAsGroup string `form:"stopasgroup" json:"stopasgroup"`
|
||||
KillAsGroup string `form:"killasgroup" json:"killasgroup"`
|
||||
RedirectStderr string `form:"redirect_stderr" json:"redirect_stderr"`
|
||||
StdoutLogfile string `form:"stdout_logfile" json:"stdout_logfile"`
|
||||
StdoutLogfileMaxBytes string `form:"stdout_logfile_maxbytes" json:"stdout_logfile_maxbytes"`
|
||||
StdoutLogfileBackups string `form:"stdout_logfile_backups" json:"stdout_logfile_backups"`
|
||||
Environment string `form:"environment" json:"environment"`
|
||||
}
|
||||
|
||||
+7
-239
@@ -1,259 +1,27 @@
|
||||
package valkey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/shell"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
redis *redis.App
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo) (*App, error) {
|
||||
|
||||
databaseServer := databaseServerRepo
|
||||
func NewApp(t *gotext.Locale, databaseServerRepo biz.DatabaseServerRepo, taskRepo biz.TaskRepo) *App {
|
||||
return &App{
|
||||
t: t,
|
||||
databaseServerRepo: databaseServer,
|
||||
}, nil
|
||||
redis: redis.New("valkey", "Valkey", t, databaseServerRepo, taskRepo),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
s.redis.Route(r)
|
||||
}
|
||||
|
||||
func (s *App) Status() string {
|
||||
ok, _ := systemctl.Status("valkey")
|
||||
return types.AggregateAppStatus(ok)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("valkey")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get valkey status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 Valkey 密码
|
||||
withPassword := ""
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
re := regexp.MustCompile(`(?m)^requirepass\s+(.+)`)
|
||||
matches := re.FindStringSubmatch(config)
|
||||
if len(matches) == 2 {
|
||||
withPassword = " -a " + matches[1]
|
||||
}
|
||||
|
||||
raw, err := shell.Execf("valkey-cli%s info", withPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get valkey info: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
dataRaw := lo.SliceToMap(strings.Split(raw, "\n"), func(item string) (string, string) {
|
||||
parts := strings.Split(item, ":")
|
||||
if len(parts) != 2 {
|
||||
return "", ""
|
||||
}
|
||||
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
|
||||
})
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("TCP Port"), Value: dataRaw["tcp_port"]},
|
||||
{Name: s.t.Get("Uptime in Days"), Value: dataRaw["uptime_in_days"]},
|
||||
{Name: s.t.Get("Connected Clients"), Value: dataRaw["connected_clients"]},
|
||||
{Name: s.t.Get("Total Allocated Memory"), Value: dataRaw["used_memory_human"]},
|
||||
{Name: s.t.Get("Total Memory Usage"), Value: dataRaw["used_memory_rss_human"]},
|
||||
{Name: s.t.Get("Peak Memory Usage"), Value: dataRaw["used_memory_peak_human"]},
|
||||
{Name: s.t.Get("Memory Fragmentation Ratio"), Value: dataRaw["mem_fragmentation_ratio"]},
|
||||
{Name: s.t.Get("Total Connections Received"), Value: dataRaw["total_connections_received"]},
|
||||
{Name: s.t.Get("Total Commands Processed"), Value: dataRaw["total_commands_processed"]},
|
||||
{Name: s.t.Get("Commands Per Second"), Value: dataRaw["instantaneous_ops_per_sec"]},
|
||||
{Name: s.t.Get("Keyspace Hits"), Value: dataRaw["keyspace_hits"]},
|
||||
{Name: s.t.Get("Keyspace Misses"), Value: dataRaw["keyspace_misses"]},
|
||||
{Name: s.t.Get("Latest Fork Time (ms)"), Value: dataRaw["latest_fork_usec"]},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *App) 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(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("valkey"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Valkey 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root))
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
tune := ConfigTune{
|
||||
Bind: s.getValkeyValue(config, "bind"),
|
||||
Port: s.getValkeyValue(config, "port"),
|
||||
Databases: s.getValkeyValue(config, "databases"),
|
||||
Requirepass: s.getValkeyValue(config, "requirepass"),
|
||||
Timeout: s.getValkeyValue(config, "timeout"),
|
||||
TCPKeepalive: s.getValkeyValue(config, "tcp-keepalive"),
|
||||
Maxmemory: s.getValkeyValue(config, "maxmemory"),
|
||||
MaxmemoryPolicy: s.getValkeyValue(config, "maxmemory-policy"),
|
||||
Appendonly: s.getValkeyValue(config, "appendonly"),
|
||||
Appendfsync: s.getValkeyValue(config, "appendfsync"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Valkey 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/valkey/valkey.conf", app.Root)
|
||||
config, err := io.Read(confPath)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
config = s.setValkeyValue(config, "bind", req.Bind)
|
||||
config = s.setValkeyValue(config, "port", req.Port)
|
||||
config = s.setValkeyValue(config, "databases", req.Databases)
|
||||
config = s.setValkeyValue(config, "requirepass", req.Requirepass)
|
||||
config = s.setValkeyValue(config, "timeout", req.Timeout)
|
||||
config = s.setValkeyValue(config, "tcp-keepalive", req.TCPKeepalive)
|
||||
config = s.setValkeyValue(config, "maxmemory", req.Maxmemory)
|
||||
config = s.setValkeyValue(config, "maxmemory-policy", req.MaxmemoryPolicy)
|
||||
config = s.setValkeyValue(config, "appendonly", req.Appendonly)
|
||||
config = s.setValkeyValue(config, "appendfsync", req.Appendfsync)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("valkey"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 同步密码到数据库服务器记录
|
||||
_ = s.databaseServerRepo.UpdatePassword("local_valkey", req.Requirepass)
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getValkeyValue 从 Valkey 配置内容中获取指定键的值
|
||||
func (s *App) getValkeyValue(content string, key string) string {
|
||||
lines := strings.SplitSeq(content, "\n")
|
||||
for line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(trimmed)
|
||||
if len(parts) >= 2 && parts[0] == key {
|
||||
return strings.Join(parts[1:], " ")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setValkeyValue 在 Valkey 配置内容中设置指定键的值
|
||||
func (s *App) setValkeyValue(content string, key string, value string) string {
|
||||
value = strings.ReplaceAll(value, "\n", "")
|
||||
value = strings.ReplaceAll(value, "\r", "")
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
found := false
|
||||
for _, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
result = append(result, line)
|
||||
continue
|
||||
}
|
||||
checkLine := trimmed
|
||||
if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
}
|
||||
parts := strings.Fields(checkLine)
|
||||
if len(parts) >= 1 && parts[0] == key {
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if value == "" {
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
result = append(result, "# "+trimmed)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
result = append(result, key+" "+value)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
}
|
||||
if !found && value != "" {
|
||||
result = append(result, key+" "+value)
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
return s.redis.Status()
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package valkey
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Valkey 配置调整
|
||||
type ConfigTune struct {
|
||||
// 常规设置
|
||||
Bind string `form:"bind" json:"bind"`
|
||||
Port string `form:"port" json:"port" validate:"number && min:1 && max:65535"`
|
||||
Databases string `form:"databases" json:"databases" validate:"number && min:1"`
|
||||
Requirepass string `form:"requirepass" json:"requirepass"`
|
||||
Timeout string `form:"timeout" json:"timeout" validate:"number"`
|
||||
TCPKeepalive string `form:"tcp_keepalive" json:"tcp_keepalive" validate:"number"`
|
||||
// 内存
|
||||
Maxmemory string `form:"maxmemory" json:"maxmemory"`
|
||||
MaxmemoryPolicy string `form:"maxmemory_policy" json:"maxmemory_policy" validate:"in:noeviction,allkeys-lru,allkeys-lfu,volatile-lru,volatile-lfu,allkeys-random,volatile-random,volatile-ttl"`
|
||||
// 持久化
|
||||
Appendonly string `form:"appendonly" json:"appendonly" validate:"in:yes,no"`
|
||||
Appendfsync string `form:"appendfsync" json:"appendfsync" validate:"in:always,everysec,no"`
|
||||
}
|
||||
+259
-227
@@ -3,9 +3,12 @@ package biz
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"maps"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -51,14 +54,6 @@ const (
|
||||
AlertTypeWebsiteExpire = "website_expire" // 网站剩余天数,目标为网站名
|
||||
)
|
||||
|
||||
// statusAlertTypes 状态类指标,语义固定为「不在运行」,不需要运算符与阈值
|
||||
var statusAlertTypes = []string{AlertTypeService, AlertTypeProject, AlertTypeContainer, AlertTypeApp, AlertTypeDatabase}
|
||||
|
||||
// IsStatusAlert 是否为状态类指标
|
||||
func IsStatusAlert(typ string) bool {
|
||||
return slices.Contains(statusAlertTypes, typ)
|
||||
}
|
||||
|
||||
const (
|
||||
// alertRetryDelay 通知发送失败后的重试间隔
|
||||
alertRetryDelay = 5 * time.Minute
|
||||
@@ -66,6 +61,14 @@ const (
|
||||
sshFailThreshold uint = 5
|
||||
// sshFailSilence 同一来源两次爆破告警的最小间隔
|
||||
sshFailSilence = 30 * time.Minute
|
||||
// sshScanTimeout 单轮 sshd 日志扫描超时,避免 journalctl 卡死拖垮整个告警评估
|
||||
sshScanTimeout = 30 * time.Second
|
||||
// sshScanLimit 单轮最多读取的日志条数,防止爆破期间读入过多日志
|
||||
sshScanLimit = 5000
|
||||
// sshCursorPrefix journalctl --show-cursor 在输出末尾打印的游标行前缀
|
||||
sshCursorPrefix = "-- cursor: "
|
||||
// sshScanBytes 单轮最多读取的文本日志字节数,防止停机过久后一次读入过多
|
||||
sshScanBytes int64 = 8 << 20
|
||||
)
|
||||
|
||||
// 告警比较运算符
|
||||
@@ -159,17 +162,19 @@ type AlertUsecase struct {
|
||||
|
||||
mu sync.Mutex
|
||||
hits map[string]uint // 连续命中次数
|
||||
fired map[string]time.Time // 上次通知时间
|
||||
silenced map[string]time.Time // 静默截止时间,此前不重复通知
|
||||
netSnaps map[string]ioSnapshot // 网卡累计流量快照
|
||||
diskSnaps map[string]ioSnapshot // 磁盘累计 IO 快照
|
||||
healthKeys map[string]struct{} // 已通知的健康问题
|
||||
sshFired map[string]time.Time // SSH 爆破上次通知时间
|
||||
sshAt time.Time // SSH 日志上次检查时间
|
||||
dbProbe map[string]bool // 本轮数据库连通性探测结果
|
||||
cleanedAt time.Time
|
||||
sshAt time.Time // SSH 日志上次检查时间(journald 回退路径)
|
||||
sshCursor string // SSH 日志上次读取到的 journal 游标
|
||||
sshLog string // 正在跟踪的 sshd 文本日志路径
|
||||
sshOffset int64 // 文本日志上次读到的字节偏移
|
||||
cleanedAt time.Time // 上次清理历史告警的时间
|
||||
}
|
||||
|
||||
func NewAlertUsecase(notifyUsecase *NotifyUsecase, loader *apploader.Loader, t *gotext.Locale, log *slog.Logger, alertRepo AlertRepo, appRepo AppRepo, containerRepo ContainerRepo, databaseServerRepo DatabaseServerRepo, settingRepo SettingRepo) (*AlertUsecase, error) {
|
||||
func NewAlertUsecase(notifyUsecase *NotifyUsecase, loader *apploader.Loader, t *gotext.Locale, log *slog.Logger, alertRepo AlertRepo, appRepo AppRepo, containerRepo ContainerRepo, databaseServerRepo DatabaseServerRepo, settingRepo SettingRepo) *AlertUsecase {
|
||||
return &AlertUsecase{
|
||||
repo: alertRepo,
|
||||
notify: notifyUsecase,
|
||||
@@ -181,12 +186,12 @@ func NewAlertUsecase(notifyUsecase *NotifyUsecase, loader *apploader.Loader, t *
|
||||
log: log,
|
||||
t: t,
|
||||
hits: make(map[string]uint),
|
||||
fired: make(map[string]time.Time),
|
||||
silenced: make(map[string]time.Time),
|
||||
netSnaps: make(map[string]ioSnapshot),
|
||||
diskSnaps: make(map[string]ioSnapshot),
|
||||
healthKeys: make(map[string]struct{}),
|
||||
sshFired: make(map[string]time.Time),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) ListRules(page, limit uint) ([]*AlertRule, int64, error) {
|
||||
@@ -209,7 +214,7 @@ func (uc *AlertUsecase) CreateRule(ctx context.Context, req *request.AlertRuleCr
|
||||
Channels: req.Channels,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
normalizeRule(rule)
|
||||
uc.normalizeRule(rule)
|
||||
|
||||
if err := uc.repo.CreateRule(rule); err != nil {
|
||||
return nil, err
|
||||
@@ -235,7 +240,7 @@ func (uc *AlertUsecase) UpdateRule(ctx context.Context, req *request.AlertRuleUp
|
||||
rule.Silence = req.Silence
|
||||
rule.Channels = req.Channels
|
||||
rule.Enabled = req.Enabled
|
||||
normalizeRule(rule)
|
||||
uc.normalizeRule(rule)
|
||||
|
||||
if err = uc.repo.UpdateRule(rule); err != nil {
|
||||
return err
|
||||
@@ -284,55 +289,61 @@ func (uc *AlertUsecase) Evaluate(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 探测结果按轮缓存,进入新一轮先失效
|
||||
uc.mu.Lock()
|
||||
uc.dbProbe = nil
|
||||
uc.mu.Unlock()
|
||||
|
||||
uc.cleanup()
|
||||
uc.checkHealth(ctx)
|
||||
uc.checkSSH(ctx)
|
||||
|
||||
var enabled []*AlertRule
|
||||
for _, rule := range rules {
|
||||
if rule.Enabled {
|
||||
enabled = append(enabled, rule)
|
||||
}
|
||||
}
|
||||
enabled := lo.Filter(rules, func(rule *AlertRule, _ int) bool { return rule.Enabled })
|
||||
if len(enabled) == 0 {
|
||||
uc.mu.Lock()
|
||||
clear(uc.hits)
|
||||
clear(uc.fired)
|
||||
clear(uc.silenced)
|
||||
uc.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
info := tools.CurrentInfo(nil, nil)
|
||||
now := time.Now()
|
||||
uc.updateSnapshots(info, now)
|
||||
var info types.CurrentInfo
|
||||
if lo.SomeBy(enabled, func(rule *AlertRule) bool { return uc.needsSystemInfo(rule.Type) }) {
|
||||
info = tools.CurrentInfo(nil, nil)
|
||||
uc.updateSnapshots(info, now)
|
||||
}
|
||||
|
||||
// 同类型规则共用一次采集结果
|
||||
collected := make(map[string][]*AlertMetric)
|
||||
alive := make(map[string]struct{})
|
||||
for _, rule := range enabled {
|
||||
metrics, err := uc.collect(ctx, rule, info)
|
||||
if err != nil {
|
||||
uc.log.Warn("failed to collect alert metric", slog.String("rule", rule.Name), slog.Any("err", err))
|
||||
continue
|
||||
cacheKey := rule.Type
|
||||
if rule.Type == AlertTypeService {
|
||||
cacheKey += ":" + rule.Target
|
||||
}
|
||||
metrics, ok := collected[cacheKey]
|
||||
if !ok {
|
||||
var err error
|
||||
if metrics, err = uc.collect(ctx, rule, info); err != nil {
|
||||
uc.log.Warn("failed to collect alert metric", slog.String("rule", rule.Name), slog.Any("err", err))
|
||||
continue
|
||||
}
|
||||
collected[cacheKey] = metrics
|
||||
}
|
||||
|
||||
for _, metric := range metrics {
|
||||
key := stateKey(rule.ID, metric.Target)
|
||||
for _, metric := range uc.filterMetrics(rule, metrics) {
|
||||
key := uc.stateKey(rule.ID, metric.Target)
|
||||
alive[key] = struct{}{}
|
||||
uc.evaluateMetric(ctx, rule, metric, key, now)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理已消失的目标状态
|
||||
// 清理已消失的目标状态;静默记录额外要求已过期,避免某轮采集失败误清后重复告警
|
||||
uc.mu.Lock()
|
||||
for key := range uc.hits {
|
||||
if _, ok := alive[key]; !ok {
|
||||
delete(uc.hits, key)
|
||||
}
|
||||
}
|
||||
maps.DeleteFunc(uc.hits, func(key string, _ uint) bool {
|
||||
_, ok := alive[key]
|
||||
return !ok
|
||||
})
|
||||
maps.DeleteFunc(uc.silenced, func(key string, until time.Time) bool {
|
||||
_, ok := alive[key]
|
||||
return !ok && now.After(until)
|
||||
})
|
||||
uc.mu.Unlock()
|
||||
|
||||
return nil
|
||||
@@ -340,26 +351,25 @@ func (uc *AlertUsecase) Evaluate(ctx context.Context) error {
|
||||
|
||||
func (uc *AlertUsecase) evaluateMetric(ctx context.Context, rule *AlertRule, metric *AlertMetric, key string, now time.Time) {
|
||||
uc.mu.Lock()
|
||||
if !matchThreshold(rule, metric.Value) {
|
||||
if !uc.matchThreshold(rule, metric.Value) {
|
||||
delete(uc.hits, key)
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
uc.hits[key]++
|
||||
hits := uc.hits[key]
|
||||
duration := max(rule.Duration, 1)
|
||||
if hits < duration {
|
||||
if uc.hits[key] < max(rule.Duration, 1) {
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 静默期内不重复告警
|
||||
if last, ok := uc.fired[key]; ok && now.Sub(last) < time.Duration(rule.Silence)*time.Minute {
|
||||
if now.Before(uc.silenced[key]) {
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
uc.fired[key] = now
|
||||
silence := time.Duration(rule.Silence) * time.Minute
|
||||
uc.silenced[key] = now.Add(silence)
|
||||
uc.mu.Unlock()
|
||||
|
||||
alert := &Alert{
|
||||
@@ -386,16 +396,16 @@ func (uc *AlertUsecase) evaluateMetric(ctx context.Context, rule *AlertRule, met
|
||||
// 一条都没送达时缩短静默期,让下一轮重试,避免临时故障吞掉整个静默窗口
|
||||
if len(rule.Channels) > 0 && sent == 0 {
|
||||
uc.mu.Lock()
|
||||
uc.fired[key] = now.Add(alertRetryDelay - time.Duration(rule.Silence)*time.Minute)
|
||||
uc.silenced[key] = now.Add(min(alertRetryDelay, silence))
|
||||
uc.mu.Unlock()
|
||||
}
|
||||
|
||||
if err := uc.repo.AddAlert(alert); err != nil {
|
||||
if err = uc.repo.AddAlert(alert); err != nil {
|
||||
uc.log.Warn("failed to save alert record", slog.String("rule", rule.Name), slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
|
||||
// collect 采集规则对应的目标取值
|
||||
// collect 采集规则类型对应的全部目标取值,不做目标过滤
|
||||
func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types.CurrentInfo) ([]*AlertMetric, error) {
|
||||
switch rule.Type {
|
||||
case AlertTypeCPU:
|
||||
@@ -417,21 +427,18 @@ func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types
|
||||
if info.Load == nil {
|
||||
return nil, nil
|
||||
}
|
||||
value := info.Load.Load1
|
||||
switch rule.Type {
|
||||
case AlertTypeLoad1:
|
||||
return []*AlertMetric{{Value: info.Load.Load1}}, nil
|
||||
case AlertTypeLoad5:
|
||||
return []*AlertMetric{{Value: info.Load.Load5}}, nil
|
||||
default:
|
||||
return []*AlertMetric{{Value: info.Load.Load15}}, nil
|
||||
value = info.Load.Load5
|
||||
case AlertTypeLoad15:
|
||||
value = info.Load.Load15
|
||||
}
|
||||
return []*AlertMetric{{Value: value}}, nil
|
||||
|
||||
case AlertTypeDisk, AlertTypeDiskInode:
|
||||
metrics := make([]*AlertMetric, 0)
|
||||
metrics := make([]*AlertMetric, 0, len(info.DiskUsage))
|
||||
for _, usage := range info.DiskUsage {
|
||||
if rule.Target != "" && rule.Target != usage.Path {
|
||||
continue
|
||||
}
|
||||
value := usage.UsedPercent
|
||||
if rule.Type == AlertTypeDiskInode {
|
||||
value = usage.InodesUsedPercent
|
||||
@@ -441,7 +448,7 @@ func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types
|
||||
return metrics, nil
|
||||
|
||||
case AlertTypeNetIn, AlertTypeNetOut, AlertTypeDiskRead, AlertTypeDiskWrite:
|
||||
return uc.rateMetrics(rule), nil
|
||||
return uc.rateMetrics(rule.Type), nil
|
||||
|
||||
case AlertTypeWebsite5xx, AlertTypeWebsiteError:
|
||||
stats, err := uc.repo.WebsiteHourStats()
|
||||
@@ -450,9 +457,6 @@ func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types
|
||||
}
|
||||
metrics := make([]*AlertMetric, 0, len(stats))
|
||||
for _, item := range stats {
|
||||
if rule.Target != "" && rule.Target != item.Site {
|
||||
continue
|
||||
}
|
||||
if rule.Type == AlertTypeWebsite5xx {
|
||||
metrics = append(metrics, &AlertMetric{Target: item.Site, Value: float64(item.Status5xx)})
|
||||
continue
|
||||
@@ -469,16 +473,18 @@ func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types
|
||||
if rule.Target == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return []*AlertMetric{{Target: rule.Target, Value: notRunning(systemctl.Status(rule.Target))}}, nil
|
||||
running, _ := systemctl.Status(rule.Target)
|
||||
return []*AlertMetric{{Target: rule.Target, Value: uc.statusValue(running)}}, nil
|
||||
|
||||
case AlertTypeProject:
|
||||
names, err := uc.repo.ProjectNames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 项目即 systemd 单元,单元名与项目名一致
|
||||
return lop.Map(filterNames(rule.Target, names), func(name string, _ int) *AlertMetric {
|
||||
return &AlertMetric{Target: name, Value: notRunning(systemctl.Status(name))}
|
||||
// 项目即 systemd 单元,单元名与项目名一致,状态并发查询
|
||||
return lop.Map(names, func(name string, _ int) *AlertMetric {
|
||||
running, _ := systemctl.Status(name)
|
||||
return &AlertMetric{Target: name, Value: uc.statusValue(running)}
|
||||
}), nil
|
||||
|
||||
case AlertTypeContainer:
|
||||
@@ -488,14 +494,7 @@ func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types
|
||||
}
|
||||
metrics := make([]*AlertMetric, 0, len(containers))
|
||||
for _, item := range containers {
|
||||
if rule.Target != "" && rule.Target != item.Name {
|
||||
continue
|
||||
}
|
||||
value := float64(0)
|
||||
if item.State != "running" {
|
||||
value = 1
|
||||
}
|
||||
metrics = append(metrics, &AlertMetric{Target: item.Name, Value: value})
|
||||
metrics = append(metrics, &AlertMetric{Target: item.Name, Value: uc.statusValue(item.State == "running")})
|
||||
}
|
||||
return metrics, nil
|
||||
|
||||
@@ -505,9 +504,6 @@ func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types
|
||||
return nil, err
|
||||
}
|
||||
targets := lo.Filter(installed, func(item *App, _ int) bool {
|
||||
if rule.Target != "" && rule.Target != item.Slug {
|
||||
return false
|
||||
}
|
||||
_, ok := uc.loader.Get(item.Slug)
|
||||
return ok
|
||||
})
|
||||
@@ -519,11 +515,7 @@ func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types
|
||||
if status == types.AppStatusNA {
|
||||
return nil
|
||||
}
|
||||
value := float64(0)
|
||||
if status != types.AppStatusRunning {
|
||||
value = 1
|
||||
}
|
||||
return &AlertMetric{Target: item.Slug, Value: value}
|
||||
return &AlertMetric{Target: item.Slug, Value: uc.statusValue(status == types.AppStatusRunning)}
|
||||
})), nil
|
||||
|
||||
case AlertTypeDatabase:
|
||||
@@ -531,115 +523,50 @@ func (uc *AlertUsecase) collect(ctx context.Context, rule *AlertRule, info types
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reachable := uc.probeDatabases(ctx, servers)
|
||||
|
||||
metrics := make([]*AlertMetric, 0, len(servers))
|
||||
for _, item := range servers {
|
||||
if rule.Target != "" && rule.Target != item.Name {
|
||||
continue
|
||||
}
|
||||
value := float64(0)
|
||||
if !reachable[item.Name] {
|
||||
value = 1
|
||||
}
|
||||
metrics = append(metrics, &AlertMetric{Target: item.Name, Value: value})
|
||||
}
|
||||
return metrics, nil
|
||||
// 并发探测连通性,单台耗时上限由 pkg/db 各驱动的连接超时保证(5~10 秒)
|
||||
return lop.Map(servers, func(item *DatabaseServer, _ int) *AlertMetric {
|
||||
return &AlertMetric{Target: item.Name, Value: uc.statusValue(uc.database.CheckServer(ctx, item))}
|
||||
}), nil
|
||||
|
||||
case AlertTypeCertExpire:
|
||||
metrics, err := uc.repo.CertExpiry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filterTarget(rule.Target, metrics), nil
|
||||
return uc.repo.CertExpiry()
|
||||
|
||||
case AlertTypeWebsiteExpire:
|
||||
metrics, err := uc.repo.WebsiteExpiry()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filterTarget(rule.Target, metrics), nil
|
||||
return uc.repo.WebsiteExpiry()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported alert type: %s", rule.Type)
|
||||
}
|
||||
|
||||
// notRunning 将运行状态转为告警取值,未运行记为 1,配合 normalizeRule 的 >=1 判定
|
||||
func notRunning(running bool, _ error) float64 {
|
||||
if running {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// filterNames 按目标名筛选,目标为空表示全部
|
||||
func filterNames(target string, names []string) []string {
|
||||
if target == "" {
|
||||
return names
|
||||
}
|
||||
if slices.Contains(names, target) {
|
||||
return []string{target}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterTarget 按目标名筛选,目标为空表示全部
|
||||
// 证书目标是逗号分隔的多域名,允许用其中任一域名命中
|
||||
func filterTarget(target string, metrics []*AlertMetric) []*AlertMetric {
|
||||
if target == "" {
|
||||
// filterMetrics 按规则目标筛选采集结果,目标为空表示全部
|
||||
func (uc *AlertUsecase) filterMetrics(rule *AlertRule, metrics []*AlertMetric) []*AlertMetric {
|
||||
if rule.Target == "" {
|
||||
return metrics
|
||||
}
|
||||
|
||||
filtered := make([]*AlertMetric, 0)
|
||||
for _, metric := range metrics {
|
||||
if metric.Target == target || slices.Contains(strings.Split(metric.Target, ","), target) {
|
||||
filtered = append(filtered, metric)
|
||||
return lo.Filter(metrics, func(metric *AlertMetric, _ int) bool {
|
||||
if metric.Target == rule.Target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
// probeDatabases 并发探测数据库连通性,结果在本轮评估内复用,避免多条规则重复整批探测
|
||||
// 单台耗时上限由 pkg/db 各驱动的连接超时保证(5~10 秒),并发后整批不会拖长分钟级评估
|
||||
func (uc *AlertUsecase) probeDatabases(ctx context.Context, servers []*DatabaseServer) map[string]bool {
|
||||
uc.mu.Lock()
|
||||
cached := uc.dbProbe
|
||||
uc.mu.Unlock()
|
||||
if cached != nil {
|
||||
return cached
|
||||
}
|
||||
|
||||
probe := lo.FromEntries(lop.Map(servers, func(item *DatabaseServer, _ int) lo.Entry[string, bool] {
|
||||
return lo.Entry[string, bool]{Key: item.Name, Value: uc.database.CheckServer(ctx, item)}
|
||||
}))
|
||||
|
||||
uc.mu.Lock()
|
||||
uc.dbProbe = probe
|
||||
uc.mu.Unlock()
|
||||
|
||||
return probe
|
||||
// 证书目标是逗号分隔的多域名,允许用其中任一域名命中
|
||||
return rule.Type == AlertTypeCertExpire && slices.Contains(strings.Split(metric.Target, ","), rule.Target)
|
||||
})
|
||||
}
|
||||
|
||||
// rateMetrics 从快照读取速率指标(MB/s)
|
||||
func (uc *AlertUsecase) rateMetrics(rule *AlertRule) []*AlertMetric {
|
||||
func (uc *AlertUsecase) rateMetrics(typ string) []*AlertMetric {
|
||||
uc.mu.Lock()
|
||||
defer uc.mu.Unlock()
|
||||
|
||||
snaps := uc.netSnaps
|
||||
if rule.Type == AlertTypeDiskRead || rule.Type == AlertTypeDiskWrite {
|
||||
if typ == AlertTypeDiskRead || typ == AlertTypeDiskWrite {
|
||||
snaps = uc.diskSnaps
|
||||
}
|
||||
|
||||
metrics := make([]*AlertMetric, 0, len(snaps))
|
||||
for name, snap := range snaps {
|
||||
if rule.Target != "" && rule.Target != name {
|
||||
continue
|
||||
}
|
||||
value := snap.inRate
|
||||
if rule.Type == AlertTypeNetOut || rule.Type == AlertTypeDiskWrite {
|
||||
if typ == AlertTypeNetOut || typ == AlertTypeDiskWrite {
|
||||
value = snap.outRate
|
||||
}
|
||||
metrics = append(metrics, &AlertMetric{Target: name, Value: float64(value) / 1024 / 1024})
|
||||
@@ -658,20 +585,45 @@ func (uc *AlertUsecase) updateSnapshots(info types.CurrentInfo, now time.Time) {
|
||||
if item.Name == "lo" {
|
||||
continue
|
||||
}
|
||||
nets[item.Name] = rateOf(uc.netSnaps[item.Name], item.BytesRecv, item.BytesSent, now)
|
||||
nets[item.Name] = uc.rateOf(uc.netSnaps[item.Name], item.BytesRecv, item.BytesSent, now)
|
||||
}
|
||||
uc.netSnaps = nets
|
||||
|
||||
disks := make(map[string]ioSnapshot, len(info.DiskIO))
|
||||
for _, item := range info.DiskIO {
|
||||
disks[item.Name] = rateOf(uc.diskSnaps[item.Name], item.ReadBytes, item.WriteBytes, now)
|
||||
disks[item.Name] = uc.rateOf(uc.diskSnaps[item.Name], item.ReadBytes, item.WriteBytes, now)
|
||||
}
|
||||
uc.diskSnaps = disks
|
||||
}
|
||||
|
||||
// rateOf 依据上次累计值计算每秒增量,首次采集速率为 0
|
||||
func (uc *AlertUsecase) rateOf(prev ioSnapshot, in, out uint64, now time.Time) ioSnapshot {
|
||||
snap := ioSnapshot{in: in, out: out, at: now}
|
||||
if prev.at.IsZero() {
|
||||
return snap
|
||||
}
|
||||
|
||||
elapsed := now.Sub(prev.at).Seconds()
|
||||
if elapsed < 1 {
|
||||
elapsed = 1
|
||||
}
|
||||
if in >= prev.in {
|
||||
snap.inRate = uint64(float64(in-prev.in) / elapsed)
|
||||
}
|
||||
if out >= prev.out {
|
||||
snap.outRate = uint64(float64(out-prev.out) / elapsed)
|
||||
}
|
||||
|
||||
return snap
|
||||
}
|
||||
|
||||
// checkHealth 上报新出现的面板健康问题,问题恢复后重新出现会再次通知
|
||||
// 同步发送并只在送达后记入去重集合,否则一次发送失败就会让持续存在的问题再也不告警
|
||||
func (uc *AlertUsecase) checkHealth(ctx context.Context) {
|
||||
if !uc.notify.EventEnabled(NotifyEventHealth) {
|
||||
return
|
||||
}
|
||||
|
||||
issues := app.Health.Snapshot()
|
||||
|
||||
uc.mu.Lock()
|
||||
@@ -707,27 +659,14 @@ func (uc *AlertUsecase) checkHealth(ctx context.Context) {
|
||||
|
||||
// checkSSH 增量检查 sshd 日志,上报登录成功与爆破尝试
|
||||
func (uc *AlertUsecase) checkSSH(ctx context.Context) {
|
||||
// 没人接收就不必读
|
||||
if !uc.notify.EventEnabled(NotifyEventSSHLogin, NotifyEventSSHBruteforce) {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
|
||||
uc.mu.Lock()
|
||||
since := uc.sshAt
|
||||
uc.sshAt = now
|
||||
uc.mu.Unlock()
|
||||
|
||||
// 首次仅记录时间,避免面板启动时把历史日志全推一遍
|
||||
if since.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := shell.ExecfWithContext(ctx, `journalctl -u sshd -u ssh --no-pager -o json --since "@%d" 2>/dev/null`, since.Unix())
|
||||
if err != nil {
|
||||
// 读取失败则回退检查点,下一轮重扫该窗口,避免丢掉这段时间的登录记录
|
||||
uc.mu.Lock()
|
||||
uc.sshAt = since
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
if raw == "" {
|
||||
raw, err := uc.readSSHLog(ctx)
|
||||
if err != nil || raw == "" {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -735,14 +674,17 @@ func (uc *AlertUsecase) checkSSH(ctx context.Context) {
|
||||
scanner := bufio.NewScanner(strings.NewReader(raw))
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
var entry struct {
|
||||
Message string `json:"MESSAGE"`
|
||||
}
|
||||
if json.Unmarshal(scanner.Bytes(), &entry) != nil {
|
||||
line := scanner.Text()
|
||||
|
||||
// 游标行由 journalctl 的 --show-cursor 在末尾单独打印
|
||||
if next, ok := strings.CutPrefix(line, sshCursorPrefix); ok {
|
||||
uc.mu.Lock()
|
||||
uc.sshCursor = next
|
||||
uc.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
record := sshlog.ParseMessage(entry.Message)
|
||||
record := sshlog.ParseMessage(line)
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
@@ -772,6 +714,87 @@ func (uc *AlertUsecase) checkSSH(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// readSSHLog 增量读取 sshd 日志
|
||||
// 优先读文本日志:journald 按单位过滤要沿匹配链遍历,日志量大时取最后一条都要几十秒
|
||||
func (uc *AlertUsecase) readSSHLog(ctx context.Context) (string, error) {
|
||||
for _, path := range []string{"/var/log/auth.log", "/var/log/secure"} {
|
||||
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
||||
return uc.readSSHFile(path, info.Size())
|
||||
}
|
||||
}
|
||||
|
||||
return uc.readSSHJournal(ctx)
|
||||
}
|
||||
|
||||
// readSSHFile 按字节偏移读取文本日志新增的部分
|
||||
func (uc *AlertUsecase) readSSHFile(path string, size int64) (string, error) {
|
||||
uc.mu.Lock()
|
||||
offset, known := uc.sshOffset, uc.sshLog == path
|
||||
uc.sshLog, uc.sshOffset = path, size
|
||||
uc.mu.Unlock()
|
||||
|
||||
// 首轮只记录位置,避免面板启动时把历史日志全推一遍
|
||||
if !known {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case offset > size:
|
||||
offset = 0 // 日志已轮转,从新文件开头读
|
||||
case size-offset > sshScanBytes:
|
||||
offset = size - sshScanBytes // 停机过久时只补最近一段,首行可能被截断解析不出
|
||||
case offset == size:
|
||||
return "", nil
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
buffer := make([]byte, size-offset)
|
||||
n, err := file.ReadAt(buffer, offset)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(buffer[:n]), nil
|
||||
}
|
||||
|
||||
// readSSHJournal 无文本日志时回退到 journald,按游标增量读取
|
||||
func (uc *AlertUsecase) readSSHJournal(ctx context.Context) (string, error) {
|
||||
now := time.Now()
|
||||
|
||||
uc.mu.Lock()
|
||||
since, cursor := uc.sshAt, uc.sshCursor
|
||||
uc.sshAt = now
|
||||
uc.mu.Unlock()
|
||||
|
||||
limit, position := sshScanLimit, fmt.Sprintf("--after-cursor %q", cursor)
|
||||
switch {
|
||||
case since.IsZero():
|
||||
limit, position = 0, ""
|
||||
case cursor == "":
|
||||
position = fmt.Sprintf(`--since "@%d"`, since.Unix())
|
||||
}
|
||||
|
||||
// 只用到 MESSAGE,-o cat 免去 journald 侧序列化几十个无关字段和这边的逐行反序列化
|
||||
scanCtx, cancel := context.WithTimeout(ctx, sshScanTimeout)
|
||||
defer cancel()
|
||||
raw, err := shell.ExecfWithContext(scanCtx, `journalctl -u sshd -u ssh --no-pager -q -o cat --show-cursor -n %d %s 2>/dev/null`, limit, position)
|
||||
if err != nil {
|
||||
// 读取失败(含游标因日志轮转失效)则回退检查点并丢弃游标,下一轮按时间窗口重扫
|
||||
uc.mu.Lock()
|
||||
uc.sshAt = since
|
||||
uc.sshCursor = ""
|
||||
uc.mu.Unlock()
|
||||
return "", err
|
||||
}
|
||||
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// sshShouldFire 判断某来源是否已过静默期,并顺带清理过期记录
|
||||
func (uc *AlertUsecase) sshShouldFire(ip string, now time.Time) bool {
|
||||
uc.mu.Lock()
|
||||
@@ -818,16 +841,14 @@ func (uc *AlertUsecase) clearState(ruleID uint) {
|
||||
delete(uc.hits, key)
|
||||
}
|
||||
}
|
||||
for key := range uc.fired {
|
||||
for key := range uc.silenced {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
delete(uc.fired, key)
|
||||
delete(uc.silenced, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) buildMessage(rule *AlertRule, metric *AlertMetric) string {
|
||||
label := uc.metricLabel(rule.Type, metric.Target)
|
||||
|
||||
switch rule.Type {
|
||||
case AlertTypeService:
|
||||
return uc.t.Get("service %s is not running", metric.Target)
|
||||
@@ -845,7 +866,7 @@ func (uc *AlertUsecase) buildMessage(rule *AlertRule, metric *AlertMetric) strin
|
||||
return uc.t.Get("website %s expires in %s days", metric.Target, uc.formatValue(rule.Type, metric.Value))
|
||||
}
|
||||
|
||||
return uc.t.Get("%s is %s, %s threshold %s", label, uc.formatValue(rule.Type, metric.Value), uc.operatorLabel(rule.Operator), uc.formatValue(rule.Type, rule.Threshold))
|
||||
return uc.t.Get("%s is %s, %s threshold %s", uc.metricLabel(rule.Type, metric.Target), uc.formatValue(rule.Type, metric.Value), uc.operatorLabel(rule.Operator), uc.formatValue(rule.Type, rule.Threshold))
|
||||
}
|
||||
|
||||
func (uc *AlertUsecase) metricLabel(typ, target string) string {
|
||||
@@ -927,16 +948,48 @@ func (uc *AlertUsecase) formatValue(typ string, value float64) string {
|
||||
return fmt.Sprintf("%.0f", value)
|
||||
}
|
||||
|
||||
if IsStatusAlert(typ) {
|
||||
if uc.isStatusType(typ) {
|
||||
return uc.t.Get("not running")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.2f", value)
|
||||
}
|
||||
|
||||
// needsSystemInfo 该规则类型是否依赖 CurrentInfo 采集的系统指标
|
||||
func (uc *AlertUsecase) needsSystemInfo(typ string) bool {
|
||||
switch typ {
|
||||
case AlertTypeCPU, AlertTypeMemory, AlertTypeSwap,
|
||||
AlertTypeLoad1, AlertTypeLoad5, AlertTypeLoad15,
|
||||
AlertTypeDisk, AlertTypeDiskInode,
|
||||
AlertTypeNetIn, AlertTypeNetOut, AlertTypeDiskRead, AlertTypeDiskWrite:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isStatusType 状态类指标只有「运行/未运行」两种取值
|
||||
func (uc *AlertUsecase) isStatusType(typ string) bool {
|
||||
switch typ {
|
||||
case AlertTypeService, AlertTypeProject, AlertTypeContainer, AlertTypeApp, AlertTypeDatabase:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// statusValue 将运行状态转为告警取值,未运行记为 1,配合 normalizeRule 的 >=1 判定
|
||||
func (uc *AlertUsecase) statusValue(running bool) float64 {
|
||||
if running {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
// normalizeRule 补齐规则的默认值,状态类规则固定为「不在运行」
|
||||
func normalizeRule(rule *AlertRule) {
|
||||
if IsStatusAlert(rule.Type) {
|
||||
func (uc *AlertUsecase) normalizeRule(rule *AlertRule) {
|
||||
if uc.isStatusType(rule.Type) {
|
||||
rule.Operator = AlertOperatorGTE
|
||||
rule.Threshold = 1
|
||||
}
|
||||
@@ -945,7 +998,7 @@ func normalizeRule(rule *AlertRule) {
|
||||
}
|
||||
}
|
||||
|
||||
func matchThreshold(rule *AlertRule, value float64) bool {
|
||||
func (uc *AlertUsecase) matchThreshold(rule *AlertRule, value float64) bool {
|
||||
switch rule.Operator {
|
||||
case AlertOperatorGTE:
|
||||
return value >= rule.Threshold
|
||||
@@ -958,27 +1011,6 @@ func matchThreshold(rule *AlertRule, value float64) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// rateOf 依据上次累计值计算每秒增量,首次采集速率为 0
|
||||
func rateOf(prev ioSnapshot, in, out uint64, now time.Time) ioSnapshot {
|
||||
snap := ioSnapshot{in: in, out: out, at: now}
|
||||
if prev.at.IsZero() {
|
||||
return snap
|
||||
}
|
||||
|
||||
elapsed := now.Sub(prev.at).Seconds()
|
||||
if elapsed < 1 {
|
||||
elapsed = 1
|
||||
}
|
||||
if in >= prev.in {
|
||||
snap.inRate = uint64(float64(in-prev.in) / elapsed)
|
||||
}
|
||||
if out >= prev.out {
|
||||
snap.outRate = uint64(float64(out-prev.out) / elapsed)
|
||||
}
|
||||
|
||||
return snap
|
||||
}
|
||||
|
||||
func stateKey(ruleID uint, target string) string {
|
||||
func (uc *AlertUsecase) stateKey(ruleID uint, target string) string {
|
||||
return fmt.Sprintf("%d:%s", ruleID, target)
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package biz
|
||||
|
||||
import "testing"
|
||||
|
||||
// 多域名证书的目标是逗号分隔的域名列表,用其中任一域名都应能命中规则
|
||||
func TestFilterTargetMatchesAnyCertDomain(t *testing.T) {
|
||||
metrics := []*AlertMetric{
|
||||
{Target: "a.example.com,b.example.com", Value: 5},
|
||||
{Target: "c.example.com", Value: 30},
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
target string
|
||||
want int
|
||||
}{
|
||||
{"", 2},
|
||||
{"a.example.com", 1},
|
||||
{"b.example.com", 1},
|
||||
{"c.example.com", 1},
|
||||
{"a.example.com,b.example.com", 1},
|
||||
{"missing.example.com", 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
if got := filterTarget(c.target, metrics); len(got) != c.want {
|
||||
t.Fatalf("target %q matched %d metrics, want %d", c.target, len(got), c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -71,13 +71,13 @@ type AppUsecase struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewAppUsecase(t *gotext.Locale, appRepo AppRepo, cacheRepo CacheRepo, taskRepo TaskRepo) (*AppUsecase, error) {
|
||||
func NewAppUsecase(t *gotext.Locale, appRepo AppRepo, cacheRepo CacheRepo, taskRepo TaskRepo) *AppUsecase {
|
||||
return &AppUsecase{
|
||||
repo: appRepo,
|
||||
cache: cacheRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *AppUsecase) Categories() []types.LV {
|
||||
@@ -160,10 +160,13 @@ func (uc *AppUsecase) GetHomeShow() ([]map[string]string, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 目录只加载一次
|
||||
catalog := lo.KeyBy(uc.All(), func(item *api.App) string { return item.Slug })
|
||||
|
||||
filtered := make([]map[string]string, 0)
|
||||
for item := range slices.Values(apps) {
|
||||
loaded, err := uc.Get(item.Slug)
|
||||
if err != nil {
|
||||
loaded, ok := catalog[item.Slug]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, map[string]string{
|
||||
|
||||
@@ -31,7 +31,7 @@ type BackupRepo interface {
|
||||
Delete(typ BackupType, name string) error
|
||||
Restore(typ BackupType, backup, target string) error
|
||||
ClearExpired(path, prefix string, save uint) error
|
||||
ClearStorageExpired(account uint, typ BackupType, prefix string, save uint) error
|
||||
ClearStorageExpired(account uint, dir, prefix string, save uint) error
|
||||
CutoffLog(path, target string) (string, error)
|
||||
CutoffUpload(account uint, typ BackupType, name string, files []string) error
|
||||
GetDefaultPath(typ BackupType) string
|
||||
@@ -46,13 +46,13 @@ type BackupUsecase struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewBackupUsecase(notifyUsecase *NotifyUsecase, t *gotext.Locale, log *slog.Logger, backupRepo BackupRepo) (*BackupUsecase, error) {
|
||||
func NewBackupUsecase(notifyUsecase *NotifyUsecase, t *gotext.Locale, log *slog.Logger, backupRepo BackupRepo) *BackupUsecase {
|
||||
return &BackupUsecase{
|
||||
repo: backupRepo,
|
||||
log: log,
|
||||
notify: notifyUsecase,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) List(typ BackupType) ([]*types.BackupFile, error) {
|
||||
@@ -113,8 +113,8 @@ func (uc *BackupUsecase) ClearExpired(path, prefix string, save uint) error {
|
||||
return uc.repo.ClearExpired(path, prefix, save)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) ClearStorageExpired(account uint, typ BackupType, prefix string, save uint) error {
|
||||
return uc.repo.ClearStorageExpired(account, typ, prefix, save)
|
||||
func (uc *BackupUsecase) ClearStorageExpired(account uint, dir, prefix string, save uint) error {
|
||||
return uc.repo.ClearStorageExpired(account, dir, prefix, save)
|
||||
}
|
||||
|
||||
func (uc *BackupUsecase) CutoffLog(path, target string) (string, error) {
|
||||
|
||||
@@ -145,13 +145,13 @@ type BackupAccountUsecase struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewBackupAccountUsecase(t *gotext.Locale, log *slog.Logger, backupAccountRepo BackupAccountRepo, settingRepo SettingRepo) (*BackupAccountUsecase, error) {
|
||||
func NewBackupAccountUsecase(t *gotext.Locale, log *slog.Logger, backupAccountRepo BackupAccountRepo, settingRepo SettingRepo) *BackupAccountUsecase {
|
||||
return &BackupAccountUsecase{
|
||||
repo: backupAccountRepo,
|
||||
setting: settingRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *BackupAccountUsecase) List(page, limit uint) ([]*BackupStorage, int64, error) {
|
||||
|
||||
+1
-1
@@ -14,5 +14,5 @@ var ProviderSet = wire.NewSet(
|
||||
NewSettingUsecase, NewSSHUsecase, NewTamperUsecase, NewTaskUsecase,
|
||||
NewTemplateUsecase, NewUserUsecase, NewUserPasskeyUsecase,
|
||||
NewUserTokenUsecase, NewWebHookUsecase, NewWebsiteUsecase,
|
||||
NewWebsiteStatUsecase, NewWafUsecase,
|
||||
NewWebsiteStatUsecase, NewToolboxMigrationUsecase,
|
||||
)
|
||||
|
||||
+78
-48
@@ -3,7 +3,6 @@ package biz
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
mholtacme "github.com/mholt/acmez/v3/acme"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
type Cert struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
AccountID uint `gorm:"not null;default:0" json:"account_id"` // 关联的 ACME 账户 ID
|
||||
WebsiteID uint `gorm:"not null;default:0" json:"website_id"` // 关联的网站 ID
|
||||
DNSID uint `gorm:"not null;default:0" json:"dns_id"` // 关联的 DNS ID
|
||||
Type string `gorm:"not null;default:''" json:"type"` // 证书类型 (P256, P384, 2048, 3072, 4096)
|
||||
Domains []string `gorm:"not null;default:'[]';serializer:json" json:"domains"` // 域名
|
||||
@@ -37,9 +36,16 @@ type Cert struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Website *Website `gorm:"foreignKey:WebsiteID" json:"website"`
|
||||
Account *CertAccount `gorm:"foreignKey:AccountID" json:"account"`
|
||||
DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"`
|
||||
Websites []*Website `gorm:"foreignKey:CertID" json:"websites"` // 部署的网站
|
||||
Account *CertAccount `gorm:"foreignKey:AccountID" json:"account"`
|
||||
DNS *CertDNS `gorm:"foreignKey:DNSID" json:"dns"`
|
||||
}
|
||||
|
||||
// WebsiteIDs 取证书关联的网站 ID 列表
|
||||
func (r *Cert) WebsiteIDs() []uint {
|
||||
return lo.Map(r.Websites, func(website *Website, _ int) uint {
|
||||
return website.ID
|
||||
})
|
||||
}
|
||||
|
||||
type CertRepo interface {
|
||||
@@ -54,10 +60,12 @@ type CertRepo interface {
|
||||
GetClient(cert *Cert) (*acme.Client, error)
|
||||
GenerateSelfSigned(domains []string) ([]byte, []byte, error)
|
||||
RunScript(cert *Cert) error
|
||||
ObtainPanel(account *CertAccount, ips []string, webServer string) ([]byte, []byte, error)
|
||||
LoadWebsite(WebsiteID uint) (*Website, error)
|
||||
ObtainPanel(account *CertAccount, names []string, webServer string) ([]byte, []byte, error)
|
||||
LoadWebsites(websiteIDs []uint) ([]*Website, error)
|
||||
BindWebsites(certID uint, websiteIDs []uint) error
|
||||
HTTPConfs(cert *Cert, webServer string) (map[string]string, []string)
|
||||
WriteCertFiles(cert *Cert, certPath, keyPath string) error
|
||||
EnableWebsiteSSL(website *Website, certPath, keyPath, webServer string, tlsVersions []string) error
|
||||
EnableWebsiteSSL(website *Website, certPath, keyPath, webServer string, tlsVersions []string, listenIPv6 bool) error
|
||||
ReloadWebserver(webServer string) error
|
||||
}
|
||||
|
||||
@@ -68,13 +76,13 @@ type CertUsecase struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewCertUsecase(t *gotext.Locale, log *slog.Logger, certRepo CertRepo, settingRepo SettingRepo) (*CertUsecase, error) {
|
||||
func NewCertUsecase(t *gotext.Locale, log *slog.Logger, certRepo CertRepo, settingRepo SettingRepo) *CertUsecase {
|
||||
return &CertUsecase{
|
||||
repo: certRepo,
|
||||
setting: settingRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) List(page, limit uint) ([]*types.CertList, int64, error) {
|
||||
@@ -85,8 +93,8 @@ func (uc *CertUsecase) Get(id uint) (*Cert, error) {
|
||||
return uc.repo.Get(id)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) GetByWebsite(WebsiteID uint) (*Cert, error) {
|
||||
return uc.repo.GetByWebsite(WebsiteID)
|
||||
func (uc *CertUsecase) GetByWebsite(websiteID uint) (*Cert, error) {
|
||||
return uc.repo.GetByWebsite(websiteID)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Upload(ctx context.Context, req *request.CertUpload) (*Cert, error) {
|
||||
@@ -198,7 +206,7 @@ func (uc *CertUsecase) ObtainAutoWithProgressCallback(ctx context.Context, id ui
|
||||
ProgressCallback: progressCallback,
|
||||
})
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
if len(cert.Websites) == 0 {
|
||||
return nil, errors.New(uc.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
}
|
||||
hasWildcard := slices.ContainsFunc(cert.Domains, func(d string) bool {
|
||||
@@ -207,8 +215,8 @@ func (uc *CertUsecase) ObtainAutoWithProgressCallback(ctx context.Context, id ui
|
||||
if hasWildcard {
|
||||
return nil, errors.New(uc.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
conf := fmt.Sprintf("%s/sites/%s/config/site/001-acme.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, webServer)
|
||||
confs, fallback := uc.repo.HTTPConfs(cert, webServer)
|
||||
client.UseHTTP(confs, fallback, webServer)
|
||||
}
|
||||
|
||||
report(uc.t.Get("issuing certificate, domains: %s", strings.Join(cert.Domains, ", ")))
|
||||
@@ -228,9 +236,9 @@ func (uc *CertUsecase) ObtainAutoWithProgressCallback(ctx context.Context, id ui
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
if len(cert.Websites) > 0 {
|
||||
report(uc.t.Get("deploying certificate to website"))
|
||||
return &ssl, uc.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
return &ssl, uc.Deploy(cert.ID, cert.WebsiteIDs(), false)
|
||||
}
|
||||
|
||||
if err = uc.repo.RunScript(cert); err != nil {
|
||||
@@ -240,9 +248,21 @@ func (uc *CertUsecase) ObtainAutoWithProgressCallback(ctx context.Context, id ui
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error) {
|
||||
func (uc *CertUsecase) ObtainPanel(account *CertAccount, domains []string) ([]byte, []byte, error) {
|
||||
names := domains
|
||||
if len(names) == 0 {
|
||||
var err error
|
||||
names, err = uc.setting.GetSlice(SettingKeyPublicIPs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil, nil, errors.New(uc.t.Get("Please set the panel IP in settings first for ACME certificate generation"))
|
||||
}
|
||||
}
|
||||
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
return uc.repo.ObtainPanel(account, ips, webServer)
|
||||
return uc.repo.ObtainPanel(account, names, webServer)
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) ObtainSelfSigned(id uint) error {
|
||||
@@ -262,8 +282,8 @@ func (uc *CertUsecase) ObtainSelfSigned(id uint) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
return uc.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
if len(cert.Websites) > 0 {
|
||||
return uc.Deploy(cert.ID, cert.WebsiteIDs(), false)
|
||||
}
|
||||
|
||||
if err = uc.repo.RunScript(cert); err != nil {
|
||||
@@ -309,17 +329,16 @@ func (uc *CertUsecase) RenewWithProgressCallback(ctx context.Context, id uint, p
|
||||
ProgressCallback: progressCallback,
|
||||
})
|
||||
} else {
|
||||
if cert.Website == nil {
|
||||
if len(cert.Websites) == 0 {
|
||||
return nil, errors.New(uc.t.Get("this certificate is not associated with a website and cannot be obtained. You can try to obtain it manually"))
|
||||
} else {
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New(uc.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
}
|
||||
conf := fmt.Sprintf("%s/sites/%s/config/site/001-acme.conf", app.Root, cert.Website.Name)
|
||||
client.UseHTTP(conf, webServer)
|
||||
}
|
||||
for _, domain := range cert.Domains {
|
||||
if strings.Contains(domain, "*") {
|
||||
return nil, errors.New(uc.t.Get("wildcard domains cannot use HTTP verification"))
|
||||
}
|
||||
}
|
||||
confs, fallback := uc.repo.HTTPConfs(cert, webServer)
|
||||
client.UseHTTP(confs, fallback, webServer)
|
||||
}
|
||||
|
||||
report(uc.t.Get("renewing certificate, domains: %s", strings.Join(cert.Domains, ", ")))
|
||||
@@ -344,9 +363,9 @@ func (uc *CertUsecase) RenewWithProgressCallback(ctx context.Context, id uint, p
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cert.Website != nil {
|
||||
if len(cert.Websites) > 0 {
|
||||
report(uc.t.Get("deploying certificate to website"))
|
||||
return &ssl, uc.Deploy(cert.ID, cert.WebsiteID, false)
|
||||
return &ssl, uc.Deploy(cert.ID, cert.WebsiteIDs(), false)
|
||||
}
|
||||
|
||||
return &ssl, nil
|
||||
@@ -382,8 +401,8 @@ func (uc *CertUsecase) RefreshRenewalInfo(id uint) (mholtacme.RenewalInfo, error
|
||||
return renewInfo, nil
|
||||
}
|
||||
|
||||
func (uc *CertUsecase) Deploy(ID, WebsiteID uint, enableHTTPS bool) error {
|
||||
cert, err := uc.repo.Get(ID)
|
||||
func (uc *CertUsecase) Deploy(id uint, websiteIDs []uint, enableHTTPS bool) error {
|
||||
cert, err := uc.repo.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -392,31 +411,42 @@ func (uc *CertUsecase) Deploy(ID, WebsiteID uint, enableHTTPS bool) error {
|
||||
return errors.New(uc.t.Get("this certificate has not been obtained successfully and cannot be deployed"))
|
||||
}
|
||||
|
||||
website, err := uc.repo.LoadWebsite(WebsiteID)
|
||||
websites, err := uc.repo.LoadWebsites(websiteIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||
certPath := filepath.Join(configDir, "fullchain.pem")
|
||||
keyPath := filepath.Join(configDir, "private.key")
|
||||
if err = uc.repo.WriteCertFiles(cert, certPath, keyPath); err != nil {
|
||||
|
||||
// 建立关联,使续签后能自动部署到这些网站
|
||||
if err = uc.repo.BindWebsites(cert.ID, websiteIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 开启 HTTPS
|
||||
if enableHTTPS && !website.SSL {
|
||||
// 原 getVhost 首步读 webserver 设置并传播错误,保持该语义
|
||||
webServer, err := uc.setting.Get(SettingKeyWebserver)
|
||||
if err != nil {
|
||||
webServer, webServerErr := uc.setting.Get(SettingKeyWebserver)
|
||||
tlsVersions, _ := uc.setting.GetSlice(SettingKeyWebsiteTLSVersions)
|
||||
listenIPv6, err := uc.setting.GetBool(SettingKeyWebsiteListenIPv6, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, website := range websites {
|
||||
configDir := filepath.Join(app.Root, "sites", website.Name, "config")
|
||||
certPath := filepath.Join(configDir, "fullchain.pem")
|
||||
keyPath := filepath.Join(configDir, "private.key")
|
||||
if err = uc.repo.WriteCertFiles(cert, certPath, keyPath); err != nil {
|
||||
return err
|
||||
}
|
||||
tlsVersions, _ := uc.setting.GetSlice(SettingKeyWebsiteTLSVersions)
|
||||
if err = uc.repo.EnableWebsiteSSL(website, certPath, keyPath, webServer, tlsVersions); err != nil {
|
||||
return err
|
||||
|
||||
// 开启 HTTPS
|
||||
if enableHTTPS && !website.SSL {
|
||||
if webServerErr != nil {
|
||||
return webServerErr
|
||||
}
|
||||
if err = uc.repo.EnableWebsiteSSL(website, certPath, keyPath, webServer, tlsVersions, listenIPv6); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webServer, _ := uc.setting.Get(SettingKeyWebserver)
|
||||
return uc.repo.ReloadWebserver(webServer)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,13 +46,13 @@ type CertAccountUsecase struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewCertAccountUsecase(t *gotext.Locale, log *slog.Logger, certAccountRepo CertAccountRepo, userRepo UserRepo) (*CertAccountUsecase, error) {
|
||||
func NewCertAccountUsecase(t *gotext.Locale, log *slog.Logger, certAccountRepo CertAccountRepo, userRepo UserRepo) *CertAccountUsecase {
|
||||
return &CertAccountUsecase{
|
||||
repo: certAccountRepo,
|
||||
userRepo: userRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *CertAccountUsecase) List(page, limit uint) ([]*CertAccount, int64, error) {
|
||||
|
||||
@@ -4,7 +4,10 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/docker"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -27,10 +30,17 @@ type ContainerRepo interface {
|
||||
type ContainerUsecase struct {
|
||||
repo ContainerRepo
|
||||
setting SettingRepo
|
||||
task TaskRepo
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewContainerUsecase(repo ContainerRepo, setting SettingRepo) *ContainerUsecase {
|
||||
return &ContainerUsecase{repo: repo, setting: setting}
|
||||
func NewContainerUsecase(t *gotext.Locale, containerRepo ContainerRepo, settingRepo SettingRepo, taskRepo TaskRepo) *ContainerUsecase {
|
||||
return &ContainerUsecase{
|
||||
repo: containerRepo,
|
||||
setting: settingRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) ListAll() ([]types.Container, error) {
|
||||
@@ -62,6 +72,29 @@ func (uc *ContainerUsecase) Create(req *request.ContainerCreate) (string, error)
|
||||
return uc.repo.Create(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) CreateBackground(req *request.ContainerCreate) error {
|
||||
shell, err := docker.RunShell(containerSock(uc.setting), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
key := ""
|
||||
if req.Name != "" {
|
||||
key = "container:create:" + req.Name
|
||||
}
|
||||
task.Key = key
|
||||
target := req.Name
|
||||
if target == "" {
|
||||
target = req.Image
|
||||
}
|
||||
task.Name = uc.t.Get("Create container %s", target)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = shell
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
// Update 删除旧容器后按新配置重建同名容器
|
||||
func (uc *ContainerUsecase) Update(id string, req *request.ContainerCreate) (string, error) {
|
||||
sock := containerSock(uc.setting)
|
||||
@@ -71,6 +104,33 @@ func (uc *ContainerUsecase) Update(id string, req *request.ContainerCreate) (str
|
||||
return uc.repo.Create(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) UpdateBackground(id string, req *request.ContainerCreate) error {
|
||||
sock := containerSock(uc.setting)
|
||||
runShell, err := docker.RunShell(sock, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shell := strings.Join([]string{
|
||||
"set -e",
|
||||
docker.Command(sock, "image", "inspect", req.Image) + " >/dev/null 2>&1 || " + docker.Command(sock, "pull", req.Image),
|
||||
docker.Command(sock, "rm", "--force", id),
|
||||
runShell,
|
||||
}, "\n")
|
||||
target := req.Name
|
||||
if target == "" {
|
||||
target = req.Image
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "container:update:" + id
|
||||
task.Name = uc.t.Get("Update container %s", target)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = shell
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *ContainerUsecase) Remove(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Remove(sock, id)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/request"
|
||||
"github.com/acepanel/panel/v3/pkg/docker"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
@@ -16,10 +19,17 @@ type ContainerImageRepo interface {
|
||||
type ContainerImageUsecase struct {
|
||||
repo ContainerImageRepo
|
||||
setting SettingRepo
|
||||
task TaskRepo
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewContainerImageUsecase(repo ContainerImageRepo, setting SettingRepo) *ContainerImageUsecase {
|
||||
return &ContainerImageUsecase{repo: repo, setting: setting}
|
||||
func NewContainerImageUsecase(t *gotext.Locale, containerImageRepo ContainerImageRepo, settingRepo SettingRepo, taskRepo TaskRepo) *ContainerImageUsecase {
|
||||
return &ContainerImageUsecase{
|
||||
repo: containerImageRepo,
|
||||
setting: settingRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) List() ([]types.ContainerImage, error) {
|
||||
@@ -37,6 +47,22 @@ func (uc *ContainerImageUsecase) Pull(req *request.ContainerImagePull) error {
|
||||
return uc.repo.Pull(sock, req)
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) PullBackground(req *request.ContainerImagePull) error {
|
||||
shell, cancelShell, err := docker.ImagePullShell(containerSock(uc.setting), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task := new(Task)
|
||||
task.Key = "container:image:pull:" + req.Name
|
||||
task.Name = uc.t.Get("Pull image %s", req.Name)
|
||||
task.Status = TaskStatusWaiting
|
||||
task.Shell = shell
|
||||
task.CancelShell = cancelShell
|
||||
|
||||
return uc.task.Push(task)
|
||||
}
|
||||
|
||||
func (uc *ContainerImageUsecase) Remove(id string) error {
|
||||
sock := containerSock(uc.setting)
|
||||
return uc.repo.Remove(sock, id)
|
||||
|
||||
@@ -48,14 +48,14 @@ type DatabaseUsecase struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseUsecase(databaseUserUsecase *DatabaseUserUsecase, t *gotext.Locale, log *slog.Logger, databaseRepo DatabaseRepo, databaseServerRepo DatabaseServerRepo) (*DatabaseUsecase, error) {
|
||||
func NewDatabaseUsecase(databaseUserUsecase *DatabaseUserUsecase, t *gotext.Locale, log *slog.Logger, databaseRepo DatabaseRepo, databaseServerRepo DatabaseServerRepo) *DatabaseUsecase {
|
||||
return &DatabaseUsecase{
|
||||
repo: databaseRepo,
|
||||
server: databaseServerRepo,
|
||||
user: databaseUserUsecase,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *DatabaseUsecase) List(ctx context.Context, page, limit uint, typ string) ([]*Database, int64, error) {
|
||||
|
||||
@@ -92,12 +92,12 @@ type DatabaseServerUsecase struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseServerUsecase(t *gotext.Locale, log *slog.Logger, databaseServerRepo DatabaseServerRepo) (*DatabaseServerUsecase, error) {
|
||||
func NewDatabaseServerUsecase(t *gotext.Locale, log *slog.Logger, databaseServerRepo DatabaseServerRepo) *DatabaseServerUsecase {
|
||||
return &DatabaseServerUsecase{
|
||||
repo: databaseServerRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *DatabaseServerUsecase) Count() (int64, error) {
|
||||
|
||||
@@ -85,12 +85,12 @@ type DatabaseUserUsecase struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewDatabaseUserUsecase(log *slog.Logger, databaseServerRepo DatabaseServerRepo, databaseUserRepo DatabaseUserRepo) (*DatabaseUserUsecase, error) {
|
||||
func NewDatabaseUserUsecase(log *slog.Logger, databaseServerRepo DatabaseServerRepo, databaseUserRepo DatabaseUserRepo) *DatabaseUserUsecase {
|
||||
return &DatabaseUserUsecase{
|
||||
repo: databaseUserRepo,
|
||||
server: databaseServerRepo,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *DatabaseUserUsecase) Count() (int64, error) {
|
||||
|
||||
@@ -27,13 +27,13 @@ type EnvironmentUsecase struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewEnvironmentUsecase(t *gotext.Locale, cacheRepo CacheRepo, environmentRepo EnvironmentRepo, taskRepo TaskRepo) (*EnvironmentUsecase, error) {
|
||||
func NewEnvironmentUsecase(t *gotext.Locale, cacheRepo CacheRepo, environmentRepo EnvironmentRepo, taskRepo TaskRepo) *EnvironmentUsecase {
|
||||
return &EnvironmentUsecase{
|
||||
repo: environmentRepo,
|
||||
cache: cacheRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *EnvironmentUsecase) Types() []types.LV {
|
||||
|
||||
@@ -33,11 +33,11 @@ type FileShareUsecase struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewFileShareUsecase(log *slog.Logger, fileShareRepo FileShareRepo) (*FileShareUsecase, error) {
|
||||
func NewFileShareUsecase(log *slog.Logger, fileShareRepo FileShareRepo) *FileShareUsecase {
|
||||
return &FileShareUsecase{
|
||||
repo: fileShareRepo,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *FileShareUsecase) List() ([]*FileShare, error) {
|
||||
|
||||
@@ -2,12 +2,9 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// operatorID 从 context 获取操作员 ID,无法获取时返回 0(系统操作)
|
||||
@@ -22,11 +19,6 @@ func operatorID(ctx context.Context) uint64 {
|
||||
return cast.ToUint64(userID)
|
||||
}
|
||||
|
||||
// IsNotFound 判断错误是否为记录不存在
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
|
||||
// containerSock 从设置读取容器 socket 路径,未配置或失败时返回默认值
|
||||
func containerSock(setting SettingRepo) string {
|
||||
sock, _ := setting.Get(SettingKeyContainerSock)
|
||||
@@ -35,7 +27,7 @@ func containerSock(setting SettingRepo) string {
|
||||
}
|
||||
// 自动补全 scheme
|
||||
if !strings.Contains(sock, "://") {
|
||||
sock = fmt.Sprintf("unix://%s", sock)
|
||||
sock = "unix://" + sock
|
||||
}
|
||||
return sock
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ const (
|
||||
OperationTypeMonitor = "monitor"
|
||||
OperationTypeWebhook = "webhook"
|
||||
OperationTypeUser = "user"
|
||||
OperationTypeWaf = "waf"
|
||||
)
|
||||
|
||||
// LogEntry 日志条目
|
||||
|
||||
+17
-2
@@ -11,14 +11,17 @@ import (
|
||||
|
||||
type Monitor struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Info types.CurrentInfo `gorm:"not null;default:'{}';serializer:json" json:"info"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Info types.CurrentInfo `gorm:"not null;default:'{}';serializer:zstd" json:"info"`
|
||||
CreatedAt time.Time `gorm:"index:idx_monitors_created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type MonitorRepo interface {
|
||||
Create(monitor *Monitor) error
|
||||
ClearBefore(t time.Time) error
|
||||
Clear() error
|
||||
List(start, end time.Time) ([]*Monitor, error)
|
||||
VacuumDB() error
|
||||
}
|
||||
|
||||
type MonitorUsecase struct {
|
||||
@@ -75,6 +78,18 @@ func (uc *MonitorUsecase) Clear() error {
|
||||
return uc.repo.Clear()
|
||||
}
|
||||
|
||||
func (uc *MonitorUsecase) Create(monitor *Monitor) error {
|
||||
return uc.repo.Create(monitor)
|
||||
}
|
||||
|
||||
func (uc *MonitorUsecase) ClearBefore(t time.Time) error {
|
||||
return uc.repo.ClearBefore(t)
|
||||
}
|
||||
|
||||
func (uc *MonitorUsecase) List(start, end time.Time) ([]*Monitor, error) {
|
||||
return uc.repo.List(start, end)
|
||||
}
|
||||
|
||||
func (uc *MonitorUsecase) VacuumDB() error {
|
||||
return uc.repo.VacuumDB()
|
||||
}
|
||||
|
||||
+16
-2
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/crypt"
|
||||
"github.com/samber/lo"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
@@ -97,14 +98,14 @@ type NotifyUsecase struct {
|
||||
pending chan struct{}
|
||||
}
|
||||
|
||||
func NewNotifyUsecase(t *gotext.Locale, log *slog.Logger, notifyChannelRepo NotifyChannelRepo, settingRepo SettingRepo) (*NotifyUsecase, error) {
|
||||
func NewNotifyUsecase(t *gotext.Locale, log *slog.Logger, notifyChannelRepo NotifyChannelRepo, settingRepo SettingRepo) *NotifyUsecase {
|
||||
return &NotifyUsecase{
|
||||
repo: notifyChannelRepo,
|
||||
setting: settingRepo,
|
||||
log: log,
|
||||
t: t,
|
||||
pending: make(chan struct{}, notifyMaxPending),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *NotifyUsecase) List(page, limit uint) ([]*NotifyChannel, int64, error) {
|
||||
@@ -239,6 +240,19 @@ func (uc *NotifyUsecase) SendEvent(event NotifyEvent, subject, body string) {
|
||||
}()
|
||||
}
|
||||
|
||||
// EventEnabled 判断给定事件中是否有已订阅且配好渠道的
|
||||
// 供调用方在采集前提前跳过,避免为没人接收的通知付出采集开销
|
||||
func (uc *NotifyUsecase) EventEnabled(events ...NotifyEvent) bool {
|
||||
setting, err := uc.GetSetting()
|
||||
if err != nil || len(setting.Channels) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return lo.SomeBy(events, func(event NotifyEvent) bool {
|
||||
return slices.Contains(setting.Events, string(event))
|
||||
})
|
||||
}
|
||||
|
||||
// SendEventSync 同步发送系统事件通知,未订阅该事件或未配置渠道时静默跳过
|
||||
// 供 CLI 等短生命周期进程使用,异步发送会随进程退出丢失
|
||||
func (uc *NotifyUsecase) SendEventSync(ctx context.Context, event NotifyEvent, subject, body string) error {
|
||||
|
||||
@@ -44,12 +44,12 @@ type ProjectUsecase struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewProjectUsecase(t *gotext.Locale, log *slog.Logger, projectRepo ProjectRepo) (*ProjectUsecase, error) {
|
||||
func NewProjectUsecase(t *gotext.Locale, log *slog.Logger, projectRepo ProjectRepo) *ProjectUsecase {
|
||||
return &ProjectUsecase{
|
||||
repo: projectRepo,
|
||||
log: log,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ProjectUsecase) Count() (int64, error) {
|
||||
|
||||
@@ -2,7 +2,7 @@ package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"errors"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ func (uc *SafeUsecase) UpdatePingStatus(ctx context.Context, status bool) error
|
||||
return err
|
||||
}
|
||||
if !running {
|
||||
return fmt.Errorf("failed to update ping status: firewall is not running")
|
||||
return errors.New("failed to update ping status: firewall is not running")
|
||||
}
|
||||
|
||||
if err = uc.repo.SetPingStatus(status); err != nil {
|
||||
|
||||
+19
-10
@@ -10,19 +10,28 @@ import (
|
||||
// ScanEvent 扫描事件模型
|
||||
type ScanEvent struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
SourceIP string `gorm:"not null;uniqueIndex:idx_scan_unique" json:"source_ip"`
|
||||
Port uint `gorm:"not null;uniqueIndex:idx_scan_unique" json:"port"`
|
||||
Protocol string `gorm:"not null;default:'tcp';uniqueIndex:idx_scan_unique" json:"protocol"`
|
||||
Date string `gorm:"not null;uniqueIndex:idx_scan_unique;index:idx_scan_date" json:"date"` // YYYY-MM-DD
|
||||
SourceID uint `gorm:"not null;uniqueIndex:idx_scan_unique,priority:2" json:"-"`
|
||||
Port uint `gorm:"not null;uniqueIndex:idx_scan_unique,priority:3" json:"port"`
|
||||
Protocol string `gorm:"not null;default:'tcp';uniqueIndex:idx_scan_unique,priority:4" json:"protocol"`
|
||||
Date string `gorm:"not null;uniqueIndex:idx_scan_unique,priority:1" json:"date"` // YYYY-MM-DD
|
||||
Count uint `gorm:"not null;default:1" json:"count"`
|
||||
Country string `gorm:"not null;default:''" json:"country"`
|
||||
Region string `gorm:"not null;default:''" json:"region"`
|
||||
City string `gorm:"not null;default:''" json:"city"`
|
||||
ISP string `gorm:"not null;default:''" json:"isp"`
|
||||
FirstSeen time.Time `gorm:"not null" json:"first_seen"`
|
||||
LastSeen time.Time `gorm:"not null" json:"last_seen"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
SourceIP string `gorm:"->;-:migration" json:"source_ip"`
|
||||
Country string `gorm:"->;-:migration" json:"country"`
|
||||
Region string `gorm:"->;-:migration" json:"region"`
|
||||
City string `gorm:"->;-:migration" json:"city"`
|
||||
ISP string `gorm:"->;-:migration" json:"isp"`
|
||||
}
|
||||
|
||||
// ScanSource 扫描来源模型
|
||||
type ScanSource struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
SourceIP string `gorm:"not null;uniqueIndex:idx_scan_sources_ip"`
|
||||
Country string `gorm:"not null;default:''"`
|
||||
Region string `gorm:"not null;default:''"`
|
||||
City string `gorm:"not null;default:''"`
|
||||
ISP string `gorm:"not null;default:''"`
|
||||
}
|
||||
|
||||
// ScanSummary 扫描汇总
|
||||
|
||||
@@ -35,6 +35,7 @@ const (
|
||||
SettingKeyProjectPath SettingKey = "project_path"
|
||||
SettingKeyContainerSock SettingKey = "container_sock"
|
||||
SettingKeyWebsiteTLSVersions SettingKey = "website_tls_versions"
|
||||
SettingKeyWebsiteListenIPv6 SettingKey = "website_listen_ipv6"
|
||||
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
|
||||
SettingKeyPostgresPassword SettingKey = "postgres_password"
|
||||
SettingKeyMongoDBAdminPassword SettingKey = "mongodb_admin_password"
|
||||
@@ -99,13 +100,13 @@ type SettingUsecase struct {
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
func NewSettingUsecase(t *gotext.Locale, log *slog.Logger, settingRepo SettingRepo, taskRepo TaskRepo) (*SettingUsecase, error) {
|
||||
func NewSettingUsecase(t *gotext.Locale, log *slog.Logger, settingRepo SettingRepo, taskRepo TaskRepo) *SettingUsecase {
|
||||
return &SettingUsecase{
|
||||
repo: settingRepo,
|
||||
task: taskRepo,
|
||||
t: t,
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *SettingUsecase) Get(key SettingKey, defaultValue ...string) (string, error) {
|
||||
|
||||
+19
-9
@@ -31,7 +31,7 @@ type TamperRule struct {
|
||||
// TamperLog 篡改拦截/告警日志
|
||||
type TamperLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Path string `gorm:"not null;default:'';index" json:"path"`
|
||||
Path string `gorm:"not null;default:'';serializer:zstd" json:"path"`
|
||||
Op string `gorm:"not null;default:''" json:"op"` // write/unlink/rename/setattr/create
|
||||
PID uint `gorm:"not null;default:0" json:"pid"`
|
||||
Comm string `gorm:"not null;default:''" json:"comm"`
|
||||
@@ -69,22 +69,23 @@ type TamperUsecase struct {
|
||||
log *slog.Logger
|
||||
t *gotext.Locale
|
||||
|
||||
mu sync.Mutex
|
||||
mgr *tamper.Manager
|
||||
buf []*TamperLog
|
||||
bufMu sync.Mutex
|
||||
notifyAt time.Time
|
||||
drainC chan struct{}
|
||||
mu sync.Mutex
|
||||
mgr *tamper.Manager
|
||||
cleanedAt time.Time
|
||||
buf []*TamperLog
|
||||
bufMu sync.Mutex
|
||||
notifyAt time.Time
|
||||
drainC chan struct{}
|
||||
}
|
||||
|
||||
func NewTamperUsecase(notifyUsecase *NotifyUsecase, settingUsecase *SettingUsecase, t *gotext.Locale, log *slog.Logger, tamperRepo TamperRepo) (*TamperUsecase, error) {
|
||||
func NewTamperUsecase(notifyUsecase *NotifyUsecase, settingUsecase *SettingUsecase, t *gotext.Locale, log *slog.Logger, tamperRepo TamperRepo) *TamperUsecase {
|
||||
return &TamperUsecase{
|
||||
repo: tamperRepo,
|
||||
setting: settingUsecase,
|
||||
notify: notifyUsecase,
|
||||
log: log,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Supported 当前平台是否支持防篡改
|
||||
@@ -312,7 +313,16 @@ func (uc *TamperUsecase) notifyBlocked(logs []*TamperLog) {
|
||||
}
|
||||
|
||||
// CleanupLogs 清理过期日志
|
||||
// 调用方每分钟触发,但日志按天过期,故限流到 6 小时一次
|
||||
func (uc *TamperUsecase) CleanupLogs() {
|
||||
uc.mu.Lock()
|
||||
if time.Since(uc.cleanedAt) < 6*time.Hour {
|
||||
uc.mu.Unlock()
|
||||
return
|
||||
}
|
||||
uc.cleanedAt = time.Now()
|
||||
uc.mu.Unlock()
|
||||
|
||||
s, err := uc.GetSetting()
|
||||
if err != nil || s.LogDays == 0 {
|
||||
return
|
||||
|
||||
@@ -26,12 +26,12 @@ type TemplateUsecase struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewTemplateUsecase(t *gotext.Locale, cacheRepo CacheRepo, templateRepo TemplateRepo) (*TemplateUsecase, error) {
|
||||
func NewTemplateUsecase(t *gotext.Locale, cacheRepo CacheRepo, templateRepo TemplateRepo) *TemplateUsecase {
|
||||
return &TemplateUsecase{
|
||||
repo: templateRepo,
|
||||
cache: cacheRepo,
|
||||
t: t,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// List 获取所有模版,包括本地模板
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user