Compare commits

...
60 Commits
Author SHA1 Message Date
CityFun 20b89a8fdd fix: update MdEditor component to prevent potential XSS attacks (#11548) 2026-01-04 09:31:43 +00:00
CityFun 8def011e63 fix: update MdEditor component to prevent potential XSS attacks (#11538) 2026-01-04 02:51:41 +00:00
J.Knight 7c5b2f41e3 feat: add support for Porkbun DNS provider (#11233)
* update: ssl add porkbun platform

* fix: some noise
2025-12-08 16:39:27 +08:00
王贺 abd3c24562 fix: update node & go version (#11195) 2025-12-04 23:18:39 +08:00
王贺 f4e64ca8b3 fix: upgrade vite and vue plugin versions (#11193)
fix: upgrade vite and vue plugin versions
2025-12-04 23:10:11 +08:00
王贺 97fde371d9 Update download URLs to use versioned paths (#11192) 2025-12-04 22:10:06 +08:00
ssongliu 85bbb9cb15 fix: Fix partial abnormal issues caused by local backup path modification (#11190) 2025-12-04 22:04:38 +08:00
ssongliu 1d34b4aff7 perf: optimize login API logic (#11187) 2025-12-04 10:06:22 +00:00
ssongliu dc8c50c07a fix: optimize IP whitelist validation logic (#11183) 2025-12-04 17:21:45 +08:00
CityFun 4ac490c6b4 fix: Fixed issue with website config menu lost (#9473) 2025-07-09 07:26:55 +00:00
ssongliu 0d548c7df9 fix: Fix http image registry edit exception (#9460) 2025-07-08 18:18:00 +08:00
ssongliu 8ec5a5bd4b fix: Fix menu internationalization issues (#9456) 2025-07-08 06:05:55 +00:00
ssongliu 2e8bcc09e6 fix: Fix dashboard refresh exception issue (#9455) 2025-07-08 04:15:53 +00:00
ssongliu 29a304e2c8 fix: Optimize container image repo restart detection logic (#9447) 2025-07-08 02:47:53 +00:00
ssongliu b1adc37920 fix: Modify OneDrive default connection settings (#9440) 2025-07-07 10:05:50 +00:00
CityFun 03fac3ec06 fix: Resolve the issue of lost GPU configuration after upgrade ollama (#9362) 2025-07-01 07:47:33 +00:00
蘭 6e465ac215 fix: Fix the issue of multiple selection moving folder errors (#9319)
refs: #8612
2025-06-27 06:03:24 +00:00
ssongliu 21bbadc37b fix: Resolve abnormal state after Docker restart (#9305) 2025-06-26 11:19:22 +00:00
ssongliu 3061d640ce fix: Modify the service status judgment logic (#9301) 2025-06-26 17:18:14 +08:00
ssongliu 874717cbcd fix: Remove code comments (#9297) 2025-06-26 06:39:22 +00:00
ssongliu 6850acabae fix: Optimize supervisor initialization logic (#9295) 2025-06-26 06:29:23 +00:00
ssongliu 890ea42749 fix: Fix ClamAV service name detection issue (#9292) 2025-06-26 14:27:57 +08:00
ssongliu 6aaf61cde4 fix: Increase systemctl operation timeout (#9281) 2025-06-25 08:21:22 +00:00
wanghe-fit2cloud 73930c5e29 feat: Compatible with system upgrades for v1.10.30 and earlier versions 2025-06-24 17:32:23 +08:00
ssongliu 8f8d280e90 Revert "fix: Fix the issue with upgrade failures (#9262)" (#9264)
This reverts commit b46192024f.
2025-06-24 17:24:42 +08:00
ssongliu b46192024f fix: Fix the issue with upgrade failures (#9262) 2025-06-24 08:51:13 +00:00
ssongliu 8f939ad5be fix: Make firewall operations compatible with OpenWrt (#9257) 2025-06-24 06:21:13 +00:00
ssongliu a6363ce4e5 fix: Fix the issue with cronjob report loading failure (#9254)
Refs #8684
2025-06-24 05:47:14 +00:00
ssongliu fd490b2642 fix: Modify the image repository validation rules (#9253)
Refs #8870
2025-06-24 05:45:13 +00:00
ssongliu 7624670ee0 fix: Add a restart prompt for HTTP-type mirror repositories (#9252)
Refs #8978
2025-06-24 05:43:14 +00:00
ssongliu 8c3145e1c6 fix: Fix the problem of ip changes in container upgrades for macvlan … (#9251)
Refs #8950
2025-06-24 05:41:13 +00:00
ssongliu bec7b6989f fix: Fix the issue of abnormal highlighting in the cronjob script (#9246)
Refs #8683
2025-06-23 10:40:14 +00:00
CityFun 326f2654e6 feat: add PbootCMS extensions template for php runtime (#9243) 2025-06-23 18:37:47 +08:00
CityFun 708499a335 feat: fix issue with build frontend failed (#8994) 2025-06-10 08:56:35 +00:00
CityFun bdeb842939 feat: change 1panel v1 docs url (#8993) 2025-06-10 08:16:34 +00:00
王贺 edefdce3ff docs: Update the installation script (#8979) 2025-06-10 11:03:13 +08:00
longjuan b348c36571 feat: add name attrs to login inputs for password managers to recognize (#8953) 2025-06-09 11:26:02 +08:00
玲珑芯ExquisiteCore 6ae150b64e feat: Move current status to first position in MySQL settings and set… (#8882)
* feat: Move current status to first position in MySQL and Redis settings and set as default tab
2025-06-03 10:22:00 +08:00
maninhill 3687b9bea2 chore: Update README.md 2025-05-25 16:42:01 +08:00
maninhill 711431da82 Update README.md 2025-05-25 16:40:02 +08:00
Alan e93afc33aa ini filetype support (#8628) 2025-05-14 16:52:41 +08:00
CityFun 82dd993751 fix: Fix issue with update website config (#8610) 2025-05-12 10:41:58 +00:00
Wankko Ree 9ba293e154 fix: the docker image which no tag but used be pruned (#8529) 2025-05-03 13:25:24 +08:00
igophper 8fa9f128d8 fix(cronjob): prevent unnecessary log updates and clean log formatting (#8481)
Refs #8425
2025-04-28 09:09:48 +08:00
maninhill ed9f1599e6 chore: Update README.md 2025-04-25 11:38:18 +08:00
igophper 787a29769d fix(editor): reset edit state after not saving content (#8474)
Refs #8461
2025-04-25 11:13:23 +08:00
巴山夜语andgcsong023 6b379389d3 WIP: refactor(service): Refactor OpenRC service manager (#8416)
* refactor(service): 重构 OpenRC 服务管理器

- 更新 IsEnabled 和 IsActive 检查逻辑,使用更可靠的命令
- 修复 ServiceExists 检查,直接使用文件路径判断
- 优化 FindServices 函数,扫描 /etc/init.d 目录
- 调整 BuildCommand 函数,支持 OpenRC 特定操作
- 修改 ParseStatus 函数,使用更新后的正则表达式

* feat(backend): 优化 Fail2ban 初始化配置以支持 Alpine 系统

- 增加对 Alpine 系统的特殊配置支持
- 改进防火墙类型检测逻辑,支持多种防火墙服务
- 增加 SSH 端口和认证日志路径的自动检测
- 优化配置文件模板,提高兼容性和安全性

* refactor(backend): 重构 SSH 日志解析功能

- 改进了对不同日志格式的支持,包括 secure, auth 和 messages 文件
- 优化了日志解析逻辑,提高了代码的可读性和可维护性
- 增加了对 RFC3339 时间格式的支持
- 改善了对失败登录尝试的解析,包括无效用户和连接关闭的情况
- 重构了日期解析和 IP 地址验证的逻辑

* refactor(upgrade): 优化升级服务中的初始化脚本选择逻辑

- 新增 selectInitScript 函数,根据系统初始化管理器类型选择合适的初始化脚本
- 支持 systemd、openrc 和 sysvinit 三种初始化管理器
- 对于 sysvinit,增加对 /etc/rc.common 文件存在性的判断,以区分不同的初始化脚本
- 默认情况下使用当前服务名称作为初始化脚本名称

* fix(upgrade): 修复升级时初始化脚本更新问题

- 修改了 criticalUpdates 数组中的服务脚本更新逻辑
- 在 selectInitScript 函数中增加了复制脚本文件的逻辑,以应对服务名和脚本名不一致的情况

* feat(snap): 添加初始化脚本到快照

- 在创建快照时,将服务脚本复制到 initscript 目录
- 然后将整个 initscript 目录复制到快照的目标目录
- 添加了日志输出,便于调试和记录

* refactor(backend): 重构快照恢复流程

- 移除了未使用的 import 语句
- 删除了注释掉的代码块
- 修改了 1Panel 服务恢复的逻辑,增加了对当前服务名称的获取
- 快照恢复过程中,根据宿主机类型,自动选择初始化服务脚本
- 添加了日志输出以提高可追踪性
- 优化了文件路径的处理方式

---------

Co-authored-by: gcsong023 <gcsong023@users.noreply.github.com>
2025-04-23 17:04:08 +08:00
ChengPlay 5bcbf32823 Fix issue with errors when reverse proxy source text includes comments. (#8444)
Refs https://github.com/1Panel-dev/1Panel/issues/8442
2025-04-22 07:51:06 +00:00
maninhill 0aa90e1b2d chore: Update README.md 2025-04-19 09:59:23 +08:00
maninhill a7b87211b4 chore: Update README.md 2025-04-19 09:53:46 +08:00
maninhill 2c666933a5 chore: Update README.md 2025-04-19 09:43:31 +08:00
maninhill f280b533c6 Update README.md 2025-04-19 09:42:05 +08:00
wanghe-fit2cloud 47a16bc9a9 chore(build): optimize build script 2025-04-18 14:48:50 +08:00
ChengPlay 713202b6eb feat: update golang.org/x/net version (#8412) 2025-04-17 03:02:20 +00:00
巴山夜语andgcsong023 79020abb1c feat(systemctl): implement service manager initialization and command execution (#8380)
* feat(systemctl): 实现服务管理器初始化和命令执行

- 新增 systemctl 包,实现对 systemd、openrc 和 sysvinit 三种服务管理器的支持
- 添加服务状态检查、启动、停止、重启和启用/禁用功能
- 实现服务发现和智能服务名处理
- 添加配置文件查看功能
- 优化错误处理和日志记录

* refactor(system): 重构系统服务管理逻辑

- 引入 systemctl 工具包以统一处理系统服务
- 优化服务状态获取、配置文件路径解析等逻辑
- 重构 HostToolService 中的 GetToolStatus 方法
- 更新 DockerService、SettingService 等相关服务的处理方式
- 调整快照创建和恢复过程中的服务处理逻辑

* feat(utils): 添加目录复制功能并优化文件复制逻辑

- 新增 CopyDirs 函数,用于复制整个目录及其内容
- 添加对符号链接的复制支持
- 实现通用的 Copy 函数,根据文件类型自动选择 CopyFile 或 CopyDirs
- 在 CopyFile 函数中增加对源文件是目录的检查和错误提示

* refactortoolbox: 重构 Fail2ban 和 Pure-FTPd 的管理逻辑

- 优化了 Fail2ban 和 Pure-FTPd 的启动、停止、重启等操作的实现
- 改进了 Fail2ban 版本信息的获取方法
- 统一了错误处理和日志记录的格式
- 调整了部分导入的包,提高了代码的可维护性

* build: 禁用 CGO 以提高构建性能和兼容性

- 在 Linux 后端构建命令中添加 CGO_ENABLED=0 环境变量
- 此修改可以提高构建速度,并确保生成的二进制文件在没有 C 库依赖的环境中也能运行

* refactor(docker): 重构 Docker 服务的重启和操作逻辑

- 添加 isDockerSnapInstalled 函数来判断 Docker 是否通过 Snap 安装
- 在 OperateDocker 和 restartDocker 函数中增加对 Snap 安装的处理
- 移除未使用的 getDockerRestartCommand 函数

* fix(service): 优化快照恢复后的服务重启逻辑

- 在使用 systemd 管理服务时,增加 daemon-reload 操作以确保服务配置更新
- 重启 1panel 服务,以应用快照恢复的更改

* refactor(server): 支持非 systemd 系统的恢复操作

- 增加 isSystemd 函数判断系统是否为 systemd 类型
- 根据系统类型选择性地恢复服务文件
- 兼容 systemd 和非 systemd 系统的恢复流程

* fix(upgrade): 优化升级过程中的服务重启逻辑

- 移动服务重启逻辑到版本号更新之后,修复因提前重启导致的版本号未更新BUG。
- 在 systemctl 重启之前添加 daemon-reload 命令

---------

Co-authored-by: gcsong023 <gcsong023@users.noreply.github.com>
2025-04-17 10:26:13 +08:00
chiichen 83ef41cf1a feat: basic support riscv64 (#8369)
* feat: add riscv64

* feat: correct arch related codes
2025-04-17 10:24:04 +08:00
dependabot[bot] 91675c69e9 chore(deps): bump golang.org/x/crypto from 0.32.0 to 0.35.0 (#8400)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.32.0 to 0.35.0.
- [Commits](https://github.com/golang/crypto/compare/v0.32.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.35.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 11:41:42 +08:00
ChengPlay 0e68065b40 feat: change commandPlaceHolder translate (#8391) 2025-04-14 08:56:12 +00:00
ssongliu 600b769083 fix: Fix the mysql root account permission switchover exception (#8378) 2025-04-11 07:08:02 +00:00
zhengkunwang e7670f96de feat: update mcp proxy_pass (#8374) 2025-04-10 16:45:24 +08:00
88 changed files with 2929 additions and 896 deletions
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20.2' node-version: '22.19.0'
- name: Build Web - name: Build Web
run: | run: |
cd frontend && npm install && npm run build:pro cd frontend && npm install && npm run build:pro
@@ -21,7 +21,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
- name: Build Release - name: Build Release
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
with: with:
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20.2' node-version: '22.19.0'
- name: Build Web - name: Build Web
run: | run: |
cd frontend && npm install && npm run build:pro cd frontend && npm install && npm run build:pro
@@ -21,7 +21,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: '1.23' go-version: '1.24'
- name: Build Release - name: Build Release
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v6
with: with:
+5 -3
View File
@@ -10,7 +10,7 @@ before:
- ./ci/script.sh - ./ci/script.sh
- sed -i 's@ORIGINAL_VERSION=.*@ORIGINAL_VERSION=v{{ .Version }}@g' 1pctl - sed -i 's@ORIGINAL_VERSION=.*@ORIGINAL_VERSION=v{{ .Version }}@g' 1pctl
- go mod tidy - go mod tidy
builds: builds:
- main: ./cmd/server/main.go - main: ./cmd/server/main.go
binary: 1panel binary: 1panel
@@ -31,6 +31,7 @@ builds:
- arm - arm
- ppc64le - ppc64le
- s390x - s390x
- riscv64
archives: archives:
- formats: [ 'tar.gz' ] - formats: [ 'tar.gz' ]
@@ -38,10 +39,11 @@ archives:
wrap_in_directory: true wrap_in_directory: true
files: files:
- 1pctl - 1pctl
- 1panel.service
- install.sh - install.sh
- GeoIP.mmdb - 1panel.service
- initscript/*
- lang/* - lang/*
- GeoIP.mmdb
checksum: checksum:
name_template: 'checksums.txt' name_template: 'checksums.txt'
+2 -2
View File
@@ -23,7 +23,7 @@ build_frontend:
build_backend_on_linux: build_backend_on_linux:
cd $(SERVER_PATH) \ cd $(SERVER_PATH) \
&& GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN) && CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) $(GOBUILD) -trimpath -ldflags '-s -w' -o $(BUILD_PATH)/$(APP_NAME) $(MAIN)
build_backend_on_darwin: build_backend_on_darwin:
cd $(SERVER_PATH) \ cd $(SERVER_PATH) \
@@ -31,4 +31,4 @@ build_backend_on_darwin:
build_all: build_frontend build_backend_on_linux build_all: build_frontend build_backend_on_linux
build_on_local: clean_assets build_frontend build_backend_on_darwin upx_bin build_on_local: clean_assets build_frontend build_backend_on_darwin
+12 -3
View File
@@ -1,5 +1,5 @@
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p> <p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
<p align="center"><b>Top-Rated Web-based Linux Server Management Tool</b><br>Best VPS control panel<br>新一代的 Linux 服务器运维管理面板</p> <p align="center"><b>Simplify Linux Server Management</b></p>
<p align="center"> <p align="center">
<a href="https://trendshift.io/repositories/2462" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2462" alt="1Panel-dev%2F1Panel | Trendshift" style="width: 240px; height: auto;" /></a> <a href="https://trendshift.io/repositories/2462" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2462" alt="1Panel-dev%2F1Panel | Trendshift" style="width: 240px; height: auto;" /></a>
</p> </p>
@@ -31,13 +31,14 @@
------------------------------ ------------------------------
1Panel is an open-source, modern web-based control panel for Linux server management. 1Panel provides an intuitive web interface and MCP Server to manage websites, files, containers, databases, and LLMs on a Linux server.
- **Efficient Management**: Through a user-friendly web graphical interface, 1Panel enables users to effortlessly manage their Linux servers. Key features include host monitoring, file management, database administration, container management, LLMs management. - **Efficient Management**: Through a user-friendly web graphical interface, 1Panel enables users to effortlessly manage their Linux servers. Key features include host monitoring, file management, database administration, container management, LLMs management.
- **Rapid Website Deployment**: With deep integration of the popular open-source website building software WordPress, 1Panel streamlines the process of domain binding and SSL certificate configuration, all achievable with just one click. - **Rapid Website Deployment**: With deep integration of the popular open-source website building software WordPress, 1Panel streamlines the process of domain binding and SSL certificate configuration, all achievable with just one click.
- **Application Store**: 1Panel curates a wide range of high-quality open-source tools and applications, facilitating easy installation and updates for its users. - **Application Store**: 1Panel curates a wide range of high-quality open-source tools and applications, facilitating easy installation and updates for its users.
- **Security and Reliability**: By leveraging containerization and secure application deployment practices, 1Panel minimizes vulnerability exposure. It further enhances security through integrated firewall management and log auditing capabilities. - **Security and Reliability**: By leveraging containerization and secure application deployment practices, 1Panel minimizes vulnerability exposure. It further enhances security through integrated firewall management and log auditing capabilities.
- **One-Click Backup & Restore**: Data protection is made simple with 1Panel's one-click backup and restore functionality, supporting various cloud storage solutions to ensure data integrity and availability. - **One-Click Backup & Restore**: Data protection is made simple with 1Panel's one-click backup and restore functionality, supporting various cloud storage solutions to ensure data integrity and availability.
- **MCP Server**: [1Panel MCP Server](https://github.com/1Panel-dev/mcp-1panel) allow user to execute server operations via natural language.
## Quick Start ## Quick Start
@@ -49,7 +50,7 @@ curl -sSL https://resource.1panel.pro/quick_start.sh -o quick_start.sh && bash q
Please refer to our [documentation](https://docs.1panel.pro/quick_start/) for more details. Please refer to our [documentation](https://docs.1panel.pro/quick_start/) for more details.
中国用户请使用这个 [安装脚本](https://1panel.cn/docs/installation/online_installation/),其应用数量比国际版本更丰富。 中国用户请使用此 [安装脚本](https://1panel.cn/docs/v2/installation/online_installation/),默认安装 V2 版本,应用数量相较国际版本更丰富。
## Screenshot ## Screenshot
@@ -69,6 +70,10 @@ Compared to the OSS Edition, 1Panel Pro Edition provides users with a wealth of
If you discover any security issues, please refer to [SECURITY.md](/SECURITY.md). If you discover any security issues, please refer to [SECURITY.md](/SECURITY.md).
## GitAds Sponsored
[![Sponsored by GitAds](https://gitads.dev/v1/ad-serve?source=1panel-dev/1panel@github)](https://gitads.dev/v1/ad-track?source=1panel-dev/1panel@github)
## License ## License
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
@@ -76,3 +81,7 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License")
<https://www.gnu.org/licenses/gpl-3.0.html> <https://www.gnu.org/licenses/gpl-3.0.html>
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
<!-- GitAds-Verify: 9M8AMBB11GYJ3RUNXGB5R1MR71KPATU9 -->
+17 -6
View File
@@ -2,12 +2,14 @@ package v1
import ( import (
"encoding/base64" "encoding/base64"
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/captcha" "github.com/1Panel-dev/1Panel/backend/utils/captcha"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -26,7 +28,9 @@ func (b *BaseApi) Login(c *gin.Context) {
return return
} }
if req.AuthMethod != "jwt" && !req.IgnoreCaptcha { ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
if needCaptcha {
if err := captcha.VerifyCode(req.CaptchaID, req.Captcha); err != nil { if err := captcha.VerifyCode(req.CaptchaID, req.Captcha); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
@@ -48,9 +52,11 @@ func (b *BaseApi) Login(c *gin.Context) {
user, err := authService.Login(c, req, string(entrance)) user, err := authService.Login(c, req, string(entrance))
go saveLoginLogs(c, err) go saveLoginLogs(c, err)
if err != nil { if err != nil {
global.IPTracker.SetNeedCaptcha(ip)
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
global.IPTracker.Clear(ip)
helper.SuccessWithData(c, user) helper.SuccessWithData(c, user)
} }
@@ -134,16 +140,21 @@ func (b *BaseApi) CheckIsIntl(c *gin.Context) {
} }
// @Tags Auth // @Tags Auth
// @Summary Load System Language // @Summary Load System Setting for login
// @Success 200 {string} language // @Success 200 {object} dto.LoginSetting
// @Router /auth/language [get] // @Router /auth/setting [get]
func (b *BaseApi) GetLanguage(c *gin.Context) { func (b *BaseApi) GetAuthSetting(c *gin.Context) {
settingInfo, err := settingService.GetSettingInfo() settingInfo, err := settingService.GetSettingInfo()
if err != nil { if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
helper.SuccessWithData(c, settingInfo.Language) ip := common.GetRealClientIP(c)
needCaptcha := global.IPTracker.NeedCaptcha(ip)
helper.SuccessWithData(c, dto.LoginSetting{
NeedCaptcha: needCaptcha,
Language: settingInfo.Language,
})
} }
func saveLoginLogs(c *gin.Context, err error) { func saveLoginLogs(c *gin.Context, err error) {
+5 -5
View File
@@ -76,7 +76,7 @@ func (b *BaseApi) CheckRepoStatus(c *gin.Context) {
// @Tags Container Image-repo // @Tags Container Image-repo
// @Summary Create image repo // @Summary Create image repo
// @Accept json // @Accept json
// @Param request body dto.ImageRepoDelete true "request" // @Param request body dto.ImageRepoCreate true "request"
// @Produce json // @Produce json
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
@@ -99,20 +99,20 @@ func (b *BaseApi) CreateRepo(c *gin.Context) {
// @Tags Container Image-repo // @Tags Container Image-repo
// @Summary Delete image repo // @Summary Delete image repo
// @Accept json // @Accept json
// @Param request body dto.ImageRepoDelete true "request" // @Param request body dto.OperateByID true "request"
// @Produce json // @Produce json
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /containers/repo/del [post] // @Router /containers/repo/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"image_repos","output_column":"name","output_value":"names"}],"formatZH":"删除镜像仓库 [names]","formatEN":"delete image repo [names]"} // @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"image_repos","output_column":"name","output_value":"name"}],"formatZH":"删除镜像仓库 [name]","formatEN":"delete image repo [name]"}
func (b *BaseApi) DeleteRepo(c *gin.Context) { func (b *BaseApi) DeleteRepo(c *gin.Context) {
var req dto.ImageRepoDelete var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if err := imageRepoService.BatchDelete(req); err != nil { if err := imageRepoService.Delete(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return return
} }
+11 -7
View File
@@ -23,13 +23,12 @@ type MfaCredential struct {
} }
type Login struct { type Login struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Password string `json:"password" validate:"required"` Password string `json:"password" validate:"required"`
IgnoreCaptcha bool `json:"ignoreCaptcha"` Captcha string `json:"captcha"`
Captcha string `json:"captcha"` CaptchaID string `json:"captchaID"`
CaptchaID string `json:"captchaID"` AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"`
AuthMethod string `json:"authMethod" validate:"required,oneof=jwt session"` Language string `json:"language" validate:"required,oneof=zh en tw ja ko ru ms 'pt-BR'"`
Language string `json:"language" validate:"required,oneof=zh en tw ja ko ru ms 'pt-BR'"`
} }
type MFALogin struct { type MFALogin struct {
@@ -38,3 +37,8 @@ type MFALogin struct {
Code string `json:"code" validate:"required"` Code string `json:"code" validate:"required"`
AuthMethod string `json:"authMethod"` AuthMethod string `json:"authMethod"`
} }
type LoginSetting struct {
NeedCaptcha bool `json:"needCaptcha"`
Language string `json:"language"`
}
+4 -4
View File
@@ -6,8 +6,8 @@ type ImageRepoCreate struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
DownloadUrl string `json:"downloadUrl"` DownloadUrl string `json:"downloadUrl"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Username string `json:"username" validate:"max=256"` Username string `json:"username"`
Password string `json:"password" validate:"max=256"` Password string `json:"password"`
Auth bool `json:"auth"` Auth bool `json:"auth"`
} }
@@ -15,8 +15,8 @@ type ImageRepoUpdate struct {
ID uint `json:"id"` ID uint `json:"id"`
DownloadUrl string `json:"downloadUrl"` DownloadUrl string `json:"downloadUrl"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Username string `json:"username" validate:"max=256"` Username string `json:"username"`
Password string `json:"password" validate:"max=256"` Password string `json:"password"`
Auth bool `json:"auth"` Auth bool `json:"auth"`
} }
+6 -5
View File
@@ -92,11 +92,12 @@ type FileWget struct {
} }
type FileMove struct { type FileMove struct {
Type string `json:"type" validate:"required"` Type string `json:"type" validate:"required"`
OldPaths []string `json:"oldPaths" validate:"required"` OldPaths []string `json:"oldPaths" validate:"required"`
NewPath string `json:"newPath" validate:"required"` NewPath string `json:"newPath" validate:"required"`
Name string `json:"name"` Name string `json:"name"`
Cover bool `json:"cover"` Cover bool `json:"cover"`
CoverPaths []string `json:"coverPaths"`
} }
type FileDownload struct { type FileDownload struct {
+1
View File
@@ -53,4 +53,5 @@ type ExistFileInfo struct {
Path string `json:"path"` Path string `json:"path"`
Size float64 `json:"size"` Size float64 `json:"size"`
ModTime time.Time `json:"modTime"` ModTime time.Time `json:"modTime"`
IsDir bool `json:"isDir"`
} }
+31 -6
View File
@@ -442,17 +442,14 @@ func deleteLink(ctx context.Context, install *model.AppInstall, deleteDB bool, f
return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID)) return appInstallResourceRepo.DeleteBy(ctx, appInstallResourceRepo.WithAppInstallId(install.ID))
} }
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) { func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map[string]interface{}, error) {
if detail.DockerCompose == "" {
return "", nil
}
composeMap := make(map[string]interface{}) composeMap := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil { if err := yaml.Unmarshal([]byte(detail.DockerCompose), &composeMap); err != nil {
return "", err return nil, err
} }
value, ok := composeMap["services"] value, ok := composeMap["services"]
if !ok || value == nil { if !ok || value == nil {
return "", buserr.New(constant.ErrFileParse) return nil, buserr.New("ErrFileParse")
} }
servicesMap := value.(map[string]interface{}) servicesMap := value.(map[string]interface{})
if len(servicesMap) == 1 { if len(servicesMap) == 1 {
@@ -470,6 +467,34 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
delete(servicesMap, oldServiceName) delete(servicesMap, oldServiceName)
} }
} }
serviceValue := servicesMap[install.ServiceName].(map[string]interface{})
oldComposeMap := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(install.DockerCompose), &oldComposeMap); err != nil {
return nil, err
}
oldValue, ok := oldComposeMap["services"]
if !ok || oldValue == nil {
return nil, buserr.New("ErrFileParse")
}
oldValueMap := oldValue.(map[string]interface{})
oldServiceValue := oldValueMap[install.ServiceName].(map[string]interface{})
if oldServiceValue["deploy"] != nil {
serviceValue["deploy"] = oldServiceValue["deploy"]
}
servicesMap[install.ServiceName] = serviceValue
composeMap["services"] = servicesMap
return composeMap, nil
}
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
if detail.DockerCompose == "" {
return "", nil
}
composeMap, err := handleUpgradeCompose(install, detail)
if err != nil {
return "", err
}
envs := make(map[string]interface{}) envs := make(map[string]interface{})
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil { if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
return "", err return "", err
+49 -15
View File
@@ -413,9 +413,9 @@ func (u *BackupService) Update(req dto.BackupOperate) error {
if strings.HasSuffix(dirStr, "/") && dirStr != "/" { if strings.HasSuffix(dirStr, "/") && dirStr != "/" {
dirStr = dirStr[:strings.LastIndex(dirStr, "/")] dirStr = dirStr[:strings.LastIndex(dirStr, "/")]
} }
if err := copyDir(oldDir, dirStr); err != nil { if err := changeLocalBackup(oldDir, dirStr); err != nil {
_ = backupRepo.Update(req.ID, map[string]interface{}{"vars": oldVars}) _ = backupRepo.Update(req.ID, map[string]interface{}{"vars": oldVars})
return err return fmt.Errorf("copy dir from %s to %s failed, err: %v", oldDir, dirStr, err)
} }
global.CONF.System.Backup = dirStr global.CONF.System.Backup = dirStr
} }
@@ -518,47 +518,40 @@ func (u *BackupService) loadAccessToken(backup *model.BackupAccount) error {
} }
func (u *BackupService) loadRecordSize(records []model.BackupRecord) ([]dto.BackupFile, error) { func (u *BackupService) loadRecordSize(records []model.BackupRecord) ([]dto.BackupFile, error) {
var datas []dto.BackupFile data := make([]dto.BackupFile, len(records))
clientMap := make(map[string]loadSizeHelper) clientMap := make(map[string]loadSizeHelper)
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < len(records); i++ { for i := 0; i < len(records); i++ {
var item dto.BackupFile data[i].ID = records[i].ID
item.ID = records[i].ID data[i].Name = records[i].FileName
item.Name = records[i].FileName
itemPath := path.Join(records[i].FileDir, records[i].FileName) itemPath := path.Join(records[i].FileDir, records[i].FileName)
if _, ok := clientMap[records[i].Source]; !ok { if _, ok := clientMap[records[i].Source]; !ok {
backup, err := backupRepo.Get(commonRepo.WithByType(records[i].Source)) backup, err := backupRepo.Get(commonRepo.WithByType(records[i].Source))
if err != nil { if err != nil {
global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].Source, err) global.LOG.Errorf("load backup model %s from db failed, err: %v", records[i].Source, err)
clientMap[records[i].Source] = loadSizeHelper{} clientMap[records[i].Source] = loadSizeHelper{}
datas = append(datas, item)
continue continue
} }
client, err := u.NewClient(&backup) client, err := u.NewClient(&backup)
if err != nil { if err != nil {
global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].Source, err) global.LOG.Errorf("load backup client %s from db failed, err: %v", records[i].Source, err)
clientMap[records[i].Source] = loadSizeHelper{} clientMap[records[i].Source] = loadSizeHelper{}
datas = append(datas, item)
continue continue
} }
item.Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath)) data[i].Size, _ = client.Size(path.Join(strings.TrimLeft(backup.BackupPath, "/"), itemPath))
datas = append(datas, item)
clientMap[records[i].Source] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true} clientMap[records[i].Source] = loadSizeHelper{backupPath: strings.TrimLeft(backup.BackupPath, "/"), client: client, isOk: true}
continue continue
} }
if clientMap[records[i].Source].isOk { if clientMap[records[i].Source].isOk {
wg.Add(1) wg.Add(1)
go func(index int) { go func(index int) {
item.Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath)) data[i].Size, _ = clientMap[records[index].Source].client.Size(path.Join(clientMap[records[index].Source].backupPath, itemPath))
datas = append(datas, item)
wg.Done() wg.Done()
}(i) }(i)
} else {
datas = append(datas, item)
} }
} }
wg.Wait() wg.Wait()
return datas, nil return data, nil
} }
func loadLocalDir() (string, error) { func loadLocalDir() (string, error) {
@@ -684,3 +677,44 @@ func (u *BackupService) Run() {
}).Error }).Error
global.LOG.Info("Successfully refreshed OneDrive token.") global.LOG.Info("Successfully refreshed OneDrive token.")
} }
func changeLocalBackup(oldPath, newPath string) error {
fileOp := fileUtils.NewFileOp()
if fileOp.Stat(path.Join(oldPath, "app")) {
if err := fileOp.CopyDir(path.Join(oldPath, "app"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "database")) {
if err := fileOp.CopyDir(path.Join(oldPath, "database"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "directory")) {
if err := fileOp.CopyDir(path.Join(oldPath, "directory"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "system_snapshot")) {
if err := fileOp.CopyDir(path.Join(oldPath, "system_snapshot"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "website")) {
if err := fileOp.CopyDir(path.Join(oldPath, "website"), newPath); err != nil {
return err
}
}
if fileOp.Stat(path.Join(oldPath, "log")) {
if err := fileOp.CopyDir(path.Join(oldPath, "log"), newPath); err != nil {
return err
}
}
_ = fileOp.RmRf(path.Join(oldPath, "app"))
_ = fileOp.RmRf(path.Join(oldPath, "database"))
_ = fileOp.RmRf(path.Join(oldPath, "directory"))
_ = fileOp.RmRf(path.Join(oldPath, "system_snapshot"))
_ = fileOp.RmRf(path.Join(oldPath, "website"))
_ = fileOp.RmRf(path.Join(oldPath, "log"))
return nil
}
+103 -129
View File
@@ -1,7 +1,6 @@
package service package service
import ( import (
"bufio"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -28,14 +27,14 @@ import (
) )
const ( const (
clamServiceNameCentOs = "clamd@scan.service" clamServiceKey = "clam"
clamServiceNameUbuntu = "clamav-daemon.service" freshClamServiceKey = "freshclam"
freshClamService = "clamav-freshclam.service" resultDir = "clamav"
resultDir = "clamav"
) )
type ClamService struct { type ClamService struct {
serviceName string serviceName string
freshClamService string
} }
type IClamService interface { type IClamService interface {
@@ -63,23 +62,32 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
var baseInfo dto.ClamBaseInfo var baseInfo dto.ClamBaseInfo
baseInfo.Version = "-" baseInfo.Version = "-"
baseInfo.FreshVersion = "-" baseInfo.FreshVersion = "-"
exist1, _ := systemctl.IsExist(clamServiceNameCentOs) clamSvc, err := systemctl.GetServiceName(clamServiceKey)
if exist1 { if err != nil {
c.serviceName = clamServiceNameCentOs baseInfo.IsExist = false
baseInfo.IsExist = true return baseInfo, nil
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameCentOs)
} }
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu) c.serviceName = clamSvc
if exist2 { isExist, err := systemctl.IsExist(c.serviceName)
c.serviceName = clamServiceNameUbuntu if err != nil {
baseInfo.IsExist = true baseInfo.IsExist = false
baseInfo.IsActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
} }
freshExist, _ := systemctl.IsExist(freshClamService) baseInfo.IsExist = isExist
if freshExist { baseInfo.IsActive, _ = systemctl.IsActive(clamSvc)
baseInfo.FreshIsExist = true
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshClamService) freshSvc, err := systemctl.GetServiceName(freshClamServiceKey)
if err != nil {
baseInfo.FreshIsExist = false
return baseInfo, nil
} }
c.freshClamService = freshSvc
freshisExist, err := systemctl.IsExist(c.freshClamService)
if err != nil {
baseInfo.FreshIsExist = false
}
baseInfo.FreshIsExist = freshisExist
baseInfo.FreshIsActive, _ = systemctl.IsActive(freshSvc)
if !cmd.Which("clamdscan") { if !cmd.Which("clamdscan") {
baseInfo.IsActive = false baseInfo.IsActive = false
} }
@@ -110,22 +118,27 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
} }
func (c *ClamService) Operate(operate string) error { func (c *ClamService) Operate(operate string) error {
var err error
switch operate { switch operate {
case "start", "restart", "stop": case "start":
stdout, err := cmd.Execf("systemctl %s %s", operate, c.serviceName) err = systemctl.Start(c.serviceName)
if err != nil { case "stop":
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout) err = systemctl.Stop(c.serviceName)
} case "restart":
return nil err = systemctl.Restart(c.serviceName)
case "fresh-start", "fresh-restart", "fresh-stop": case "fresh-start":
stdout, err := cmd.Execf("systemctl %s %s", strings.TrimPrefix(operate, "fresh-"), freshClamService) err = systemctl.Start(c.freshClamService)
if err != nil { case "fresh-stop":
return fmt.Errorf("%s the %s failed, err: %s", operate, c.serviceName, stdout) err = systemctl.Stop(c.freshClamService)
} case "fresh-restart":
return nil err = systemctl.Restart(c.freshClamService)
default: default:
return fmt.Errorf("not support such operation: %v", operate) return fmt.Errorf("unsupported operation: %s", operate)
} }
if err != nil {
return fmt.Errorf("%s %s failed: %v", operate, c.serviceName, err)
}
return nil
} }
func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) { func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interface{}, error) {
@@ -432,102 +445,80 @@ func (c *ClamService) LoadFile(req dto.ClamFileReq) (string, error) {
filePath := "" filePath := ""
switch req.Name { switch req.Name {
case "clamd": case "clamd":
if c.serviceName == clamServiceNameUbuntu { filePath = c.getConfigPath("clamd")
filePath = "/etc/clamav/clamd.conf"
} else {
filePath = "/etc/clamd.d/scan.conf"
}
case "clamd-log": case "clamd-log":
filePath = c.loadLogPath("clamd-log") filePath = c.loadLogPath("clamd-log")
if len(filePath) != 0 {
break
}
if c.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/clamav.log"
} else {
filePath = "/var/log/clamd.scan"
}
case "freshclam": case "freshclam":
if c.serviceName == clamServiceNameUbuntu { filePath = c.getConfigPath("freshclam")
filePath = "/etc/clamav/freshclam.conf"
} else {
filePath = "/etc/freshclam.conf"
}
case "freshclam-log": case "freshclam-log":
filePath = c.loadLogPath("freshclam-log") filePath = c.loadLogPath("freshclam-log")
if len(filePath) != 0 {
break
}
if c.serviceName == clamServiceNameUbuntu {
filePath = "/var/log/clamav/freshclam.log"
} else {
filePath = "/var/log/freshclam.log"
}
default: default:
return "", fmt.Errorf("not support such type") return "", fmt.Errorf("unsupported file type")
} }
if _, err := os.Stat(filePath); err != nil {
content, err := systemctl.ViewConfig(filePath, systemctl.ConfigOption{TailLines: req.Tail})
if err != nil {
return "", buserr.New("ErrHttpReqNotFound") return "", buserr.New("ErrHttpReqNotFound")
} }
var tail string return content, nil
if req.Tail != "0" {
tail = req.Tail
} else {
tail = "+1"
}
cmd := exec.Command("tail", "-n", tail, filePath)
stdout, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("tail -n %v failed, err: %v", req.Tail, err)
}
return string(stdout), nil
} }
func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error { func (c *ClamService) UpdateFile(req dto.UpdateByNameAndFile) error {
filePath := "" var (
service := "" filePath string
service string
)
switch req.Name { switch req.Name {
case "clamd": case "clamd":
if c.serviceName == clamServiceNameUbuntu { filePath = c.getConfigPath("clamd")
service = clamServiceNameUbuntu service = c.serviceName
filePath = "/etc/clamav/clamd.conf"
} else {
service = clamServiceNameCentOs
filePath = "/etc/clamd.d/scan.conf"
}
case "freshclam": case "freshclam":
if c.serviceName == clamServiceNameUbuntu { filePath = c.getConfigPath("freshclam")
filePath = "/etc/clamav/freshclam.conf" service = c.freshClamService
} else {
filePath = "/etc/freshclam.conf"
}
service = "clamav-freshclam.service"
default: default:
return fmt.Errorf("not support such type") return fmt.Errorf("unsupported file type")
} }
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640) file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0640)
if err != nil { if err != nil {
return err return err
} }
defer file.Close() defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.WriteString(req.File)
write.Flush()
_ = systemctl.Restart(service) if _, err := file.WriteString(req.File); err != nil {
return err
}
if err := systemctl.Restart(service); err != nil {
return fmt.Errorf("restart %s failed: %v", service, err)
}
return nil return nil
} }
func (c *ClamService) getConfigPath(confType string) string {
switch confType {
case "clamd":
if _, err := os.Stat("/etc/clamav/clamd.conf"); err == nil {
return "/etc/clamav/clamd.conf"
}
return "/etc/clamd.d/scan.conf"
case "freshclam":
if _, err := os.Stat("/etc/clamav/freshclam.conf"); err == nil {
return "/etc/clamav/freshclam.conf"
}
return "/etc/freshclam.conf"
default:
return ""
}
}
func StopAllCronJob(withCheck bool) bool { func StopAllCronJob(withCheck bool) bool {
if withCheck { if withCheck {
isActive := false isActive := false
exist1, _ := systemctl.IsExist(clamServiceNameCentOs) isexist, _ := systemctl.IsExist(clamServiceKey)
if exist1 { if isexist {
isActive, _ = systemctl.IsActive(clamServiceNameCentOs) isActive, _ = systemctl.IsActive(clamServiceKey)
}
exist2, _ := systemctl.IsExist(clamServiceNameUbuntu)
if exist2 {
isActive, _ = systemctl.IsActive(clamServiceNameUbuntu)
} }
if isActive { if isActive {
return false return false
@@ -590,42 +581,25 @@ func loadResultFromLog(pathItem string) dto.ClamLog {
return data return data
} }
func (c *ClamService) loadLogPath(name string) string { func (c *ClamService) loadLogPath(name string) string {
confPath := "" configKey := "clamd"
if name == "clamd-log" { searchPrefix := "LogFile "
if c.serviceName == clamServiceNameUbuntu { if name != "clamd-log" {
confPath = "/etc/clamav/clamd.conf" configKey = "freshclam"
} else { searchPrefix = "UpdateLogFile "
confPath = "/etc/clamd.d/scan.conf"
}
} else {
if c.serviceName == clamServiceNameUbuntu {
confPath = "/etc/clamav/freshclam.conf"
} else {
confPath = "/etc/freshclam.conf"
}
}
if _, err := os.Stat(confPath); err != nil {
return ""
} }
confPath := c.getConfigPath(configKey)
content, err := os.ReadFile(confPath) content, err := os.ReadFile(confPath)
if err != nil { if err != nil {
global.LOG.Debugf("Failed to read %s config: %v", configKey, err)
return "" return ""
} }
lines := strings.Split(string(content), "\n")
if name == "clamd-log" { for _, line := range strings.Split(string(content), "\n") {
for _, line := range lines { if strings.HasPrefix(line, searchPrefix) {
if strings.HasPrefix(line, "LogFile ") { return strings.TrimSpace(strings.TrimPrefix(line, searchPrefix))
return strings.Trim(strings.ReplaceAll(line, "LogFile ", ""), " ")
}
}
} else {
for _, line := range lines {
if strings.HasPrefix(line, "UpdateLogFile ") {
return strings.Trim(strings.ReplaceAll(line, "UpdateLogFile ", ""), " ")
}
} }
} }
return "" return ""
} }
+2 -8
View File
@@ -558,10 +558,7 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
hostConf := oldContainer.HostConfig hostConf := oldContainer.HostConfig
var networkConf network.NetworkingConfig var networkConf network.NetworkingConfig
if oldContainer.NetworkSettings != nil { if oldContainer.NetworkSettings != nil {
for networkKey := range oldContainer.NetworkSettings.Networks { networkConf.EndpointsConfig = oldContainer.NetworkSettings.Networks
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
} }
if err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}); err != nil { if err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}); err != nil {
return err return err
@@ -1215,10 +1212,7 @@ func reCreateAfterUpdate(name string, client *client.Client, config *container.C
var oldNetworkConf network.NetworkingConfig var oldNetworkConf network.NetworkingConfig
if networkConf != nil { if networkConf != nil {
for networkKey := range networkConf.Networks { oldNetworkConf.EndpointsConfig = networkConf.Networks
oldNetworkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
} }
oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name) oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name)
+2 -1
View File
@@ -18,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/ntp" "github.com/1Panel-dev/1Panel/backend/utils/ntp"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v3/mem"
) )
@@ -121,7 +122,7 @@ func (u *DeviceService) Update(key, value string) error {
return err return err
} }
go func() { go func() {
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system for new time zone failed, err: %v", err) global.LOG.Errorf("restart system for new time zone failed, err: %v", err)
} }
+2 -2
View File
@@ -11,12 +11,12 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/docker" "github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/filters"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
fileUtils "github.com/1Panel-dev/1Panel/backend/utils/files" fileUtils "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/google/uuid" "github.com/google/uuid"
@@ -285,7 +285,7 @@ func (u *DeviceService) Clean(req []dto.Clean) {
if restart { if restart {
go func() { go func() {
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system port failed, err: %v", err) global.LOG.Errorf("restart system port failed, err: %v", err)
} }
+30 -26
View File
@@ -365,17 +365,18 @@ func (u *DockerService) UpdateConfByFile(req dto.DaemonJsonUpdateByFile) error {
func (u *DockerService) OperateDocker(req dto.DockerOperation) error { func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
service := "docker" service := "docker"
sudo := cmd.SudoHandleCmd() h, err := systemctl.DefaultHandler(service)
dockerCmd, err := getDockerRestartCommand()
if err != nil { if err != nil {
return err return err
} }
if req.Operation == "stop" { if req.Operation == "stop" {
isSocketActive, _ := systemctl.IsActive("docker.socket") socketHandle, err := systemctl.DefaultHandler("docker.socket")
if isSocketActive { if err == nil {
std, err := cmd.Execf("%s systemctl stop docker.socket", sudo) status, err := socketHandle.IsActive()
if err != nil { if err == nil && status.IsActive {
global.LOG.Errorf("handle systemctl stop docker.socket failed, err: %v", std) if std, err := socketHandle.ExecuteAction("stop"); err != nil {
global.LOG.Errorf("handle stop docker.socket failed, err: %v", std)
}
} }
} }
} }
@@ -386,13 +387,20 @@ func (u *DockerService) OperateDocker(req dto.DockerOperation) error {
} }
} }
stdout, err := cmd.Execf("%s %s %s", dockerCmd, req.Operation, service) if isDockerSnapInstalled() {
command := fmt.Sprintf("snap %s docker", req.Operation)
stdout, err := cmd.Exec(command)
if err != nil {
return fmt.Errorf("failed to restart docker: %v", stdout)
}
return nil
}
result, err := h.ExecuteAction(req.Operation)
if err != nil { if err != nil {
return errors.New(stdout) return errors.New(result.Output)
} }
return nil return nil
} }
func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize string) { func changeLogOption(daemonMap map[string]interface{}, logMaxFile, logMaxSize string) {
if opts, ok := daemonMap["log-opts"]; ok { if opts, ok := daemonMap["log-opts"]; ok {
if len(logMaxFile) != 0 || len(logMaxSize) != 0 { if len(logMaxFile) != 0 || len(logMaxSize) != 0 {
@@ -451,31 +459,27 @@ func validateDockerConfig() error {
return nil return nil
} }
if err != nil || (stdout != "" && strings.TrimSpace(stdout) != "configuration OK") { if err != nil || (stdout != "" && strings.TrimSpace(stdout) != "configuration OK") {
return fmt.Errorf("Docker configuration validation failed, err: %v", stdout) return fmt.Errorf("docker configuration validation failed, err: %v", stdout)
} }
return nil return nil
} }
func getDockerRestartCommand() (string, error) { func isDockerSnapInstalled() bool {
stdout, err := cmd.Exec("which docker") stdout, err := cmd.Exec("which docker")
if err != nil { if err != nil {
return "", fmt.Errorf("failed to find docker: %v", err) return false
} }
dockerPath := stdout stdout = strings.TrimSpace(stdout)
if strings.Contains(dockerPath, "snap") { return strings.Contains(stdout, "snap")
return "snap", nil
}
return "systemctl", nil
} }
func restartDocker() error { func restartDocker() error {
restartCmd, err := getDockerRestartCommand() if isDockerSnapInstalled() {
if err != nil { stdout, err := cmd.Exec("snap restart docker")
return err if err != nil {
return fmt.Errorf("failed to restart docker: %v", stdout)
}
return nil
} }
stdout, err := cmd.Execf("%s restart docker", restartCmd) return systemctl.Restart("docker")
if err != nil {
return fmt.Errorf("failed to restart Docker: %s", stdout)
}
return nil
} }
+10 -1
View File
@@ -366,10 +366,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
return buserr.New(constant.ErrMovePathFailed) return buserr.New(constant.ErrMovePathFailed)
} }
} }
var errs []error
if m.Type == "cut" { if m.Type == "cut" {
if len(m.CoverPaths) > 0 {
for _, src := range m.CoverPaths {
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
errs = append(errs, err)
global.LOG.Errorf("cut copy file [%s] to [%s] failed, err: %s", src, m.NewPath, err.Error())
}
}
}
return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover) return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover)
} }
var errs []error
if m.Type == "copy" { if m.Type == "copy" {
for _, src := range m.OldPaths { for _, src := range m.OldPaths {
if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil { if err := fo.CopyAndReName(src, m.NewPath, m.Name, m.Cover); err != nil {
@@ -518,6 +526,7 @@ func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.Exi
Name: info.Name(), Name: info.Name(),
Path: filePath, Path: filePath,
ModTime: info.ModTime(), ModTime: info.ModTime(),
IsDir: info.IsDir(),
}) })
} }
} }
+119 -89
View File
@@ -3,6 +3,12 @@ package service
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/backend/app/dto/request" "github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto/response" "github.com/1Panel-dev/1Panel/backend/app/dto/response"
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
@@ -14,11 +20,6 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/systemctl" "github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/ini.v1" "gopkg.in/ini.v1"
"os/exec"
"os/user"
"path"
"strconv"
"strings"
) )
type HostToolService struct{} type HostToolService struct{}
@@ -39,93 +40,108 @@ func NewIHostToolService() IHostToolService {
} }
func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) { func (h *HostToolService) GetToolStatus(req request.HostToolReq) (*response.HostToolRes, error) {
res := &response.HostToolRes{} res := &response.HostToolRes{Type: req.Type}
res.Type = req.Type
switch req.Type { switch req.Type {
case constant.Supervisord: case constant.Supervisord:
supervisorConfig := &response.Supervisor{} return h.getSupervisorStatus(res)
if !cmd.Which(constant.Supervisord) {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.IsExist = true
serviceExist, _ := systemctl.IsExist(constant.Supervisord)
if !serviceExist {
serviceExist, _ = systemctl.IsExist(constant.Supervisor)
if !serviceExist {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
} else {
supervisorConfig.ServiceName = constant.Supervisor
}
} else {
supervisorConfig.ServiceName = constant.Supervisord
}
serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName))
if serviceNameSet.ID != 0 || serviceNameSet.Value != "" {
supervisorConfig.ServiceName = serviceNameSet.Value
}
versionRes, _ := cmd.Exec("supervisord -v")
supervisorConfig.Version = strings.TrimSuffix(versionRes, "\n")
_, ctlRrr := exec.LookPath("supervisorctl")
supervisorConfig.CtlExist = ctlRrr == nil
active, _ := systemctl.IsActive(supervisorConfig.ServiceName)
if active {
supervisorConfig.Status = "running"
} else {
supervisorConfig.Status = "stopped"
}
pathSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath))
if pathSet.ID != 0 || pathSet.Value != "" {
supervisorConfig.ConfigPath = pathSet.Value
res.Config = supervisorConfig
return res, nil
} else {
supervisorConfig.Init = true
}
servicePath := "/usr/lib/systemd/system/supervisor.service"
fileOp := files.NewFileOp()
if !fileOp.Stat(servicePath) {
servicePath = "/usr/lib/systemd/system/supervisord.service"
}
if fileOp.Stat(servicePath) {
startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart")
if startCmd != "" {
args := strings.Fields(startCmd)
cIndex := -1
for i, arg := range args {
if arg == "-c" {
cIndex = i
break
}
}
if cIndex != -1 && cIndex+1 < len(args) {
supervisorConfig.ConfigPath = args[cIndex+1]
}
}
}
if supervisorConfig.ConfigPath == "" {
configPath := "/etc/supervisord.conf"
if !fileOp.Stat(configPath) {
configPath = "/etc/supervisor/supervisord.conf"
if fileOp.Stat(configPath) {
supervisorConfig.ConfigPath = configPath
}
}
}
res.Config = supervisorConfig
} }
return res, nil return res, nil
} }
func (h *HostToolService) getSupervisorStatus(res *response.HostToolRes) (*response.HostToolRes, error) {
supervisorConfig := &response.Supervisor{}
if !cmd.Which(constant.Supervisord) {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.IsExist = true
serviceName, err := h.determineServiceName()
if err != nil || serviceName == "" {
supervisorConfig.IsExist = false
res.Config = supervisorConfig
return res, nil
}
supervisorConfig.ServiceName = serviceName
if nameSetting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)); nameSetting.Value != "" {
supervisorConfig.ServiceName = nameSetting.Value
}
if version, err := cmd.Exec("supervisord -v"); err == nil {
supervisorConfig.Version = strings.TrimSpace(version)
}
_, errCtl := exec.LookPath("supervisorctl")
supervisorConfig.CtlExist = errCtl == nil
if active, err := systemctl.IsActive(supervisorConfig.ServiceName); err == nil && active {
supervisorConfig.Status = "running"
} else {
supervisorConfig.Status = "stopped"
}
h.resolveConfigPath(supervisorConfig)
res.Config = supervisorConfig
return res, nil
}
func (h *HostToolService) determineServiceName() (string, error) {
if setting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)); setting.Value != "" {
serviceName, err := systemctl.GetServiceName(setting.Value)
if err != nil {
return "", fmt.Errorf("get service name error: %v", err)
}
return serviceName, nil
}
if serviceName, err := systemctl.GetServiceName(constant.Supervisord); err == nil {
return serviceName, nil
}
return systemctl.GetServiceName(constant.Supervisor)
}
func (h *HostToolService) resolveConfigPath(config *response.Supervisor) {
if pathSetting, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorConfigPath)); pathSetting.Value != "" {
config.ConfigPath = pathSetting.Value
return
}
config.Init = true
if servicePath, err := systemctl.GetServicePath(config.ServiceName); err == nil {
if startCmd, _ := ini_conf.GetIniValue(servicePath, "Service", "ExecStart"); startCmd != "" {
if path := parseConfigPathFromCommand(startCmd); path != "" {
config.ConfigPath = path
return
}
}
}
defaultPaths := []string{
"/etc/supervisord.conf",
"/etc/supervisor/supervisord.conf",
"/usr/local/etc/supervisord.conf",
}
for _, path := range defaultPaths {
if systemctl.FileExist(path) {
config.ConfigPath = path
return
}
}
}
func parseConfigPathFromCommand(cmd string) string {
parts := strings.Fields(cmd)
for i, part := range parts {
if part == "-c" && i+1 < len(parts) {
return parts[i+1]
}
}
return ""
}
func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error { func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error {
switch req.Type { switch req.Type {
case constant.Supervisord: case constant.Supervisord:
@@ -200,14 +216,28 @@ func (h *HostToolService) CreateToolConfig(req request.HostToolCreate) error {
} }
func (h *HostToolService) OperateTool(req request.HostToolReq) error { func (h *HostToolService) OperateTool(req request.HostToolReq) error {
serviceName := req.Type var serviceName string
if req.Type == constant.Supervisord { var err error
switch req.Type {
case constant.Supervisord:
serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName)) serviceNameSet, _ := settingRepo.Get(settingRepo.WithByKey(constant.SupervisorServiceName))
if serviceNameSet.ID != 0 || serviceNameSet.Value != "" { if serviceNameSet.ID != 0 || serviceNameSet.Value != "" {
serviceName = serviceNameSet.Value serviceName = serviceNameSet.Value
} else {
serviceName = req.Type
} }
default:
serviceName = req.Type
} }
return systemctl.Operate(req.Operate, serviceName) serviceName, err = systemctl.GetServiceName(serviceName)
if err != nil {
if errors.Is(err, systemctl.ErrServiceNotFound) {
return fmt.Errorf("%s service unavailable. Please ensure supervisor server is installed and configured", serviceName)
}
return err
}
_, err = systemctl.CustomAction(req.Operate, serviceName)
return err
} }
func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) { func (h *HostToolService) OperateToolConfig(req request.HostToolConfig) (*response.HostToolConfig, error) {
+73 -47
View File
@@ -26,7 +26,7 @@ type IImageRepoService interface {
Login(req dto.OperateByID) error Login(req dto.OperateByID) error
Create(req dto.ImageRepoCreate) error Create(req dto.ImageRepoCreate) error
Update(req dto.ImageRepoUpdate) error Update(req dto.ImageRepoUpdate) error
BatchDelete(req dto.ImageRepoDelete) error Delete(req dto.OperateByID) error
} }
func NewIImageRepoService() IImageRepoService { func NewIImageRepoService() IImageRepoService {
@@ -89,31 +89,7 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil { if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err) return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
} }
if err := validateDockerConfig(); err != nil { if err := stopBeforeUpdateRepo(); err != nil {
return err
}
if err := restartDocker(); err != nil {
return err
}
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
if err := func() error {
for range ticker.C {
select {
case <-ctx.Done():
cancel()
return errors.New("the docker service cannot be restarted")
default:
stdout, err := cmd.Exec("systemctl is-active docker")
if string(stdout) == "active\n" && err == nil {
global.LOG.Info("docker restart with new conf successful!")
return nil
}
}
}
return nil
}(); err != nil {
return err return err
} }
} }
@@ -131,15 +107,32 @@ func (u *ImageRepoService) Create(req dto.ImageRepoCreate) error {
return imageRepoRepo.Create(&imageRepo) return imageRepoRepo.Create(&imageRepo)
} }
func (u *ImageRepoService) BatchDelete(req dto.ImageRepoDelete) error { func (u *ImageRepoService) Delete(req dto.OperateByID) error {
for _, id := range req.Ids { if req.ID == 1 {
if id == 1 { return errors.New("The default value cannot be edit !")
return errors.New("The default value cannot be edit !")
}
} }
if err := imageRepoRepo.Delete(commonRepo.WithIdsIn(req.Ids)); err != nil { itemRepo, _ := imageRepoRepo.Get(commonRepo.WithByID(req.ID))
if itemRepo.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
if itemRepo.Auth {
_, _ = cmd.Execf("docker logout -i %s", itemRepo.DownloadUrl)
}
if itemRepo.Protocol == "https" {
return imageRepoRepo.Delete(commonRepo.WithByID(req.ID))
}
if err := u.handleRegistries("", itemRepo.DownloadUrl, "delete"); err != nil {
return fmt.Errorf("delete registry %s failed, err: %v", itemRepo.DownloadUrl, err)
}
if err := validateDockerConfig(); err != nil {
return err return err
} }
if err := imageRepoRepo.Delete(commonRepo.WithByID(req.ID)); err != nil {
return err
}
go func() {
_ = restartDocker()
}()
return nil return nil
} }
@@ -154,37 +147,39 @@ func (u *ImageRepoService) Update(req dto.ImageRepoUpdate) error {
if err != nil { if err != nil {
return err return err
} }
needRestart := false
if repo.Protocol == "http" && req.Protocol == "https" { if repo.Protocol == "http" && req.Protocol == "https" {
if err := u.handleRegistries("", repo.DownloadUrl, "delete"); err != nil { if err := u.handleRegistries("", repo.DownloadUrl, "delete"); err != nil {
return fmt.Errorf("delete registry %s failed, err: %v", repo.DownloadUrl, err) return fmt.Errorf("delete registry %s failed, err: %v", repo.DownloadUrl, err)
} }
needRestart = true
} }
if repo.Protocol == "http" && req.Protocol == "http" { if repo.Protocol == "http" && req.Protocol == "http" {
if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil { if repo.DownloadUrl != req.DownloadUrl {
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err) if err := u.handleRegistries(req.DownloadUrl, repo.DownloadUrl, "update"); err != nil {
return fmt.Errorf("update registry %s => %s failed, err: %v", repo.DownloadUrl, req.DownloadUrl, err)
}
needRestart = true
} }
} }
if repo.Protocol == "https" && req.Protocol == "http" { if repo.Protocol == "https" && req.Protocol == "http" {
if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil { if err := u.handleRegistries(req.DownloadUrl, "", "create"); err != nil {
return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err) return fmt.Errorf("create registry %s failed, err: %v", req.DownloadUrl, err)
} }
needRestart = true
} }
if repo.Auth != req.Auth || repo.DownloadUrl != req.DownloadUrl { if needRestart {
if repo.Auth { if err := stopBeforeUpdateRepo(); err != nil {
_, _ = cmd.ExecWithCheck("docker", "logout", repo.DownloadUrl) return err
}
if req.Auth {
if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
return err
}
} }
} }
if repo.Auth {
if err := validateDockerConfig(); err != nil { _, _ = cmd.Execf("docker logout -i %s", repo.DownloadUrl)
return err
} }
if err := restartDocker(); err != nil { if req.Auth {
return err if err := u.CheckConn(req.DownloadUrl, req.Username, req.Password); err != nil {
return err
}
} }
upMap := make(map[string]interface{}) upMap := make(map[string]interface{})
@@ -256,3 +251,34 @@ func (u *ImageRepoService) handleRegistries(newHost, delHost, handle string) err
} }
return nil return nil
} }
func stopBeforeUpdateRepo() error {
if err := validateDockerConfig(); err != nil {
return err
}
if err := restartDocker(); err != nil {
return err
}
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
if err := func() error {
for range ticker.C {
select {
case <-ctx.Done():
cancel()
return errors.New("the docker service cannot be restarted")
default:
stdout, err := cmd.Exec("systemctl is-active docker")
if string(stdout) == "active\n" && err == nil {
global.LOG.Info("docker restart with new conf successful!")
return nil
}
}
}
return nil
}(); err != nil {
return err
}
return nil
}
+2 -2
View File
@@ -430,7 +430,7 @@ func addProxy(server *model.McpServer) {
if !ok { if !ok {
return return
} }
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.1:%d%s", server.Port, server.SsePath)}) location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
location.ChangePath("^~", server.SsePath) location.ChangePath("^~", server.SsePath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
global.LOG.Errorf("write config failed, err: %v", buserr.WithErr(constant.ErrUpdateBuWebsite, err)) global.LOG.Errorf("write config failed, err: %v", buserr.WithErr(constant.ErrUpdateBuWebsite, err))
@@ -486,7 +486,7 @@ func addMCPProxy(websiteID uint) error {
err = errors.New("error") err = errors.New("error")
return err return err
} }
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.1:%d%s", server.Port, server.SsePath)}) location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, server.SsePath)})
location.ChangePath("^~", server.SsePath) location.ChangePath("^~", server.SsePath)
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
return buserr.WithErr(constant.ErrUpdateBuWebsite, err) return buserr.WithErr(constant.ErrUpdateBuWebsite, err)
+6 -6
View File
@@ -21,10 +21,10 @@ import (
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/encrypt" "github.com/1Panel-dev/1Panel/backend/utils/encrypt"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
) )
@@ -166,7 +166,7 @@ func (u *SettingService) UpdateBindInfo(req dto.BindInfo) error {
} }
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system with new bind info failed, err: %v", err) global.LOG.Errorf("restart system with new bind info failed, err: %v", err)
} }
@@ -214,7 +214,7 @@ func (u *SettingService) UpdatePort(port uint) error {
} }
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system port failed, err: %v", err) global.LOG.Errorf("restart system port failed, err: %v", err)
} }
@@ -237,7 +237,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
c.SetCookie(constant.SessionName, sID, 0, "", "", false, true) c.SetCookie(constant.SessionName, sID, 0, "", "", false, true)
go func() { go func() {
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system failed, err: %v", err) global.LOG.Errorf("restart system failed, err: %v", err)
} }
@@ -335,7 +335,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
c.SetCookie(constant.SessionName, sID, 0, "", "", true, true) c.SetCookie(constant.SessionName, sID, 0, "", "", true, true)
go func() { go func() {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
_, err := cmd.Exec("systemctl restart 1panel.service") err := systemctl.Restart("1panel")
if err != nil { if err != nil {
global.LOG.Errorf("restart system failed, err: %v", err) global.LOG.Errorf("restart system failed, err: %v", err)
} }
@@ -552,7 +552,7 @@ func (u *SettingService) GenerateRSAKey() error {
return err return err
} }
privateKeyPEM := exportPrivateKeyToPEM(privateKey) privateKeyPEM := exportPrivateKeyToPEM(privateKey)
publicKeyPEM, err := exportPublicKeyToPEM(&privateKey.PublicKey) publicKeyPEM, _ := exportPublicKeyToPEM(&privateKey.PublicKey)
err = settingRepo.UpdateOrCreate("PASSWORD_PRIVATE_KEY", privateKeyPEM) err = settingRepo.UpdateOrCreate("PASSWORD_PRIVATE_KEY", privateKeyPEM)
if err != nil { if err != nil {
return err return err
+2 -1
View File
@@ -503,7 +503,8 @@ func hasOs(name string) bool {
strings.Contains(name, "arm64") || strings.Contains(name, "arm64") ||
strings.Contains(name, "armv7") || strings.Contains(name, "armv7") ||
strings.Contains(name, "ppc64le") || strings.Contains(name, "ppc64le") ||
strings.Contains(name, "s390x") strings.Contains(name, "s390x") ||
strings.Contains(name, "riscv64")
} }
func loadOs() string { func loadOs() string {
+31 -7
View File
@@ -17,6 +17,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
) )
type snapHelper struct { type snapHelper struct {
@@ -43,16 +44,39 @@ func snapPanel(snap snapHelper, targetDir string) {
defer snap.Wg.Done() defer snap.Wg.Done()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running}) _ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": constant.Running})
status := constant.StatusDone status := constant.StatusDone
if err := common.CopyFile("/usr/local/bin/1panel", path.Join(targetDir, "1panel")); err != nil { h, err := systemctl.DefaultHandler("1panel")
status = err.Error()
if err != nil {
status = fmt.Sprintf("initialize service handle failed: %v", err)
snap.Status.Panel = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
return
} }
if err := common.CopyFile("/usr/local/bin/1pctl", targetDir); err != nil { binDir := systemctl.BinaryPath
servicePath, err := h.GetServicePath()
if err != nil {
status = fmt.Sprintf("get service path failed: %v", err)
snap.Status.Panel = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel": status})
return
}
if err := common.CopyFile(path.Join(binDir, "1panel"), path.Join(targetDir, "1panel")); err != nil {
status = err.Error() status = err.Error()
} }
_, _ = cmd.Execf("cp -r /usr/local/bin/lang %s", targetDir) if err := common.CopyFile(path.Join(binDir, "1pctl"), targetDir); err != nil {
status = err.Error()
if err := common.CopyFile("/etc/systemd/system/1panel.service", targetDir); err != nil { }
if _, err := cmd.Execf("cp -r %s/lang %s", binDir, targetDir); err != nil {
status = err.Error()
}
initScriptDir := path.Join(constant.DataDir, "initscript")
if err := common.CopyFile(servicePath, initScriptDir); err != nil {
status = err.Error()
}
global.LOG.Debugf("from %s copy init script to %s", initScriptDir, path.Join(targetDir, "initscript"))
if err := common.CopyDirs(initScriptDir, path.Join(targetDir, "initscript")); err != nil { // copy init script to targetDir
status = err.Error() status = err.Error()
} }
snap.Status.Panel = status snap.Status.Panel = status
@@ -260,7 +284,7 @@ func handleSnapTar(sourceDir, targetDir, name, exclusionRules string, secret str
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -print) -zcf %s %s %s %s", sourceDir, " -"+exStr, path, extraCmd, targetDir+"/"+name) commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -print) -zcf %s %s %s %s", sourceDir, " -"+exStr, path, extraCmd, targetDir+"/"+name)
global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******")) global.LOG.Debug(strings.ReplaceAll(commands, fmt.Sprintf(" %s ", secret), "******"))
} else { } else {
commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%s' | sed 's|^|./|') -zcf %s %s -C %s .", sourceDir, "%P\n", targetDir+"/"+name, exStr, sourceDir) commands = fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read --exclude-from=<(find %s -type s -printf '%%P\n' | sed 's|^|./|') -zcf %s %s -C %s .", sourceDir, targetDir+"/"+name, exStr, sourceDir)
global.LOG.Debug(commands) global.LOG.Debug(commands)
} }
stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute) stdout, err := cmd.ExecWithTimeOut(commands, 30*time.Minute)
+34 -5
View File
@@ -15,6 +15,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@@ -91,8 +92,15 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
req.IsNew = true req.IsNew = true
} }
h, err := systemctl.DefaultHandler("1panel")
if err != nil {
updateRecoverStatus(snap.ID, isRecover, "ServiceHandle", constant.StatusFailed, fmt.Sprintf("initialize service handle failed: %v", err))
return
}
if req.IsNew || snap.InterruptStep == "1PanelBinary" { if req.IsNew || snap.InterruptStep == "1PanelBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), "/usr/local/bin"); err != nil { binDir := systemctl.BinaryPath
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), binDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error()) updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error())
return return
} }
@@ -100,16 +108,32 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
req.IsNew = true req.IsNew = true
} }
if req.IsNew || snap.InterruptStep == "1PctlBinary" { if req.IsNew || snap.InterruptStep == "1PctlBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), "/usr/local/bin"); err != nil { binDir := systemctl.BinaryPath
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), binDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error()) updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error())
return return
} }
_, _ = cmd.Execf("cp -r %s %s", path.Join(snapFileDir, "1panel/lang"), "/usr/local/bin/") langDir := path.Join(binDir, "lang")
if err := os.RemoveAll(langDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "RemoveLang", constant.StatusFailed, fmt.Sprintf("remove lang dir failed: %v", err))
return
}
if err := common.CopyDirs(path.Join(snapFileDir, "1panel/lang"), langDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "CopyLang", constant.StatusFailed, fmt.Sprintf("copy lang files failed: %v", err))
return
}
global.LOG.Debug("recover 1pctl from snapshot file successful!") global.LOG.Debug("recover 1pctl from snapshot file successful!")
req.IsNew = true req.IsNew = true
} }
if req.IsNew || snap.InterruptStep == "1PanelService" { if req.IsNew || snap.InterruptStep == "1PanelService" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel.service"), "/etc/systemd/system"); err != nil { servicePath, err := h.GetServicePath()
currentServiceName := h.GetServiceName()
if err != nil {
updateRecoverStatus(snap.ID, isRecover, "GetServicePath", constant.StatusFailed, fmt.Sprintf("get service path failed: %v", err))
return
}
global.LOG.Debugf("current service path: %s", servicePath)
if err := common.CopyFile(selectInitScript(path.Join(snapFileDir, "1panel/initscript"), currentServiceName), servicePath); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error()) updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error())
return return
} }
@@ -147,7 +171,12 @@ func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover b
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir)) global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
_ = os.RemoveAll(path.Dir(snapFileDir)) _ = os.RemoveAll(path.Dir(snapFileDir))
} }
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service") if h.ManagerName() == "systemd" {
_, _ = cmd.Exec("systemctl daemon-reload")
}
if err := systemctl.Restart("1panel"); err != nil {
global.LOG.Errorf("restart 1panel service failed: %v", err)
}
} }
func backupBeforeRecover(snap model.Snapshot) error { func backupBeforeRecover(snap model.Snapshot) error {
+214 -162
View File
@@ -2,8 +2,7 @@ package service
import ( import (
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/backend/utils/geo" "net"
"github.com/gin-gonic/gin"
"os" "os"
"os/user" "os/user"
"path" "path"
@@ -12,6 +11,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/1Panel-dev/1Panel/backend/utils/geo"
"github.com/gin-gonic/gin"
"github.com/1Panel-dev/1Panel/backend/app/dto" "github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr" "github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
@@ -59,26 +61,16 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
if err != nil { if err != nil {
data.Status = constant.StatusDisable data.Status = constant.StatusDisable
data.Message = err.Error() data.Message = err.Error()
} else {
active, err := systemctl.IsActive(serviceName)
if !active {
data.Status = constant.StatusDisable
if err != nil {
data.Message = err.Error()
}
} else {
data.Status = constant.StatusEnable
}
} }
isEnabled, _ := systemctl.IsEnable(serviceName)
out, err := systemctl.RunSystemCtl("is-enabled", serviceName) data.AutoStart = isEnabled
if err != nil { isActive, err := systemctl.IsActive(serviceName)
data.AutoStart = false if isActive {
data.Status = constant.StatusEnable
} else { } else {
if out == "alias\n" { data.Status = constant.StatusDisable
data.AutoStart, _ = systemctl.IsEnable("ssh") if err != nil {
} else { data.Message = err.Error()
data.AutoStart = out == "enabled\n"
} }
} }
@@ -121,29 +113,26 @@ func (u *SSHService) OperateSSH(operation string) error {
if err != nil { if err != nil {
return err return err
} }
sudo := cmd.SudoHandleCmd() switch operation {
if operation == "enable" || operation == "disable" { case "enable":
serviceName += ".service" return systemctl.Enable(serviceName)
} case "disable":
if operation == "stop" { return systemctl.Disable(serviceName)
case "start":
return systemctl.Start(serviceName)
case "stop":
isSocketActive, _ := systemctl.IsActive(serviceName + ".socket") isSocketActive, _ := systemctl.IsActive(serviceName + ".socket")
if isSocketActive { if isSocketActive {
std, err := cmd.Execf("%s systemctl stop %s", sudo, serviceName+".socket") if err := systemctl.Stop(serviceName + ".socket"); err != nil {
if err != nil { global.LOG.Errorf("Failed to stop %s.socket: %v", serviceName, err)
global.LOG.Errorf("handle systemctl stop %s.socket failed, err: %v", serviceName, std)
} }
} }
return systemctl.Stop(serviceName)
case "restart":
return systemctl.Restart(serviceName)
} }
if result, err := systemctl.CustomAction(operation, serviceName); err != nil {
stdout, err := cmd.Execf("%s systemctl %s %s", sudo, operation, serviceName) return fmt.Errorf("failed to execute custom action: %v", result.Output)
if err != nil {
if strings.Contains(stdout, "alias name or linked unit file") {
stdout, err := cmd.Execf("%s systemctl %s ssh", sudo, operation)
if err != nil {
return fmt.Errorf("%s ssh(alias name or linked unit file) failed, stdout: %s, err: %v", operation, stdout, err)
}
}
return fmt.Errorf("%s %s failed, stdout: %s, err: %v", operation, serviceName, stdout, err)
} }
return nil return nil
} }
@@ -198,7 +187,10 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
} }
} }
_, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) err = systemctl.Restart(serviceName)
if err != nil {
global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err)
}
return nil return nil
} }
@@ -216,8 +208,10 @@ func (u *SSHService) UpdateByFile(value string) error {
if _, err = file.WriteString(value); err != nil { if _, err = file.WriteString(value); err != nil {
return err return err
} }
sudo := cmd.SudoHandleCmd() err = systemctl.Restart(serviceName)
_, _ = cmd.Execf("%s systemctl restart %s", sudo, serviceName) if err != nil {
global.LOG.Errorf("handle restart %s failed, err: %v", serviceName, err)
}
return nil return nil
} }
@@ -298,7 +292,7 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
if err != nil { if err != nil {
return err return err
} }
if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth")) { if !info.IsDir() && (strings.HasPrefix(info.Name(), "secure") || strings.HasPrefix(info.Name(), "auth") || strings.HasPrefix(info.Name(), "messages")) {
if !strings.HasSuffix(info.Name(), ".gz") { if !strings.HasSuffix(info.Name(), ".gz") {
fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()}) fileList = append(fileList, sshFileItem{Name: pathItem, Year: info.ModTime().Year()})
return nil return nil
@@ -326,7 +320,8 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd()) nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
for _, file := range fileList { for _, file := range fileList {
commandItem := "" commandItem := ""
if strings.HasPrefix(path.Base(file.Name), "secure") { switch {
case strings.HasPrefix(path.Base(file.Name), "secure"):
switch req.Status { switch req.Status {
case constant.StatusSuccess: case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command)
@@ -335,8 +330,16 @@ func (u *SSHService) LoadLog(c *gin.Context, req dto.SearchSSHLog) (*dto.SSHLog,
default: default:
commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command) commandItem = fmt.Sprintf("cat %s | grep -aE '(Failed password for|Accepted)' %s", file.Name, command)
} }
} case strings.HasPrefix(path.Base(file.Name), "messages"):
if strings.HasPrefix(path.Base(file.Name), "auth.log") { switch req.Status {
case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*Accepted (password|publickey)' %s", file.Name, command)
case constant.StatusFailed:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Failed password for|Connection closed by authenticating user)' %s", file.Name, command)
default:
commandItem = fmt.Sprintf("cat %s | grep -aE 'sshd.*(Accepted|Failed password for|Connection closed)' %s", file.Name, command)
}
case strings.HasPrefix(path.Base(file.Name), "auth.log"):
switch req.Status { switch req.Status {
case constant.StatusSuccess: case constant.StatusSuccess:
commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command) commandItem = fmt.Sprintf("cat %s | grep -a Accepted %s", file.Name, command)
@@ -432,107 +435,160 @@ func loadSSHData(c *gin.Context, command string, showCountFrom, showCountTo, cur
lines := strings.Split(string(stdout2), "\n") lines := strings.Split(string(stdout2), "\n")
for i := len(lines) - 1; i >= 0; i-- { for i := len(lines) - 1; i >= 0; i-- {
var itemData dto.SSHHistory var itemData dto.SSHHistory
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
parts := strings.Fields(line)
if len(parts) < 12 {
continue
}
// 统一时间解析逻辑
var dateStr string
var timeIndex int
if strings.Contains(parts[0], "-") { // 处理RFC3339时间格式
t, err := time.Parse(time.RFC3339Nano, parts[0])
if err == nil {
dateStr = t.Format("2006 Jan 2 15:04:05")
timeIndex = 0
}
} else { // 处理系统日志格式
dateParts := parts[:3]
if len(dateParts) < 3 {
continue
}
dateStr = strings.Join(dateParts, " ")
timeIndex = 3
}
// 根据日志类型解析内容
switch { switch {
case strings.Contains(lines[i], "Failed password for"): case strings.Contains(line, "Failed password for"):
itemData = loadFailedSecureDatas(lines[i]) itemData = parseFailedPasswordLog(parts, timeIndex, dateStr)
if len(itemData.Address) != 0 { case strings.Contains(line, "Connection closed by authenticating user"):
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { itemData = parseConnectionClosedLog(parts, timeIndex, dateStr)
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c)) case strings.Contains(line, "Accepted"):
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) itemData = parseAcceptedLog(parts, timeIndex, dateStr)
datas = append(datas, itemData) default:
} continue
failedCount++ }
} if itemData.Address == "" {
case strings.Contains(lines[i], "Connection closed by authenticating user"): continue
itemData = loadFailedAuthDatas(lines[i]) }
if len(itemData.Address) != 0 {
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { total := successCount + failedCount
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c)) if total >= showCountFrom && total < showCountTo {
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc) itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c))
datas = append(datas, itemData) itemData.Date = parseLogDate(currentYear, dateStr, nyc)
} datas = append(datas, itemData)
failedCount++ }
}
case strings.Contains(lines[i], "Accepted "): if itemData.Status == constant.StatusSuccess {
itemData = loadSuccessDatas(lines[i]) successCount++
if len(itemData.Address) != 0 { } else {
if successCount+failedCount >= showCountFrom && successCount+failedCount < showCountTo { failedCount++
itemData.Area, _ = geo.GetIPLocation(itemData.Address, common.GetLang(c)) }
itemData.Date = loadDate(currentYear, itemData.DateStr, nyc)
datas = append(datas, itemData) if total >= showCountTo {
} break
successCount++
}
} }
} }
return datas, successCount, failedCount return datas, successCount, failedCount
} }
func loadSuccessDatas(line string) dto.SSHHistory { func parseFailedPasswordLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
var data dto.SSHHistory data := dto.SSHHistory{
parts := strings.Fields(line) DateStr: dateStr,
index, dataStr := analyzeDateStr(parts) Status: constant.StatusFailed,
if dataStr == "" { AuthMode: "publickey",
return data
} }
data.DateStr = dataStr
data.AuthMode = parts[4+index]
data.User = parts[6+index]
data.Address = parts[8+index]
data.Port = parts[10+index]
data.Status = constant.StatusSuccess
return data
}
func loadFailedAuthDatas(line string) dto.SSHHistory { // 查找关键字段位置
var data dto.SSHHistory for i := timeIndex; i < len(parts); i++ {
parts := strings.Fields(line) switch parts[i] {
index, dataStr := analyzeDateStr(parts) case "for":
if dataStr == "" { if i+1 < len(parts) {
return data data.User = parts[i+1]
}
case "from":
if i+1 < len(parts) {
data.Address = parts[i+1]
}
case "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case "password":
data.AuthMode = "password"
}
} }
data.DateStr = dataStr
switch index { if strings.Contains(strings.Join(parts, " "), "invalid user") {
case 1: data.Message = "invalid user attempt"
data.User = parts[9]
case 2:
data.User = parts[10]
default:
data.User = parts[7]
}
data.AuthMode = parts[6+index]
data.Address = parts[9+index]
data.Port = parts[11+index]
data.Status = constant.StatusFailed
if strings.Contains(line, ": ") {
data.Message = strings.Split(line, ": ")[1]
}
return data
}
func loadFailedSecureDatas(line string) dto.SSHHistory {
var data dto.SSHHistory
parts := strings.Fields(line)
index, dataStr := analyzeDateStr(parts)
if dataStr == "" {
return data
}
data.DateStr = dataStr
if strings.Contains(line, " invalid ") {
data.AuthMode = parts[4+index]
index += 2
} else {
data.AuthMode = parts[4+index]
}
data.User = parts[6+index]
data.Address = parts[8+index]
data.Port = parts[10+index]
data.Status = constant.StatusFailed
if strings.Contains(line, ": ") {
data.Message = strings.Split(line, ": ")[1]
} }
return data return data
} }
func parseConnectionClosedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
data := dto.SSHHistory{
DateStr: dateStr,
Status: constant.StatusFailed,
}
// 解析Alpine格式的特殊字段
fieldStart := timeIndex + 5 // 跳过时间、主机、进程字段
for i := fieldStart; i < len(parts); i++ {
switch {
case parts[i] == "user":
if i+1 < len(parts) {
data.User = parts[i+1]
}
case parts[i] == "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case isIPAddress(parts[i]):
data.Address = parts[i]
}
}
return data
}
func parseAcceptedLog(parts []string, timeIndex int, dateStr string) dto.SSHHistory {
data := dto.SSHHistory{
DateStr: dateStr,
Status: constant.StatusSuccess,
AuthMode: "password", // 默认值
}
// 处理不同日志格式
fieldStart := timeIndex + 5 // 基础字段偏移
for i := fieldStart; i < len(parts); i++ {
switch {
case parts[i] == "for":
if i+1 < len(parts) {
data.User = parts[i+1]
}
case parts[i] == "from":
if i+1 < len(parts) {
data.Address = parts[i+1]
}
case parts[i] == "port":
if i+1 < len(parts) {
data.Port = parts[i+1]
}
case strings.Contains(parts[i], "ssh2:"):
data.AuthMode = "publickey"
case parts[i] == "publickey":
data.AuthMode = "publickey"
case parts[i] == "password":
data.AuthMode = "password"
}
}
return data
}
func handleGunzip(path string) error { func handleGunzip(path string) error {
if _, err := cmd.Execf("gunzip %s", path); err != nil { if _, err := cmd.Execf("gunzip %s", path); err != nil {
return err return err
@@ -541,40 +597,36 @@ func handleGunzip(path string) error {
} }
func loadServiceName() (string, error) { func loadServiceName() (string, error) {
if exist, _ := systemctl.IsExist("sshd"); exist { serviceName, err := systemctl.GetServiceName("ssh")
return "sshd", nil
} else if exist, _ := systemctl.IsExist("ssh"); exist {
return "ssh", nil
}
return "", errors.New("The ssh or sshd service is unavailable")
}
func loadDate(currentYear int, DateStr string, nyc *time.Location) time.Time {
itemDate, err := time.ParseInLocation("2006 Jan 2 15:04:05", fmt.Sprintf("%d %s", currentYear, DateStr), nyc)
if err != nil { if err != nil {
itemDate, _ = time.ParseInLocation("2006 Jan 2 15:04:05", DateStr, nyc) if errors.Is(err, systemctl.ErrServiceNotFound) {
return "", fmt.Errorf("SSH service unavailable. Please ensure OpenSSH server is installed and configured")
}
return "", fmt.Errorf("failed to load SSH service name: %w", err)
} }
return itemDate return serviceName, nil
} }
func analyzeDateStr(parts []string) (int, string) { func parseLogDate(currentYear int, dateStr string, loc *time.Location) time.Time {
t, err := time.Parse(time.RFC3339Nano, parts[0]) // 处理带年份和不带年份的情况
if err == nil { formats := []string{
if len(parts) < 12 { "2006 Jan 2 15:04:05",
return 0, "" "Jan 2 15:04:05",
}
return 0, t.Format("2006 Jan 2 15:04:05")
}
t, err = time.Parse(constant.DateTimeLayout, fmt.Sprintf("%s %s", parts[0], parts[1]))
if err == nil {
if len(parts) < 14 {
return 0, ""
}
return 1, t.Format("2006 Jan 2 15:04:05")
} }
if len(parts) < 14 { for _, format := range formats {
return 0, "" t, err := time.ParseInLocation(format, dateStr, loc)
if err == nil {
if t.Year() == 0 {
return time.Date(currentYear, t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, loc)
}
return t
}
} }
return 2, fmt.Sprintf("%s %s %s", parts[0], parts[1], parts[2]) return time.Now().In(loc)
}
func isIPAddress(s string) bool {
return net.ParseIP(s) != nil
} }
+154 -80
View File
@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -17,6 +18,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/utils/common" "github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/files" "github.com/1Panel-dev/1Panel/backend/utils/files"
httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http" httpUtil "github.com/1Panel-dev/1Panel/backend/utils/http"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
) )
type UpgradeService struct{} type UpgradeService struct{}
@@ -103,123 +105,159 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
} }
downloadPath := fmt.Sprintf("%s/%s/%s/release", global.CONF.System.RepoUrl, mode, req.Version) downloadPath := fmt.Sprintf("%s/%s/%s/release", global.CONF.System.RepoUrl, mode, req.Version)
fileName := fmt.Sprintf("1panel-%s-%s-%s.tar.gz", req.Version, "linux", itemArch) fileName := fmt.Sprintf("1panel-%s-%s-%s.tar.gz", req.Version, "linux", itemArch)
_ = settingRepo.Update("SystemStatus", "Upgrading") serviceHandle, _ := systemctl.DefaultHandler("1panel")
currentServiceName := serviceHandle.GetServiceName()
if err := settingRepo.Update("SystemStatus", "Upgrading"); err != nil {
return fmt.Errorf("update system status failed: %w", err)
}
go func() { go func() {
defer func() {
if err := settingRepo.Update("SystemStatus", "Free"); err != nil {
global.LOG.Errorf("Reset system status failed: %v", err)
}
}()
_ = global.Cron.Stop() _ = global.Cron.Stop()
defer func() { defer global.Cron.Start()
global.Cron.Start()
}() if err := fileOp.DownloadFileWithProxy(
if err := fileOp.DownloadFileWithProxy(downloadPath+"/"+fileName, rootDir+"/"+fileName); err != nil { fmt.Sprintf("%s/%s", downloadPath, fileName),
global.LOG.Errorf("download service file failed, err: %v", err) path.Join(rootDir, fileName),
_ = settingRepo.Update("SystemStatus", "Free") ); err != nil {
global.LOG.Errorf("Failed to download upgrade package: %v", err)
return return
} }
global.LOG.Info("download all file successful!") defer os.RemoveAll(rootDir)
defer func() {
_ = os.Remove(rootDir) if err := handleUnTar(path.Join(rootDir, fileName), rootDir, ""); err != nil {
}() global.LOG.Errorf("Failed to extract package: %v", err)
if err := handleUnTar(rootDir+"/"+fileName, rootDir, ""); err != nil {
global.LOG.Errorf("decompress file failed, err: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
return return
} }
tmpDir := rootDir + "/" + strings.ReplaceAll(fileName, ".tar.gz", "")
tmpDir := path.Join(rootDir, strings.TrimSuffix(fileName, ".tar.gz"))
if err := u.handleBackup(fileOp, originalDir); err != nil { if err := u.handleBackup(fileOp, originalDir); err != nil {
global.LOG.Errorf("handle backup original file failed, err: %v", err) global.LOG.Errorf("Backup failed: %v", err)
_ = settingRepo.Update("SystemStatus", "Free")
return
}
global.LOG.Info("backup original data successful, now start to upgrade!")
if err := common.CopyFile(path.Join(tmpDir, "1panel"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("upgrade 1panel failed, err: %v", err)
u.handleRollback(originalDir, 1)
return return
} }
if err := common.CopyFile(path.Join(tmpDir, "1pctl"), "/usr/local/bin"); err != nil { binDir := systemctl.BinaryPath
global.LOG.Errorf("upgrade 1pctl failed, err: %v", err) servicePath, _ := serviceHandle.GetServicePath()
u.handleRollback(originalDir, 2) geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
return
}
_, _ = cmd.Execf("cp -r %s /usr/local/bin", path.Join(tmpDir, "lang"))
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmd.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpDir, "GeoIP.mmdb"), geoPath)
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl", global.CONF.System.BaseDir); err != nil { criticalUpdates := []struct {
global.LOG.Errorf("upgrade basedir in 1pctl failed, err: %v", err) src string
dest string
step int
}{
{path.Join(tmpDir, "1panel"), path.Join(binDir, "1panel"), 1},
{path.Join(tmpDir, "1pctl"), path.Join(binDir, "1pctl"), 2},
{selectInitScript(path.Join(tmpDir, "initscript"), currentServiceName), servicePath, 3},
}
for _, update := range criticalUpdates {
if err := common.Copy(update.src, update.dest); err != nil {
global.LOG.Errorf("Update %s failed: %v", path.Base(update.dest), err)
u.handleRollback(originalDir, update.step)
return
}
}
if _, err := cmd.Execf("sed -i -e 's#BASE_DIR=.*#BASE_DIR=%s#g' /usr/local/bin/1pctl",
global.CONF.System.BaseDir); err != nil {
global.LOG.Errorf("Update base directory failed: %v", err)
u.handleRollback(originalDir, 2) u.handleRollback(originalDir, 2)
return return
} }
if err := common.CopyFile(path.Join(tmpDir, "1panel.service"), "/etc/systemd/system"); err != nil { langDir := path.Join(binDir, "lang")
global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err) if err := common.Copy(path.Join(tmpDir, "lang"), langDir); err != nil {
u.handleRollback(originalDir, 3) global.LOG.Errorf("Update language files failed: %v", err)
return }
if err := common.Copy(path.Join(tmpDir, "GeoIP.mmdb"), geoPath); err != nil {
global.LOG.Warnf("Update GeoIP database failed: %v", err)
} }
global.LOG.Info("upgrade successful!") global.LOG.Info("upgrade successful!")
go writeLogs(req.Version) go writeLogs(req.Version)
_ = settingRepo.Update("SystemVersion", req.Version)
_ = settingRepo.Update("SystemStatus", "Free")
checkPointOfWal() checkPointOfWal()
_, _ = cmd.ExecWithTimeOut("systemctl daemon-reload && systemctl restart 1panel.service", 1*time.Minute) if err := settingRepo.Update("SystemVersion", req.Version); err != nil {
global.LOG.Errorf("Update system version failed: %v", err)
}
if serviceHandle.ManagerName() == "systemd" {
_, _ = cmd.Exec("systemctl daemon-reload")
}
if err := systemctl.Restart("1panel"); err != nil {
global.LOG.Errorf("Service restart failed: %v", err)
return
}
}() }()
return nil return nil
} }
func (u *UpgradeService) handleBackup(fileOp files.FileOp, originalDir string) error { func (u *UpgradeService) handleBackup(fileOp files.FileOp, originalDir string) error {
if err := fileOp.Copy("/usr/local/bin/1panel", originalDir); err != nil { global.LOG.Info("Initiating backup procedure...")
return err h, _ := systemctl.DefaultHandler("1panel")
binDir := systemctl.BinaryPath
servicePath, _ := h.GetServicePath()
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
backupItems := []struct {
src string
dest string
}{
{path.Join(binDir, "1panel"), originalDir},
{path.Join(binDir, "1pctl"), originalDir},
{servicePath, originalDir},
{path.Join(binDir, "lang"), originalDir},
{geoPath, originalDir},
} }
if err := fileOp.Copy("/usr/local/bin/1pctl", originalDir); err != nil {
return err for _, item := range backupItems {
if err := fileOp.Copy(item.src, item.dest); err != nil {
return fmt.Errorf("backup %s failed: %w", path.Base(item.src), err)
}
} }
if err := fileOp.Copy("/etc/systemd/system/1panel.service", originalDir); err != nil {
return err if err := handleTar(
} path.Join(global.CONF.System.BaseDir, "1panel/db"),
_, _ = cmd.Execf("cp -r /usr/local/bin/lang %s", originalDir) originalDir,
_, _ = cmd.Execf("cp %s %s", path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb"), originalDir) "db.tar.gz",
checkPointOfWal() "db/1Panel.db-*",
if err := handleTar(path.Join(global.CONF.System.BaseDir, "1panel/db"), originalDir, "db.tar.gz", "db/1Panel.db-*", ""); err != nil { "",
return err ); err != nil {
return fmt.Errorf("database backup failed: %w", err)
} }
return nil return nil
} }
func (u *UpgradeService) handleRollback(originalDir string, errStep int) { func (u *UpgradeService) handleRollback(originalDir string, errStep int) {
_ = settingRepo.Update("SystemStatus", "Free") global.LOG.Info("Initiating rollback procedure...")
h, _ := systemctl.DefaultHandler("1panel")
binDir := systemctl.BinaryPath
servicePath, _ := h.GetServicePath()
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo/GeoIP.mmdb")
checkPointOfWal() rollbackSteps := []struct {
if _, err := os.Stat(path.Join(originalDir, "1Panel.db")); err == nil { src string
if err := common.CopyFile(path.Join(originalDir, "1Panel.db"), global.CONF.System.DbPath); err != nil { dest string
global.LOG.Errorf("rollback 1panel db failed, err: %v", err) }{
{path.Join(originalDir, "1panel"), path.Join(binDir, "1panel")},
{path.Join(originalDir, "1pctl"), path.Join(binDir, "1pctl")},
{path.Join(originalDir, filepath.Base(servicePath)), servicePath},
{path.Join(originalDir, "lang"), path.Join(binDir, "lang")},
{path.Join(originalDir, "GeoIP.mmdb"), geoPath},
}
for _, step := range rollbackSteps[:errStep] {
if err := common.CopyFile(step.src, step.dest); err != nil {
global.LOG.Errorf("Rollback %s failed: %v", path.Base(step.src), err)
} }
} }
if _, err := os.Stat(path.Join(originalDir, "db.tar.gz")); err == nil {
if err := handleUnTar(path.Join(originalDir, "db.tar.gz"), global.CONF.System.DbPath, ""); err != nil {
global.LOG.Errorf("rollback 1panel db failed, err: %v", err)
}
}
if err := common.CopyFile(path.Join(originalDir, "1panel"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("rollback 1pctl failed, err: %v", err)
}
if errStep == 1 {
return
}
if err := common.CopyFile(path.Join(originalDir, "1pctl"), "/usr/local/bin"); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err)
}
_, _ = cmd.Execf("cp -r %s /usr/local/bin", path.Join(originalDir, "lang"))
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmd.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(originalDir, "GeoIP.mmdb"), geoPath)
if errStep == 2 { if err := systemctl.Restart("1panel"); err != nil {
return global.LOG.Errorf("Service restart during rollback failed: %v", err)
}
if err := common.CopyFile(path.Join(originalDir, "1panel.service"), "/etc/systemd/system"); err != nil {
global.LOG.Errorf("rollback 1panel failed, err: %v", err)
} }
} }
@@ -357,5 +395,41 @@ func loadArch() (string, error) {
if strings.Contains(std, "s390x") { if strings.Contains(std, "s390x") {
return "s390x", nil return "s390x", nil
} }
if strings.Contains(std, "riscv64") {
return "riscv64", nil
}
return "", fmt.Errorf("unsupported such arch: %s", std) return "", fmt.Errorf("unsupported such arch: %s", std)
} }
func selectInitScript(path string, serviceName string) string {
path = strings.TrimSuffix(path, "/")
mgr := systemctl.GetGlobalManager().Name()
var serviceFileName string
switch mgr {
case "systemd":
serviceFileName = "1panel.service"
case "openrc":
serviceFileName = "1paneld.openrc"
case "sysvinit":
isWrt := systemctl.FileExist("/etc/rc.common")
if isWrt {
serviceFileName = "1paneld.procd"
} else {
serviceFileName = "1paneld.init"
}
default:
serviceFileName = serviceName
global.LOG.Warnf("[%s]unselect InitScript, used default: %s", mgr, serviceName)
}
sourcePath := filepath.Join(path, serviceFileName)
targetPath := filepath.Join(path, serviceName)
if serviceFileName != serviceName {
if _, err := cmd.Execf("cp %s %s", sourcePath, targetPath); err != nil {
global.LOG.Errorf("Failed to copy init script from %s to %s: %v",
serviceFileName, serviceName, err)
}
}
return targetPath
}
+18 -5
View File
@@ -427,7 +427,9 @@ func (w WebsiteService) UpdateWebsite(req request.WebsiteUpdate) error {
} }
} }
website.PrimaryDomain = req.PrimaryDomain website.PrimaryDomain = req.PrimaryDomain
website.WebsiteGroupID = req.WebsiteGroupID if req.WebsiteGroupID > 0 {
website.WebsiteGroupID = req.WebsiteGroupID
}
website.Remark = req.Remark website.Remark = req.Remark
website.IPV6 = req.IPV6 website.IPV6 = req.IPV6
@@ -1768,11 +1770,22 @@ func (w WebsiteService) GetProxies(id uint) (res []request.WebsiteProxyConfig, e
} }
directives := config.GetDirectives() directives := config.GetDirectives()
location, ok := directives[0].(*components.Location) var (
if !ok { location *components.Location
err = errors.New("error") ok bool
return )
for _, directive := range directives {
if directive.GetName() == "location" {
location, ok = directive.(*components.Location)
if ok {
break
}
}
} }
if location == nil {
return nil, buserr.New("ErrConfigParse")
}
proxyConfig.ProxyPass = location.ProxyPass proxyConfig.ProxyPass = location.ProxyPass
proxyConfig.Cache = location.Cache proxyConfig.Cache = location.Cache
if location.CacheTime > 0 { if location.CacheTime > 0 {
+3
View File
@@ -2,6 +2,7 @@ package global
import ( import (
"github.com/1Panel-dev/1Panel/backend/configs" "github.com/1Panel-dev/1Panel/backend/configs"
"github.com/1Panel-dev/1Panel/backend/init/auth"
"github.com/1Panel-dev/1Panel/backend/init/cache/badger_db" "github.com/1Panel-dev/1Panel/backend/init/cache/badger_db"
"github.com/1Panel-dev/1Panel/backend/init/session/psession" "github.com/1Panel-dev/1Panel/backend/init/session/psession"
"github.com/dgraph-io/badger/v4" "github.com/dgraph-io/badger/v4"
@@ -28,6 +29,8 @@ var (
MonitorCronID cron.EntryID MonitorCronID cron.EntryID
OneDriveCronID cron.EntryID OneDriveCronID cron.EntryID
IPTracker *auth.IPTracker
I18n *i18n.Localizer I18n *i18n.Localizer
I18nForCmd *i18n.Localizer I18nForCmd *i18n.Localizer
) )
+99
View File
@@ -0,0 +1,99 @@
package auth
import (
"sync"
"time"
)
const (
MaxIPCount = 100
ExpireDuration = 30 * time.Minute
)
type IPRecord struct {
NeedCaptcha bool
LastUpdate time.Time
}
type IPTracker struct {
records map[string]*IPRecord
ipOrder []string
mu sync.RWMutex
}
func NewIPTracker() *IPTracker {
return &IPTracker{
records: make(map[string]*IPRecord),
ipOrder: make([]string, 0),
}
}
func (t *IPTracker) NeedCaptcha(ip string) bool {
t.mu.Lock()
defer t.mu.Unlock()
record, exists := t.records[ip]
if !exists {
return false
}
if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
return false
}
return record.NeedCaptcha
}
func (t *IPTracker) SetNeedCaptcha(ip string) {
t.mu.Lock()
defer t.mu.Unlock()
if record, exists := t.records[ip]; exists {
if time.Since(record.LastUpdate) > ExpireDuration {
t.removeIPUnsafe(ip)
} else {
record.NeedCaptcha = true
record.LastUpdate = time.Now()
return
}
}
if len(t.records) >= MaxIPCount {
t.removeOldestUnsafe()
}
t.records[ip] = &IPRecord{
NeedCaptcha: true,
LastUpdate: time.Now(),
}
t.ipOrder = append(t.ipOrder, ip)
}
func (t *IPTracker) Clear(ip string) {
t.mu.Lock()
defer t.mu.Unlock()
t.removeIPUnsafe(ip)
}
func (t *IPTracker) removeIPUnsafe(ip string) {
delete(t.records, ip)
for i, storedIP := range t.ipOrder {
if storedIP == ip {
t.ipOrder = append(t.ipOrder[:i], t.ipOrder[i+1:]...)
break
}
}
}
func (t *IPTracker) removeOldestUnsafe() {
if len(t.ipOrder) == 0 {
return
}
oldestIP := t.ipOrder[0]
delete(t.records, oldestIP)
t.ipOrder = t.ipOrder[1:]
}
+3
View File
@@ -108,6 +108,9 @@ func Init() {
migrations.AddLicenseVerify, migrations.AddLicenseVerify,
migrations.AddMcpServer, migrations.AddMcpServer,
migrations.AddPbootCMSPHPExtensions,
migrations.DeleteV2Openresty,
migrations.UpdateOnedrive,
}) })
if err := m.Migrate(); err != nil { if err := m.Migrate(); err != nil {
global.LOG.Error(err) global.LOG.Error(err)
@@ -469,3 +469,43 @@ var AddMcpServer = &gormigrate.Migration{
return nil return nil
}, },
} }
var AddPbootCMSPHPExtensions = &gormigrate.Migration{
ID: "20250625-add-php-extensions",
Migrate: func(tx *gorm.DB) error {
item := &model.PHPExtensions{
Name: "PbootCMS",
}
if err := tx.Where("name = ?", "PbootCMS").FirstOrCreate(item, &model.PHPExtensions{
Name: "PbootCMS",
Extensions: "curl,gd,mbstring,mysqli,openssl,pdo_mysql,pdo_sqlite,sqlite3",
}).Error; err != nil {
return err
}
return nil
},
}
var DeleteV2Openresty = &gormigrate.Migration{
ID: "20250701-delete-v2-openresty",
Migrate: func(tx *gorm.DB) error {
return tx.Where("version = '1.27.1.2-0-1-focal'").Delete(&model.AppDetail{}).Error
},
}
var UpdateOnedrive = &gormigrate.Migration{
ID: "20250704-update-onedrive",
Migrate: func(tx *gorm.DB) error {
if err := tx.Model(&model.Setting{}).
Where("key = ?", "OneDriveID").
Update("value", "NTQ0NmNmZTMtNGM3OS00N2EwLWFlMjUtZmM2NDU0NzhlMmQ5").Error; err != nil {
return err
}
if err := tx.Model(&model.Setting{}).
Where("key = ?", "OneDriveSc").
Update("value", "bGRlOFF+WEVrR1M0b25Vb1VsRWpMYzE2MW9rTXZEM25KdnZ1MGN6MA==").Error; err != nil {
return err
}
return nil
},
}
+5 -4
View File
@@ -3,14 +3,15 @@ package router
import ( import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/cmd/server/res"
"net/http" "net/http"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"github.com/1Panel-dev/1Panel/backend/app/service"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/cmd/server/res"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/i18n" "github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/middleware" "github.com/1Panel-dev/1Panel/backend/middleware"
@@ -160,7 +161,7 @@ func setWebStatic(rootRouter *gin.RouterGroup) {
} }
func Routers() *gin.Engine { func Routers() *gin.Engine {
Router = gin.Default() Router = gin.New()
Router.Use(middleware.OperationLog()) Router.Use(middleware.OperationLog())
// Router.Use(middleware.CSRF()) // Router.Use(middleware.CSRF())
// Router.Use(middleware.LoadCsrfToken()) // Router.Use(middleware.LoadCsrfToken())
+6 -1
View File
@@ -8,11 +8,17 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/repo" "github.com/1Panel-dev/1Panel/backend/app/repo"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func WhiteAllow() gin.HandlerFunc { func WhiteAllow() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
clientIP := common.GetRealClientIP(c)
if common.IsPrivateIP(clientIP) {
c.Next()
return
}
settingRepo := repo.NewISettingRepo() settingRepo := repo.NewISettingRepo()
status, err := settingRepo.Get(settingRepo.WithByKey("AllowIPs")) status, err := settingRepo.Get(settingRepo.WithByKey("AllowIPs"))
if err != nil { if err != nil {
@@ -24,7 +30,6 @@ func WhiteAllow() gin.HandlerFunc {
c.Next() c.Next()
return return
} }
clientIP := c.ClientIP()
for _, ip := range strings.Split(status.Value, ",") { for _, ip := range strings.Split(status.Value, ",") {
if len(ip) == 0 { if len(ip) == 0 {
continue continue
+1 -1
View File
@@ -16,7 +16,7 @@ func (s *BaseRouter) InitRouter(Router *gin.RouterGroup) {
baseRouter.POST("/login", baseApi.Login) baseRouter.POST("/login", baseApi.Login)
baseRouter.POST("/logout", baseApi.LogOut) baseRouter.POST("/logout", baseApi.LogOut)
baseRouter.GET("/demo", baseApi.CheckIsDemo) baseRouter.GET("/demo", baseApi.CheckIsDemo)
baseRouter.GET("/language", baseApi.GetLanguage) baseRouter.GET("/setting", baseApi.GetAuthSetting)
baseRouter.GET("/intl", baseApi.CheckIsIntl) baseRouter.GET("/intl", baseApi.CheckIsIntl)
} }
} }
+2
View File
@@ -13,6 +13,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/i18n" "github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/init/app" "github.com/1Panel-dev/1Panel/backend/init/app"
"github.com/1Panel-dev/1Panel/backend/init/auth"
"github.com/1Panel-dev/1Panel/backend/init/business" "github.com/1Panel-dev/1Panel/backend/init/business"
"github.com/1Panel-dev/1Panel/backend/init/lang" "github.com/1Panel-dev/1Panel/backend/init/lang"
@@ -52,6 +53,7 @@ func Start() {
business.Init() business.Init()
rootRouter := router.Routers() rootRouter := router.Routers()
global.IPTracker = auth.NewIPTracker()
tcpItem := "tcp4" tcpItem := "tcp4"
if global.CONF.System.Ipv6 == "enable" { if global.CONF.System.Ipv6 == "enable" {
+3 -3
View File
@@ -11,13 +11,13 @@ import (
var store = base64Captcha.DefaultMemStore var store = base64Captcha.DefaultMemStore
func VerifyCode(codeID string, code string) error { func VerifyCode(codeID string, code string) error {
if codeID == "" {
return constant.ErrCaptchaCode
}
vv := store.Get(codeID, true) vv := store.Get(codeID, true)
vv = strings.TrimSpace(vv) vv = strings.TrimSpace(vv)
code = strings.TrimSpace(code) code = strings.TrimSpace(code)
if codeID == "" || code == "" {
return constant.ErrCaptchaCode
}
if strings.EqualFold(vv, code) { if strings.EqualFold(vv, code) {
return nil return nil
} }
+73 -2
View File
@@ -3,13 +3,12 @@ package common
import ( import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/gin-gonic/gin"
"io" "io"
mathRand "math/rand" mathRand "math/rand"
"net" "net"
"os" "os"
"path" "path"
"path/filepath"
"reflect" "reflect"
"regexp" "regexp"
"sort" "sort"
@@ -18,6 +17,9 @@ import (
"time" "time"
"unicode" "unicode"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/gin-gonic/gin"
"golang.org/x/net/idna" "golang.org/x/net/idna"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -124,6 +126,9 @@ func GetSortedVersions(versions []string) []string {
} }
func CopyFile(src, dst string) error { func CopyFile(src, dst string) error {
if fi, err := os.Stat(src); err == nil && fi.IsDir() {
return fmt.Errorf("src is a directory, use CopyDirs instead")
}
source, err := os.Open(src) source, err := os.Open(src)
if err != nil { if err != nil {
return err return err
@@ -152,7 +157,57 @@ func CopyFile(src, dst string) error {
} }
return nil return nil
} }
func CopyDirs(srcDir, dstDir string) error {
srcInfo, err := os.Stat(srcDir)
if err != nil {
return err
}
if !srcInfo.IsDir() {
return fmt.Errorf("%s is not a directory", srcDir)
}
if err := os.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
return err
}
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(srcDir, path)
if err != nil {
return err
}
dstPath := filepath.Join(dstDir, relPath)
if info.IsDir() {
return os.MkdirAll(dstPath, info.Mode())
}
if info.Mode()&os.ModeSymlink != 0 {
return copySymlink(path, dstPath)
}
return CopyFile(path, dstPath)
})
}
func copySymlink(src, dst string) error {
link, err := os.Readlink(src)
if err != nil {
return err
}
return os.Symlink(link, dst)
}
func Copy(src, dst string) error {
fi, err := os.Stat(src)
if err != nil {
return err
}
switch mode := fi.Mode(); {
case mode.IsDir():
return CopyDirs(src, dst)
case mode.IsRegular():
return CopyFile(src, dst)
default:
return fmt.Errorf("unsupported file type: %s", mode)
}
}
func IsCrossVersion(version1, version2 string) bool { func IsCrossVersion(version1, version2 string) bool {
version1s := strings.Split(version1, ".") version1s := strings.Split(version1, ".")
version2s := strings.Split(version2, ".") version2s := strings.Split(version2, ".")
@@ -371,3 +426,19 @@ func HandleIPList(content string) ([]string, error) {
} }
return res, nil return res, nil
} }
func GetRealClientIP(c *gin.Context) string {
addr := c.Request.RemoteAddr
if ip, _, err := net.SplitHostPort(addr); err == nil {
return ip
}
return addr
}
func IsPrivateIP(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.IsPrivate() || ip.IsLoopback()
}
+20 -16
View File
@@ -349,26 +349,30 @@ func (f FileOp) DownloadFileWithProxy(url, dst string) error {
} }
func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error { func (f FileOp) Cut(oldPaths []string, dst, name string, cover bool) error {
for _, p := range oldPaths { if len(oldPaths) == 0 {
var dstPath string return nil
if name != "" { }
dstPath = filepath.Join(dst, name) var dstPath string
if f.Stat(dstPath) { coverFlag := ""
dstPath = dst if name != "" {
} dstPath = filepath.Join(dst, name)
} else { if f.Stat(dstPath) {
base := filepath.Base(p) dstPath = dst
dstPath = filepath.Join(dst, base)
} }
coverFlag := ""
if cover { if cover {
coverFlag = "-f" coverFlag = "-f"
} }
} else {
cmdStr := fmt.Sprintf(`mv %s '%s' '%s'`, coverFlag, p, dstPath) dstPath = dst
if err := cmd.ExecCmd(cmdStr); err != nil { coverFlag = "-f"
return err }
} var quotedPaths []string
for _, p := range oldPaths {
quotedPaths = append(quotedPaths, fmt.Sprintf("'%s'", p))
}
mvCommand := fmt.Sprintf("mv %s %s '%s'", coverFlag, strings.Join(quotedPaths, " "), dstPath)
if err := cmd.ExecCmd(mvCommand); err != nil {
return err
} }
return nil return nil
} }
+4 -15
View File
@@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/systemctl"
) )
var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`) var ForwardListRegex = regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`)
@@ -41,27 +42,15 @@ func (f *Firewall) Version() (string, error) {
} }
func (f *Firewall) Start() error { func (f *Firewall) Start() error {
stdout, err := cmd.Exec("systemctl start firewalld") return systemctl.Start("firewalld")
if err != nil {
return fmt.Errorf("enable the firewall failed, err: %s", stdout)
}
return nil
} }
func (f *Firewall) Stop() error { func (f *Firewall) Stop() error {
stdout, err := cmd.Exec("systemctl stop firewalld") return systemctl.Stop("firewalld")
if err != nil {
return fmt.Errorf("stop the firewall failed, err: %s", stdout)
}
return nil
} }
func (f *Firewall) Restart() error { func (f *Firewall) Restart() error {
stdout, err := cmd.Exec("systemctl restart firewalld") return systemctl.Restart("firewalld")
if err != nil {
return fmt.Errorf("restart the firewall failed, err: %s", stdout)
}
return nil
} }
func (f *Firewall) Reload() error { func (f *Firewall) Reload() error {
+12 -13
View File
@@ -188,19 +188,18 @@ func (r *Local) ChangeAccess(info AccessChangeInfo) error {
info.Name = "*" info.Name = "*"
info.Password = r.Password info.Password = r.Password
} }
if info.Permission == info.OldPermission { if info.Permission != info.OldPermission {
return nil if err := r.Delete(DeleteInfo{
} Version: info.Version,
if err := r.Delete(DeleteInfo{ Username: info.Username,
Version: info.Version, Permission: info.OldPermission,
Username: info.Username, ForceDelete: true,
Permission: info.OldPermission, Timeout: 300}); err != nil {
ForceDelete: true, return err
Timeout: 300}); err != nil { }
return err if info.Username == "root" {
} return nil
if info.Username == "root" { }
return nil
} }
if err := r.CreateUser(CreateInfo{ if err := r.CreateUser(CreateInfo{
Name: info.Name, Name: info.Name,
+12 -13
View File
@@ -199,19 +199,18 @@ func (r *Remote) ChangeAccess(info AccessChangeInfo) error {
info.Name = "*" info.Name = "*"
info.Password = r.Password info.Password = r.Password
} }
if info.Permission == info.OldPermission { if info.Permission != info.OldPermission {
return nil if err := r.Delete(DeleteInfo{
} Version: info.Version,
if err := r.Delete(DeleteInfo{ Username: info.Username,
Version: info.Version, Permission: info.OldPermission,
Username: info.Username, ForceDelete: true,
Permission: info.OldPermission, Timeout: 300}); err != nil {
ForceDelete: true, return err
Timeout: 300}); err != nil { }
return err if info.Username == "root" {
} return nil
if info.Username == "root" { }
return nil
} }
if err := r.CreateUser(CreateInfo{ if err := r.CreateUser(CreateInfo{
Name: info.Name, Name: info.Name,
+13 -4
View File
@@ -3,14 +3,10 @@ package ssl
import ( import (
"crypto" "crypto"
"encoding/json" "encoding/json"
"github.com/go-acme/lego/v4/providers/dns/westcn"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/go-acme/lego/v4/providers/dns/freemyip"
"github.com/go-acme/lego/v4/providers/dns/rainyun"
"github.com/1Panel-dev/1Panel/backend/app/model" "github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme"
"github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/acme/api"
@@ -23,13 +19,17 @@ import (
"github.com/go-acme/lego/v4/providers/dns/cloudflare" "github.com/go-acme/lego/v4/providers/dns/cloudflare"
"github.com/go-acme/lego/v4/providers/dns/cloudns" "github.com/go-acme/lego/v4/providers/dns/cloudns"
"github.com/go-acme/lego/v4/providers/dns/dnspod" "github.com/go-acme/lego/v4/providers/dns/dnspod"
"github.com/go-acme/lego/v4/providers/dns/freemyip"
"github.com/go-acme/lego/v4/providers/dns/godaddy" "github.com/go-acme/lego/v4/providers/dns/godaddy"
"github.com/go-acme/lego/v4/providers/dns/huaweicloud" "github.com/go-acme/lego/v4/providers/dns/huaweicloud"
"github.com/go-acme/lego/v4/providers/dns/namecheap" "github.com/go-acme/lego/v4/providers/dns/namecheap"
"github.com/go-acme/lego/v4/providers/dns/namedotcom" "github.com/go-acme/lego/v4/providers/dns/namedotcom"
"github.com/go-acme/lego/v4/providers/dns/namesilo" "github.com/go-acme/lego/v4/providers/dns/namesilo"
"github.com/go-acme/lego/v4/providers/dns/porkbun"
"github.com/go-acme/lego/v4/providers/dns/rainyun"
"github.com/go-acme/lego/v4/providers/dns/tencentcloud" "github.com/go-acme/lego/v4/providers/dns/tencentcloud"
"github.com/go-acme/lego/v4/providers/dns/volcengine" "github.com/go-acme/lego/v4/providers/dns/volcengine"
"github.com/go-acme/lego/v4/providers/dns/westcn"
"github.com/go-acme/lego/v4/providers/http/webroot" "github.com/go-acme/lego/v4/providers/http/webroot"
"github.com/go-acme/lego/v4/registration" "github.com/go-acme/lego/v4/registration"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -87,6 +87,7 @@ const (
HuaweiCloud DnsType = "HuaweiCloud" HuaweiCloud DnsType = "HuaweiCloud"
RainYun DnsType = "RainYun" RainYun DnsType = "RainYun"
WestCN DnsType = "WestCN" WestCN DnsType = "WestCN"
PorkBun DnsType = "PorkBun"
) )
type DNSParam struct { type DNSParam struct {
@@ -244,6 +245,14 @@ func (c *AcmeClient) UseDns(dnsType DnsType, params string, websiteSSL model.Web
westcnConfig.PollingInterval = pollingInterval westcnConfig.PollingInterval = pollingInterval
westcnConfig.TTL = ttl westcnConfig.TTL = ttl
p, err = westcn.NewDNSProviderConfig(westcnConfig) p, err = westcn.NewDNSProviderConfig(westcnConfig)
case PorkBun:
porkbunConfig := porkbun.NewDefaultConfig()
porkbunConfig.APIKey = param.APIkey
porkbunConfig.SecretAPIKey = param.SecretKey
porkbunConfig.PropagationTimeout = propagationTimeout
porkbunConfig.PollingInterval = pollingInterval
porkbunConfig.TTL = ttl
p, err = porkbun.NewDNSProviderConfig(porkbunConfig)
} }
if err != nil { if err != nil {
return err return err
+393
View File
@@ -0,0 +1,393 @@
package systemctl
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"go4.org/syncutil/singleflight"
"golang.org/x/sync/errgroup"
)
var (
aliasFile string
serviceAliases sync.Map
saveTimer *time.Timer
saveMutex sync.Mutex
afterSaveTime = 20 * time.Second
)
var (
ErrServiceNotFound = errors.New("service not found")
ErrDiscoveryTimeout = errors.New("service discovery timeout for: %w")
ErrServiceDiscovery = errors.New("service discovery failed for: %w")
ErrNoValidService = errors.New("no valid service found for: %w")
)
func loadPredefinedAliases() map[string][]string {
return map[string][]string{
"clam": {"clamav-daemon.service", "clamd@scan.service", "clamd"},
"freshclam": {"clamav-freshclam.service", "freshclam.service"},
"fail2ban": {"fail2ban.service", "fail2ban"},
"supervisor": {"supervisord.service", "supervisor.service", "supervisord", "supervisor"},
"ssh": {"sshd.service", "ssh.service", "sshd", "ssh"},
"1panel": {"1panel.service", "1paneld"},
"docker": {"docker.service", "dockerd"},
}
}
func InitializeServiceDiscovery() {
svcName := loadAliasesFromConfig()
if len(svcName) > 0 {
RegisterServiceAliases(svcName)
}
}
func RegisterServiceAliases(aliases map[string][]string) {
for key, values := range aliases {
existing, loaded := serviceAliases.LoadOrStore(key, values)
if loaded {
merged := append(existing.([]string), values...)
serviceAliases.Store(key, merged)
}
}
}
func loadAliasesFromConfig() map[string][]string {
data, err := os.ReadFile(aliasFile)
if err != nil {
return nil
}
var rawAliases map[string][]string
json.Unmarshal(data, &rawAliases)
validAliases := make(map[string][]string)
for key, aliases := range rawAliases {
valid := []string{}
for _, alias := range aliases {
confirmed, _ := confirmServiceExists(alias)
if confirmed {
valid = append(valid, alias)
}
}
if len(valid) > 0 {
validAliases[key] = valid
}
}
return validAliases
}
func cleanupKeywordAliases(keyword string) {
serviceAliases.Range(func(k, v interface{}) bool {
if k.(string) != keyword {
return true
}
aliases := v.([]string)
valid := make([]string, 0)
for _, alias := range aliases {
confirmed, _ := confirmServiceExists(alias)
if confirmed {
valid = append(valid, alias)
}
}
if len(valid) == 0 {
serviceAliases.Delete(k)
serviceExistenceCache.Delete(k)
} else {
serviceAliases.Store(k, valid)
}
return true
})
go scheduleSave()
}
func smartServiceName(keyword string) (string, error) {
mgr := GetGlobalManager()
processedName := handleServiceNaming(mgr, keyword)
confirmed, _ := confirmServiceExists(processedName)
if confirmed {
updateAliases(keyword, processedName)
return processedName, nil
}
candidates := append([]string{processedName}, getAliases(keyword)...)
if name, err := validateCandidatesConcurrently(candidates); err == nil {
updateAliases(keyword, name)
return name, nil
}
discoveredName, err := discoverAndSelectService(keyword)
if err != nil {
cleanupKeywordAliases(keyword)
return "", ErrServiceNotFound
}
updateAliases(keyword, discoveredName)
return discoveredName, nil
}
func handleServiceNaming(mgr ServiceManager, keyword string) string {
keyword = strings.ToLower(keyword)
if strings.HasSuffix(keyword, ".service.socket") {
keyword = strings.TrimSuffix(keyword, ".service.socket") + ".socket"
}
if mgr.Name() != "systemd" {
keyword = strings.TrimSuffix(keyword, ".service")
return keyword
}
if !strings.HasSuffix(keyword, ".service") &&
!strings.HasSuffix(keyword, ".socket") {
keyword += ".service"
}
return keyword
}
func validateCandidatesConcurrently(candidates []string) (string, error) {
var (
g errgroup.Group
found = make(chan string, 1)
)
for _, candidate := range candidates {
cand := candidate
g.Go(func() error {
confirmed, _ := confirmServiceExists(cand)
if confirmed {
select {
case found <- cand:
default:
}
return nil
}
return ErrServiceNotFound
})
}
resultErr := make(chan error, 1)
go func() {
defer close(found)
resultErr <- g.Wait()
}()
select {
case name := <-found:
return name, nil
case <-time.After(1000 * time.Millisecond):
return "", fmt.Errorf(ErrDiscoveryTimeout.Error(), candidates[0])
case err := <-resultErr:
if err != nil {
return "", ErrServiceNotFound
}
return "", ErrServiceNotFound
}
}
func discoverAndSelectService(keyword string) (string, error) {
discovered, err := discoverServices(keyword)
if err != nil {
return "", ErrServiceNotFound
}
if len(discovered) == 0 {
return "", ErrServiceNotFound
}
selected, err := selectBestMatch(keyword, discovered)
if err != nil {
return "", ErrServiceNotFound
}
confirmed, err := confirmServiceExists(selected)
if err != nil {
return "", fmt.Errorf("service existence check failed: %w", err)
}
if confirmed {
return selected, nil
}
return "", ErrServiceNotFound
}
func selectBestMatch(keyword string, candidates []string) (string, error) {
if len(candidates) == 0 {
return "", ErrServiceNotFound
}
lowerKeyword := strings.ToLower(keyword)
var exactMatch string
var firstContainMatch string
for _, name := range candidates {
if strings.EqualFold(name, keyword) {
exactMatch = name
break
}
}
if exactMatch != "" {
return exactMatch, nil
}
for _, name := range candidates {
if strings.Contains(strings.ToLower(name), lowerKeyword) {
firstContainMatch = name
global.LOG.Debugf("[%s] [keyword: %s] Found first contain match: %s", getManagerName(), keyword, firstContainMatch)
break
}
}
if firstContainMatch != "" {
return firstContainMatch, nil
}
return "", fmt.Errorf("%w: %q (no exact or partial match)", ErrNoValidService, keyword)
}
type cacheItem struct {
services []string
expires time.Time
exists bool
}
var (
discoveryCache sync.Map
discoveryGroup singleflight.Group
)
func discoverServices(keyword string) ([]string, error) {
result, err := discoveryGroup.Do(keyword, func() (interface{}, error) {
if cached, ok := discoveryCache.Load(keyword); ok {
item := cached.(cacheItem)
if time.Now().Before(item.expires) {
return item.services, nil
}
discoveryCache.Delete(keyword)
}
manager := GetGlobalManager()
results, err := manager.FindServices(keyword)
if err != nil {
global.LOG.Errorf("Find services failed for %s: %v", keyword, err)
return nil, fmt.Errorf("%w: %q (%v)", ErrServiceDiscovery, keyword, err)
} else {
discoveryCache.Store(keyword, cacheItem{
services: results,
expires: time.Now().Add(5 * time.Minute),
})
}
return results, err
})
if err != nil {
return nil, err
}
return result.([]string), nil
}
func updateAliases(keyword, alias string) {
if keyword == alias {
return
}
existing, _ := serviceAliases.LoadOrStore(keyword, []string{})
aliases := existing.([]string)
if contains(aliases, alias) {
return
}
serviceAliases.Store(keyword, append(aliases, alias))
go scheduleSave()
}
func scheduleSave() {
saveMutex.Lock()
defer saveMutex.Unlock()
if saveTimer != nil {
saveTimer.Stop()
}
dataSnapshot := make(map[string][]string)
serviceAliases.Range(func(k, v interface{}) bool {
dataSnapshot[k.(string)] = append([]string{}, v.([]string)...)
return true
})
aliasFile = filepath.Join(constant.ResourceDir, "svcaliases.json")
saveTimer = time.AfterFunc(afterSaveTime, func() {
tmpFile := aliasFile + ".tmp"
if err := saveAliasesToFile(dataSnapshot, tmpFile); err == nil {
os.Rename(tmpFile, aliasFile)
}
})
}
func saveAliasesToFile(data map[string][]string, path string) error {
fileData, err := json.MarshalIndent(data, "", " ")
if err != nil {
return fmt.Errorf("serialization failed: %w", err)
}
if err := os.WriteFile(path, fileData, 0644); err != nil {
return fmt.Errorf("file write failed: %w", err)
}
return nil
}
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
var serviceExistenceCache sync.Map
func confirmServiceExists(serviceName string) (bool, error) {
if val, ok := serviceExistenceCache.Load(serviceName); ok {
if item, ok := val.(cacheItem); ok && time.Now().Before(item.expires) {
return item.exists, nil
}
serviceExistenceCache.Delete(serviceName)
}
handler := NewServiceHandler(defaultServiceConfig(serviceName))
isExist, err := handler.IsExists()
if err != nil {
return false, fmt.Errorf("check service existence failed: %w", err)
}
serviceExistenceCache.Store(serviceName, cacheItem{
exists: isExist.IsExists,
expires: time.Now().Add(30 * time.Second),
})
return isExist.IsExists, nil
}
func getAliases(keyword string) []string {
predefined := loadPredefinedAliases()[keyword]
runtimeAliases, _ := serviceAliases.LoadOrStore(keyword, []string{})
merged := make(map[string]struct{})
for _, alias := range predefined {
merged[alias] = struct{}{}
}
for _, alias := range runtimeAliases.([]string) {
merged[alias] = struct{}{}
}
result := make([]string, 0, len(merged))
for k := range merged {
result = append(result, k)
}
return result
}
type ConfigOption struct {
TailLines string
}
func ViewConfig(path string, opt ConfigOption) (string, error) {
var cmd []string
if opt.TailLines != "" && opt.TailLines != "0" {
cmd = []string{"tail", "-n", opt.TailLines, path}
} else {
cmd = []string{"cat", path}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
output, err := executeCommand(ctx, cmd[0], cmd[1:]...)
if err != nil {
return "", fmt.Errorf("view config failed: %w", err)
}
return string(output), nil
}
+283
View File
@@ -0,0 +1,283 @@
package systemctl
import (
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
)
type ServiceConfig struct {
ServiceName map[string]string
}
type ServiceHandler struct {
config *ServiceConfig
manager ServiceManager
}
func NewServiceHandler(serviceNames map[string]string) *ServiceHandler {
mgr := GetGlobalManager()
if mgr == nil {
global.LOG.Error("failed to get global service manager when creating ServiceHandler")
return nil
}
return &ServiceHandler{
config: &ServiceConfig{
ServiceName: serviceNames,
},
manager: mgr,
}
}
type ServiceStatus struct {
IsActive bool `json:"isActive"`
IsEnabled bool `json:"isEnabled"`
IsExists bool `json:"isExists"`
Output string `json:"output"`
}
type ServiceIsActive struct {
IsActive bool `json:"isActive"`
Output string `json:"output"`
}
type ServiceIsEnabled struct {
IsEnabled bool `json:"isEnabled"`
Output string `json:"output"`
}
type ServiceResult struct {
Success bool `json:"success"`
Message string `json:"message"`
Output string `json:"output"`
}
var (
BinaryPath = "/usr/local/bin" // 1panl service default path
ErrServiceNotExist = errors.New("service does not exist")
)
func defaultServiceConfig(serviceName string) map[string]string {
mgr := getManagerName()
if mgr == "" {
global.LOG.Error("failed to get manager name for default service config")
return nil
}
return map[string]string{
mgr: serviceName,
}
}
func (h *ServiceHandler) ManagerName() string { return h.manager.Name() }
func getManagerName() string {
if mgr := GetGlobalManager(); mgr != nil {
return mgr.Name()
}
global.LOG.Error("failed to get global service manager")
return ""
}
func (h *ServiceHandler) GetServiceName() string {
manager := h.ManagerName()
if manager == "" {
global.LOG.Error("manager name is empty when getting service name")
return ""
}
return h.config.ServiceName[manager]
}
func (h *ServiceHandler) GetServicePath() (string, error) {
manager := h.ManagerName()
serviceName := h.config.ServiceName[manager]
if serviceName == "" {
err := fmt.Errorf("service name not found for %s", manager)
global.LOG.Errorf("GetServicePath error: %v", err)
return "", err
}
cleanPath := filepath.Clean(serviceName)
if strings.Contains(cleanPath, "..") {
err := fmt.Errorf("invalid path: %q", cleanPath)
global.LOG.Errorf("GetServicePath security check failed: %v", err)
return "", err
}
switch manager {
case "systemd":
return findSystemdPath(cleanPath)
case "openrc", "sysvinit":
return checkInitDPath(cleanPath)
default:
err := fmt.Errorf("unsupported init system: %s", manager)
global.LOG.Errorf("GetServicePath error: %v", err)
return "", err
}
}
func findSystemdPath(name string) (string, error) {
paths := []string{"/etc/systemd/system", "/usr/lib/systemd/system",
"/usr/share/systemd/system", "/usr/local/lib/systemd/system"}
for _, p := range paths {
if path := filepath.Join(p, name); FileExist(path) {
return path, nil
}
}
err := fmt.Errorf("service path not found for %s", name)
global.LOG.Errorf("findSystemdPath error: %v", err)
return "", err
}
func checkInitDPath(name string) (string, error) {
path := filepath.Join("/etc/init.d", name)
if !FileExist(path) {
err := fmt.Errorf("service path not found for %s", name)
global.LOG.Errorf("checkInitDPath error: %v", err)
return "", err
}
return path, nil
}
func (h *ServiceHandler) ExecuteAction(action string) (ServiceResult, error) {
successMsg := fmt.Sprintf("%s : %s completed", action, h.GetServiceName())
return h.executeAction(action, successMsg)
}
func (h *ServiceHandler) IsExists() (ServiceStatus, error) {
manager := GetGlobalManager()
isExist, _ := manager.ServiceExists(h.config)
return ServiceStatus{
IsExists: isExist,
}, nil
}
func (h *ServiceHandler) IsActive() (ServiceStatus, error) {
manager := GetGlobalManager()
if manager == nil {
global.LOG.Error("service manager not initialized during active check")
return ServiceStatus{}, fmt.Errorf("service manager not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
activeCmd, err := manager.BuildCommand("status", h.config)
if err != nil {
global.LOG.Errorf("Build status command failed: %v", err)
return ServiceStatus{}, fmt.Errorf("build status command failed: %w", err)
}
output, err := executeCommand(ctx, activeCmd[0], activeCmd[1:]...)
if err != nil {
if strings.Contains(err.Error(), "inactive") {
return ServiceStatus{
IsExists: false,
}, nil
}
global.LOG.Errorf("Active check execution failed: %v", err)
return ServiceStatus{}, fmt.Errorf("status check failed: %w", err)
}
isActive, err := manager.ParseStatus(string(output), h.config, "active")
if err != nil {
global.LOG.Warnf("Status parse error: %v", err)
}
return ServiceStatus{
IsActive: isActive,
Output: string(output),
}, nil
}
func (h *ServiceHandler) IsEnabled() (ServiceStatus, error) {
manager := GetGlobalManager()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
enabledCmd, err := manager.BuildCommand("is-enabled", h.config)
if err != nil {
if strings.Contains(err.Error(), "not found") {
return ServiceStatus{
IsEnabled: false,
}, nil
}
global.LOG.Errorf("Build is-enabled command failed: %v", err)
return ServiceStatus{}, fmt.Errorf("build enabled check command failed: %w", err)
}
output, err := executeCommand(ctx, enabledCmd[0], enabledCmd[1:]...)
if err != nil {
if strings.Contains(err.Error(), "disabled") {
return ServiceStatus{
IsEnabled: false,
}, nil
}
// // isEnabled, err := h.ParseStatus(string(output), h.config, "enabled")
// global.LOG.Errorf("Enabled check execution failed: %v", err)
// return ServiceStatus{}, fmt.Errorf("enabled check failed: %w", err)
}
isEnabled, err := manager.ParseStatus(string(output), h.config, "enabled")
if err != nil {
global.LOG.Warnf("Enabled status parse error: %v", err)
}
return ServiceStatus{
IsEnabled: isEnabled,
Output: string(output),
}, nil
}
func (h *ServiceHandler) StartService() (ServiceResult, error) {
return h.ExecuteAction("start")
}
func (h *ServiceHandler) StopService() (ServiceResult, error) {
return h.ExecuteAction("stop")
}
func (h *ServiceHandler) RestartService() (ServiceResult, error) {
return h.ExecuteAction("restart")
}
func (h *ServiceHandler) EnableService() (ServiceResult, error) {
return h.ExecuteAction("enable")
}
func (h *ServiceHandler) DisableService() (ServiceResult, error) {
return h.ExecuteAction("disable")
}
func (h *ServiceHandler) executeAction(action, successMsg string) (ServiceResult, error) {
manager := GetGlobalManager()
if manager == nil {
global.LOG.Error("service manager not initialized during action execution")
return ServiceResult{}, fmt.Errorf("service manager not initialized")
}
ctx, cancel := context.WithTimeout(context.Background(), defaultCommandTimeout)
defer cancel()
cmdArgs, err := manager.BuildCommand(action, h.config)
if err != nil {
global.LOG.Errorf("Build command failed for action %s: %v", action, err)
return ServiceResult{}, fmt.Errorf("build command failed: %w", err)
}
output, err := executeCommand(ctx, cmdArgs[0], cmdArgs[1:]...)
if err != nil {
global.LOG.Errorf("%s operation failed: %v", action, err)
return ServiceResult{
Success: false,
Message: fmt.Sprintf("%s failed", action),
Output: string(output),
}, fmt.Errorf("%s operation failed: %w", action, err)
}
global.LOG.Infof("[%s]: %s", manager.Name(), successMsg)
return ServiceResult{
Success: true,
Message: successMsg,
Output: string(output),
}, nil
}
+440
View File
@@ -0,0 +1,440 @@
package systemctl
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/backend/global"
)
var (
managers = make(map[string]ServiceManager)
mu sync.RWMutex
globalManager ServiceManager
managerPriority = []string{"systemd", "openrc", "sysvinit"}
)
const (
defaultCommandTimeout = 60 * time.Second
serviceCheckTimeout = 5 * time.Second
)
type ServiceManager interface {
Name() string
IsAvailable() bool
ServiceExists(*ServiceConfig) (bool, error)
BuildCommand(string, *ServiceConfig) ([]string, error)
ParseStatus(string, *ServiceConfig, string) (bool, error)
FindServices(string) ([]string, error)
}
type baseManager struct {
name string
cmdTool string
activeRegex *regexp.Regexp
enabledRegex *regexp.Regexp
}
func (b *baseManager) Name() string { return b.name }
func isRootUser() bool {
return os.Geteuid() == 0
}
func (b *baseManager) buildBaseCommand() []string {
var cmdArgs []string
if !isRootUser() {
cmdArgs = append(cmdArgs, "sudo")
}
cmdArgs = append(cmdArgs, b.cmdTool)
return cmdArgs
}
func (b *baseManager) commonServiceExists(config *ServiceConfig, checkFn func(string) (bool, error)) (bool, error) {
if name := config.ServiceName[b.name]; name != "" {
exists, checkErr := checkFn(name)
if checkErr != nil {
return false, nil
}
return exists, nil
}
return false, nil
}
func (b *baseManager) ParseStatus(output string, _ *ServiceConfig, statusType string) (bool, error) {
if output == "" {
return false, nil
}
switch statusType {
case "active":
if b.activeRegex == nil {
return false, nil
}
return b.activeRegex.MatchString(output), nil
case "enabled":
if b.enabledRegex == nil {
return false, nil
}
return b.enabledRegex.MatchString(output), nil
default:
return false, nil
}
}
func registerManager(m ServiceManager) {
mu.Lock()
defer mu.Unlock()
managers[m.Name()] = m
}
func init() {
for _, mgr := range []ServiceManager{
newSystemdManager(),
newOpenrcManager(),
newSysvinitManager(),
} {
registerManager(mgr)
}
}
func InitializeGlobalManager() (err error) {
for _, name := range managerPriority {
if mgr, ok := managers[name]; ok && mgr.IsAvailable() {
if testManager(mgr) {
globalManager = mgr
global.LOG.Infof("Initialized service manager: %s", name)
return
}
}
}
err = fmt.Errorf("no available service manager found (tried: %v)", managerPriority)
return
}
func testManager(mgr ServiceManager) bool {
_, err := mgr.BuildCommand("status", &ServiceConfig{
ServiceName: map[string]string{mgr.Name(): "test-service"},
})
return err == nil
}
func GetGlobalManager() ServiceManager {
if globalManager == nil {
mu.Lock()
defer mu.Unlock()
if globalManager == nil {
return initializeWithRetry()
}
}
return globalManager
}
func initializeWithRetry() ServiceManager {
const (
maxRetries = 5
initialWait = 1 * time.Second
)
backoff := initialWait
for attempt := 1; attempt <= maxRetries; attempt++ {
if err := InitializeGlobalManager(); err == nil {
return globalManager
}
logMessage := fmt.Sprintf("Manager init attempt %d/%d failed", attempt, maxRetries)
if global.LOG != nil {
global.LOG.Warn(logMessage)
} else {
fmt.Printf("[WARN] %s\n", logMessage)
}
if attempt < maxRetries {
time.Sleep(backoff)
backoff *= 2
}
}
if global.LOG != nil {
global.LOG.Error("All manager initialization attempts failed")
} else {
fmt.Println("[FATAL] All manager initialization attempts failed")
}
panic("unable to initialize service manager")
}
func executeCommand(ctx context.Context, command string, args ...string) ([]byte, error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, defaultCommandTimeout)
defer cancel()
}
cmd := exec.CommandContext(ctx, command, args...)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if err := cmd.Run(); err != nil {
return buf.Bytes(), &CommandError{
Cmd: cmd.String(),
Output: buf.String(),
Err: err,
}
}
return buf.Bytes(), nil
}
type systemdManager struct{ baseManager }
func newSystemdManager() ServiceManager {
return &systemdManager{baseManager{
name: "systemd",
cmdTool: "systemctl",
activeRegex: regexp.MustCompile(`(?i)Active:\s+active\b`),
enabledRegex: regexp.MustCompile(`(?i)^\s*enabled\s*$`),
}}
}
func (m *systemdManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *systemdManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), serviceCheckTimeout)
defer cancel()
out, cmdErr := executeCommand(ctx, m.cmdTool, "is-enabled", name)
if cmdErr == nil || strings.Contains(string(out), "disabled") {
return true, nil
}
return false, nil
})
}
func (m *systemdManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
cmdArgs := m.buildBaseCommand()
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
cmdArgs = append(cmdArgs, "is-enabled", service)
default:
cmdArgs = append(cmdArgs, action, service)
}
return cmdArgs, nil
}
func (m *systemdManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
if strings.Contains(output, "could not be found") {
return false, nil
}
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
return false, nil
}
return result, nil
}
func (m *systemdManager) FindServices(keyword string) ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
out, err := executeCommand(ctx, m.cmdTool, "list-unit-files", "--type=service", "--no-legend")
if err != nil {
return nil, fmt.Errorf("failed to list systemd services: %w", err)
}
var services []string
lines := strings.Split(string(out), "\n")
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 1 {
continue
}
serviceName := fields[0]
if strings.Contains(serviceName, keyword) {
services = append(services, serviceName)
}
}
return services, nil
}
type sysvinitManager struct{ baseManager }
func newSysvinitManager() ServiceManager {
return &sysvinitManager{baseManager{
name: "sysvinit",
cmdTool: "service",
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
}}
}
func (m *sysvinitManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *sysvinitManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
_, err := os.Stat(filepath.Join("/etc/init.d", name))
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
}
return true, nil
})
}
func (m *sysvinitManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
return []string{
"sh",
"-c",
fmt.Sprintf("if ls /etc/rc*.d/S*%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
case "is-active", "status":
return []string{
"sh",
"-c",
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
}, nil
default:
cmdArgs := m.buildBaseCommand()
cmdArgs = append(cmdArgs, service, action)
return cmdArgs, nil
}
}
func (m *sysvinitManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
serviceName := config.ServiceName[m.name]
switch statusType {
case "enabled":
if strings.Contains(output, "no such file or directory") {
return false, nil
}
return strings.TrimSpace(output) != "", nil
case "active":
if strings.Contains(output, "not found") {
return false, nil
}
if strings.Contains(output, "running") || strings.Contains(output, "active") {
return true, nil
}
default:
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
global.LOG.Debugf("[sysvinit] Status parse failed. [ServiceName:%s] Type: %s, Output: %q", serviceName, statusType, output)
}
return result, err
}
return false, fmt.Errorf("unsupported status type: %s", statusType)
}
func (m *sysvinitManager) FindServices(keyword string) ([]string, error) {
files, err := os.ReadDir("/etc/init.d/")
if err != nil {
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
}
var services []string
for _, file := range files {
if strings.Contains(file.Name(), keyword) {
services = append(services, file.Name())
}
}
return services, nil
}
type openrcManager struct{ baseManager }
func newOpenrcManager() ServiceManager {
return &openrcManager{baseManager{
name: "openrc",
cmdTool: "rc-service",
// activeRegex: regexp.MustCompile(`(?i)^\s*status:\s+(started|running|active)\s*$`),
// enabledRegex: regexp.MustCompile(`(?i)^[^\|]+\|\s*(default|enabled)\b.*$`),
activeRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(running|active)\b(?:$|\s)`),
enabledRegex: regexp.MustCompile(`(?i)(?:^|\s)\b(enabled)\b(?:$|\s)`),
}}
}
func (m *openrcManager) IsAvailable() bool {
_, err := exec.LookPath(m.cmdTool)
return err == nil
}
func (m *openrcManager) ServiceExists(config *ServiceConfig) (bool, error) {
return m.commonServiceExists(config, func(name string) (bool, error) {
_, err := os.Stat(filepath.Join("/etc/init.d", name))
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat /etc/init.d/%s failed: %w", name, err)
}
return true, nil
})
}
func (m *openrcManager) BuildCommand(action string, config *ServiceConfig) ([]string, error) {
cmdArgs := m.buildBaseCommand()
service := config.ServiceName[m.name]
switch action {
case "is-enabled":
return []string{
"sh",
"-c",
fmt.Sprintf("if ls /etc/runlevels/default/%s >/dev/null 2>&1; then echo 'enabled'; else echo 'disabled'; fi", service)}, nil
case "is-active", "status":
return []string{
"sh",
"-c",
fmt.Sprintf("if service %s status >/dev/null 2>&1; then echo 'active'; else echo 'inactive'; fi", service),
}, nil
case "enable":
return []string{"rc-update", "add", service, "default"}, nil
case "disable":
return []string{"rc-update", "del", service, "default"}, nil
default:
cmdArgs = append(cmdArgs, service, action)
return cmdArgs, nil
}
}
func (m *openrcManager) ParseStatus(output string, config *ServiceConfig, statusType string) (bool, error) {
if strings.Contains(output, "does not exist") {
return false, nil
}
result, err := m.baseManager.ParseStatus(output, config, statusType)
if err != nil {
return false, nil
}
return result, nil
}
func (m *openrcManager) FindServices(keyword string) ([]string, error) {
files, err := os.ReadDir("/etc/init.d/")
if err != nil {
return nil, fmt.Errorf("failed to read init.d directory: %w", err)
}
keyword = strings.ToLower(keyword)
var services []string
for _, file := range files {
if strings.Contains(file.Name(), keyword) {
services = append(services, file.Name())
}
}
return services, nil
}
type CommandError struct {
Cmd string
Err error
Output string
}
func (e CommandError) Error() string {
return fmt.Sprintf("command %q failed: %v \nOutput: %s",
e.Cmd, e.Err, e.Output)
}
+121 -31
View File
@@ -2,63 +2,153 @@ package systemctl
import ( import (
"fmt" "fmt"
"github.com/pkg/errors" "os"
"os/exec"
"strings" "github.com/1Panel-dev/1Panel/backend/global"
) )
func RunSystemCtl(args ...string) (string, error) { func DefaultHandler(serviceName string) (*ServiceHandler, error) {
cmd := exec.Command("systemctl", args...) svcName, err := smartServiceName(serviceName)
output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return string(output), fmt.Errorf("failed to run command: %w", err) return nil, ErrServiceNotFound
} }
return string(output), nil return NewServiceHandler(defaultServiceConfig(svcName)), nil
} }
func IsActive(serviceName string) (bool, error) { func GetServiceName(serviceName string) (string, error) {
out, err := RunSystemCtl("is-active", serviceName) serviceName, err := smartServiceName(serviceName)
if err != nil { if err != nil {
return false, err return "", ErrServiceNotFound
} }
return out == "active\n", nil return serviceName, nil
} }
func IsEnable(serviceName string) (bool, error) { func GetServicePath(serviceName string) (string, error) {
out, err := RunSystemCtl("is-enabled", serviceName) handler, err := DefaultHandler(serviceName)
if err != nil { if err != nil {
return false, err return "", ErrServiceNotFound
} }
return out == "enabled\n", nil return handler.GetServicePath()
}
func CustomAction(action string, serviceName string) (ServiceResult, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
global.LOG.Errorf("CustomAction handler init failed: %v", err)
return ServiceResult{}, ErrServiceNotFound
}
result, err := handler.ExecuteAction(action)
if err != nil {
global.LOG.Errorf("CustomAction %s failed: %v", action, err)
return result, fmt.Errorf("%s operation failed: %w | Output: %s", action, err, result.Output)
}
return result, nil
} }
func IsExist(serviceName string) (bool, error) { func IsExist(serviceName string) (bool, error) {
out, err := RunSystemCtl("is-enabled", serviceName) handler, err := DefaultHandler(serviceName)
if err != nil { if err != nil {
if strings.Contains(out, "disabled") {
return true, nil
}
return false, nil return false, nil
} }
return true, nil result, _ := handler.IsExists()
if result.IsExists {
return true, nil
} else {
return false, nil
}
} }
func handlerErr(out string, err error) error { func Start(serviceName string) error {
handler, _ := DefaultHandler(serviceName)
result, err := handler.StartService()
if err != nil { if err != nil {
if out != "" { global.LOG.Errorf("Service start failed: %v | Output: %s", err, result.Output)
return errors.New(out) return fmt.Errorf("start failed: %v | Output: %s", err, result.Output)
} }
return err return nil
}
func Stop(serviceName string) error {
handler, err := DefaultHandler(serviceName)
if err != nil {
global.LOG.Errorf("Stop handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.StopService()
if err != nil {
global.LOG.Errorf("Service stop failed: %v", err)
return fmt.Errorf("stop failed: %v | Output: %s", err, result.Output)
} }
return nil return nil
} }
func Restart(serviceName string) error { func Restart(serviceName string) error {
out, err := RunSystemCtl("restart", serviceName) handler, err := DefaultHandler(serviceName)
return handlerErr(out, err) if err != nil {
global.LOG.Errorf("Restart handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.RestartService()
if err != nil {
global.LOG.Errorf("Service restart failed: %v", err)
return fmt.Errorf("restart failed: %v | Output: %s", err, result.Output)
}
return nil
} }
func Operate(operate, serviceName string) error { func Enable(serviceName string) error {
out, err := RunSystemCtl(operate, serviceName) handler, err := DefaultHandler(serviceName)
return handlerErr(out, err) if err != nil {
global.LOG.Errorf("Enable handler init failed: %v", err)
return fmt.Errorf("%s is not exist", serviceName)
}
result, err := handler.EnableService()
if err != nil {
global.LOG.Errorf("Service enable failed: %v | Output: %s", err, result.Output)
return fmt.Errorf("%s enable failed: %v ", serviceName, err)
}
return nil
}
func Disable(serviceName string) error {
handler, _ := DefaultHandler(serviceName)
result, err := handler.DisableService()
if err != nil {
global.LOG.Errorf("Service disable failed: %v", err)
return fmt.Errorf("disable failed: %v | Output: %s", err, result.Output)
}
return nil
}
func IsActive(serviceName string) (bool, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
return false, nil
}
status, err := handler.IsActive()
if err != nil {
return false, nil
}
return status.IsActive, nil
}
func IsEnable(serviceName string) (bool, error) {
handler, err := DefaultHandler(serviceName)
if err != nil {
return false, nil
}
status, err := handler.IsEnabled()
if err != nil {
return false, nil
}
return status.IsEnabled, nil
}
type LogOption struct {
TailLines string
}
func FileExist(path string) bool {
_, err := os.Stat(path)
return err == nil
} }
+115 -27
View File
@@ -3,6 +3,7 @@ package toolbox
import ( import (
"fmt" "fmt"
"os" "os"
"regexp"
"strings" "strings"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
@@ -22,15 +23,15 @@ type FirewallClient interface {
} }
func NewFail2Ban() (*Fail2ban, error) { func NewFail2Ban() (*Fail2ban, error) {
isExist, _ := systemctl.IsExist("fail2ban.service") isExist, _ := systemctl.IsExist("fail2ban")
if isExist { if isExist {
if _, err := os.Stat(defaultPath); err != nil { if _, err := os.Stat(defaultPath); err != nil {
if err := initLocalFile(); err != nil { if err := initLocalFile(); err != nil {
return nil, err return nil, err
} }
stdout, err := cmd.Exec("systemctl restart fail2ban.service") err := systemctl.Restart("fail2ban")
if err != nil { if err != nil {
global.LOG.Errorf("restart fail2ban failed, err: %s", stdout) global.LOG.Errorf("restart fail2ban failed, err: %s", err)
return nil, err return nil, err
} }
} }
@@ -47,20 +48,25 @@ func (f *Fail2ban) Status() (bool, bool, bool) {
} }
func (f *Fail2ban) Version() string { func (f *Fail2ban) Version() string {
stdout, err := cmd.Exec("fail2ban-client version") stdout, err := cmd.Exec("fail2ban-client --version")
if err != nil { if err != nil {
global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout) global.LOG.Errorf("load the fail2ban version failed, err: %s", stdout)
return "-" return "-"
} }
return strings.ReplaceAll(stdout, "\n", "") versionRe := regexp.MustCompile(`(?i)fail2ban[:\s-]*v?(\d+\.\d+\.\d+)`)
matches := versionRe.FindStringSubmatch(stdout)
if len(matches) > 1 {
return matches[1]
}
global.LOG.Errorf("Version regex failed to match output: %s", stdout)
return "-"
} }
func (f *Fail2ban) Operate(operate string) error { func (f *Fail2ban) Operate(operate string) error {
switch operate { switch operate {
case "start", "restart", "stop", "enable", "disable": case "start", "restart", "stop", "enable", "disable":
stdout, err := cmd.Execf("systemctl %s fail2ban.service", operate) stdout, err := systemctl.CustomAction(operate, "fail2ban")
if err != nil { if err != nil {
return fmt.Errorf("%s the fail2ban.service failed, err: %s", operate, stdout) return fmt.Errorf("%s the fail2ban failed, err: %s", operate, stdout.Output)
} }
return nil return nil
case "reload": case "reload":
@@ -136,7 +142,32 @@ func initLocalFile() error {
return err return err
} }
defer f.Close() defer f.Close()
initFile := `#DEFAULT-START var initFile string
if systemctl.GetGlobalManager().Name() == "openrc" {
initFile = `[sshd]
enabled = true
filter = alpine-sshd
port = $ssh_port
logpath = $logpath
maxretry = 2
banaction = $banaction
[sshd-ddos]
enabled = true
filter = alpine-sshd-ddos
port = $ssh_port
logpath = /var/log/messages
maxretry = 2
[sshd-key]
enabled = true
filter = alpine-sshd-key
port = $ssh_port
logpath = /var/log/messages
maxretry = 2
`
} else {
initFile = `# DEFAULT-START
[DEFAULT] [DEFAULT]
bantime = 600 bantime = 600
findtime = 300 findtime = 300
@@ -146,36 +177,93 @@ action = %(action_mwl)s
#DEFAULT-END #DEFAULT-END
[sshd] [sshd]
ignoreip = 127.0.0.1/8 ignoreip = 127.0.0.1/8 ::1
enabled = true enabled = true
filter = sshd filter = sshd
port = 22 port = $ssh_port
maxretry = 5 maxretry = 5
findtime = 300 findtime = 300
bantime = 600 bantime = 600
banaction = $banaction banaction = $banaction
action = %(action_mwl)s action = %(action_mwl)s
logpath = $logpath` logpath = $logpath
maxmatches = 3
banaction := "" `
if active, _ := systemctl.IsActive("firewalld"); active {
banaction = "firewallcmd-ipset"
} else if active, _ := systemctl.IsActive("ufw"); active {
banaction = "ufw"
} else {
banaction = "iptables-allports"
} }
// 检测防火墙类型
banaction := detectFirewall()
// 检测SSH端口(支持Alpine的sshd_config位置)
sshPort := detectSSHPort()
// 检测日志路径(兼容Alpine的多种日志位置)
logPath := detectAuthLogPath()
// 执行变量替换
initFile = strings.ReplaceAll(initFile, "$banaction", banaction) initFile = strings.ReplaceAll(initFile, "$banaction", banaction)
logPath := ""
if _, err := os.Stat("/var/log/secure"); err == nil {
logPath = "/var/log/secure"
} else {
logPath = "/var/log/auth.log"
}
initFile = strings.ReplaceAll(initFile, "$logpath", logPath) initFile = strings.ReplaceAll(initFile, "$logpath", logPath)
initFile = strings.ReplaceAll(initFile, "$ssh_port", sshPort)
if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil { if err := os.WriteFile(defaultPath, []byte(initFile), 0640); err != nil {
return err return err
} }
return nil return nil
} }
func detectFirewall() string {
if active, _ := systemctl.IsActive("firewalld"); active {
return "firewallcmd-ipset"
}
if active, _ := systemctl.IsActive("ufw"); active {
return "ufw"
}
if active, _ := systemctl.IsActive("iptables"); active {
return "iptables-allports"
}
if active, _ := systemctl.IsActive("nftables"); active {
return "nftables-allports"
}
return "iptables-allports"
}
func detectAuthLogPath() string {
paths := []string{
"/var/log/auth.log", // 常见默认路径
"/var/log/messages", // Alpine系统日志
"/var/log/secure", // RHEL风格路径
"/var/log/syslog", // Debian风格路径
}
for _, path := range paths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return "/var/log/auth.log"
}
func detectSSHPort() string {
// 检查标准sshd_config路径
configPaths := []string{
"/etc/ssh/sshd_config",
"/etc/sshd_config",
}
for _, path := range configPaths {
data, err := os.ReadFile(path)
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "Port") {
parts := strings.Fields(line)
if len(parts) >= 2 {
return parts[1]
}
}
}
}
return "22" // 默认SSH端口
}
+4 -3
View File
@@ -4,7 +4,6 @@ import (
"bufio" "bufio"
"errors" "errors"
"fmt" "fmt"
"golang.org/x/crypto/bcrypt"
"os" "os"
"os/user" "os/user"
"path" "path"
@@ -12,6 +11,8 @@ import (
"strings" "strings"
"time" "time"
"golang.org/x/crypto/bcrypt"
"github.com/1Panel-dev/1Panel/backend/constant" "github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global" "github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd" "github.com/1Panel-dev/1Panel/backend/utils/cmd"
@@ -94,9 +95,9 @@ func (f *Ftp) Status() (bool, bool) {
func (f *Ftp) Operate(operate string) error { func (f *Ftp) Operate(operate string) error {
switch operate { switch operate {
case "start", "restart", "stop": case "start", "restart", "stop":
stdout, err := cmd.Execf("systemctl %s pure-ftpd.service", operate) stdout, err := systemctl.CustomAction(operate, "pure-ftpd")
if err != nil { if err != nil {
return fmt.Errorf("%s the pure-ftpd.service failed, err: %s", operate, stdout) return fmt.Errorf("%s the pure-ftpd service failed, err: %s", operate, stdout.Output)
} }
return nil return nil
default: default:
+20 -13
View File
@@ -6,25 +6,32 @@ command -v wget >/dev/null || {
} }
if [ ! -f "1pctl" ]; then if [ ! -f "1pctl" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/1pctl wget https://github.com/1Panel-dev/installer/raw/v1/1pctl
fi
if [ ! -f "1panel.service" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/1panel.service
fi fi
if [ ! -f "install.sh" ]; then if [ ! -f "install.sh" ]; then
wget https://github.com/1Panel-dev/installer/raw/main/install.sh wget https://github.com/1Panel-dev/installer/raw/v1/install.sh
fi
if [ ! -d "initscript" ]; then
wget https://github.com/1Panel-dev/installer/raw/v1/initscript/1panel.service
mkdir -p initscript && cd initscript
for file in 1panel.service 1paneld.init 1paneld.openrc 1paneld.procd; do
wget -q https://github.com/1Panel-dev/installer/raw/v1/initscript/$file
done
cd ..
fi
if [ ! -d "lang" ]; then
mkdir -p lang && cd lang
for lang in en fa pt-BR ru zh; do
wget -q https://github.com/1Panel-dev/installer/raw/v1/lang/$lang.sh
done
cd ..
fi fi
if [ ! -f "GeoIP.mmdb" ]; then if [ ! -f "GeoIP.mmdb" ]; then
wget https://resource.1panel.pro/geo/GeoIP.mmdb wget https://resource.fit2cloud.com/1panel/package/geo/GeoIP.mmdb
fi
if [ ! -f "lang.tar.gz" ]; then
wget https://resource.1panel.pro/language/lang.tar.gz
tar zxvf lang.tar.gz
rm -rf lang.tar.gz
fi fi
chmod 755 1pctl install.sh chmod 755 1pctl install.sh
+16 -2
View File
@@ -58,8 +58,15 @@ var restoreCmd = &cobra.Command{
geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo") geoPath := path.Join(global.CONF.System.BaseDir, "1panel/geo")
_, _ = cmdUtils.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpPath, "GeoIP.mmdb"), geoPath) _, _ = cmdUtils.Execf("mkdir %s && cp %s %s/", geoPath, path.Join(tmpPath, "GeoIP.mmdb"), geoPath)
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep3")) fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep3"))
if err := common.CopyFile(path.Join(tmpPath, "1panel.service"), "/etc/systemd/system"); err != nil { if isSystemd() {
return err if err := common.CopyFile(path.Join(tmpPath, "1panel.service"), "/etc/systemd/system"); err != nil {
return err
}
} else {
if err := common.CopyFile(path.Join(tmpPath, "1paneld"), "/etc/init.d"); err != nil {
return err
}
} }
fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4")) fmt.Println(i18n.GetMsgByKeyForCmd("RestoreStep4"))
checkPointOfWal() checkPointOfWal()
@@ -124,3 +131,10 @@ func handleUnTar(sourceFile, targetDir string) error {
} }
return nil return nil
} }
func isSystemd() bool {
if _, err := os.Stat("/etc/systemd/system"); err == nil {
return true
}
return false
}
+1
View File
@@ -7,6 +7,7 @@ location ^~ /test {
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection; proxy_set_header Connection $http_connection;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_http_version 1.1; proxy_http_version 1.1;
add_header X-Cache $upstream_cache_status; add_header X-Cache $upstream_cache_status;
add_header Cache-Control no-cache; add_header Cache-Control no-cache;
+1
View File
@@ -28,3 +28,4 @@ stats.html
*.sln *.sln
*.sw? *.sw?
.vscode .vscode
*.code-workspace
+5 -4
View File
@@ -38,8 +38,9 @@
"axios": "^1.7.2", "axios": "^1.7.2",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dompurify": "^3.3.1",
"echarts": "^5.5.0", "echarts": "^5.5.0",
"element-plus": "^2.7.5", "element-plus": "2.9.9",
"fit2cloud-ui-plus": "^1.2.0", "fit2cloud-ui-plus": "^1.2.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
@@ -64,8 +65,8 @@
"@types/node": "^20.15.0", "@types/node": "^20.15.0",
"@typescript-eslint/eslint-plugin": "^5.22.0", "@typescript-eslint/eslint-plugin": "^5.22.0",
"@typescript-eslint/parser": "^5.22.0", "@typescript-eslint/parser": "^5.22.0",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.0.0", "@vitejs/plugin-vue-jsx": "^5.1.1",
"autoprefixer": "^10.4.7", "autoprefixer": "^10.4.7",
"commitizen": "^4.2.4", "commitizen": "^4.2.4",
"eslint": "^8.43.0", "eslint": "^8.43.0",
@@ -84,7 +85,7 @@
"typescript": "^4.5.4", "typescript": "^4.5.4",
"unplugin-auto-import": "^0.16.4", "unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.0", "unplugin-vue-components": "^0.25.0",
"vite": "^6.0.7", "vite": "^7.1.5",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2",
+4 -1
View File
@@ -2,7 +2,6 @@ export namespace Login {
export interface ReqLoginForm { export interface ReqLoginForm {
name: string; name: string;
password: string; password: string;
ignoreCaptcha: boolean;
captcha: string; captcha: string;
captchaID: string; captchaID: string;
authMethod: string; authMethod: string;
@@ -26,4 +25,8 @@ export namespace Login {
export interface ResAuthButtons { export interface ResAuthButtons {
[propName: string]: any; [propName: string]: any;
} }
export interface LoginSetting {
language: string;
needCaptcha: boolean;
}
} }
-3
View File
@@ -212,9 +212,6 @@ export namespace Container {
password: string; password: string;
auth: boolean; auth: boolean;
} }
export interface RepoDelete {
ids: Array<number>;
}
export interface RepoInfo { export interface RepoInfo {
id: number; id: number;
createdAt: Date; createdAt: Date;
+2 -2
View File
@@ -21,8 +21,8 @@ export const checkIsDemo = () => {
return http.get<boolean>('/auth/demo'); return http.get<boolean>('/auth/demo');
}; };
export const getLanguage = () => { export const getAuthSetting = () => {
return http.get<string>(`/auth/language`); return http.get<Login.LoginSetting>(`/auth/setting`);
}; };
export const checkIsIntl = () => { export const checkIsIntl = () => {
+2 -2
View File
@@ -135,8 +135,8 @@ export const createImageRepo = (params: Container.RepoCreate) => {
export const updateImageRepo = (params: Container.RepoUpdate) => { export const updateImageRepo = (params: Container.RepoUpdate) => {
return http.post(`/containers/repo/update`, params, TimeoutEnum.T_40S); return http.post(`/containers/repo/update`, params, TimeoutEnum.T_40S);
}; };
export const deleteImageRepo = (params: Container.RepoDelete) => { export const deleteImageRepo = (id: Number) => {
return http.post(`/containers/repo/del`, params, TimeoutEnum.T_40S); return http.post(`/containers/repo/del`, { id: id }, TimeoutEnum.T_40S);
}; };
// composeTemplate // composeTemplate
@@ -0,0 +1,24 @@
<template>
<MdEditor previewOnly v-model="sanitizedReadMe" :theme="isDarkTheme ? 'dark' : 'light'" />
</template>
<script lang="ts" setup>
import MdEditor from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import DOMPurify from 'dompurify';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
const globalStore = GlobalStore();
const { isDarkTheme } = storeToRefs(globalStore);
const props = defineProps({
content: {
type: String,
default: '',
},
});
const sanitizedReadMe = computed(() => {
return DOMPurify.sanitize(props.content);
});
</script>
@@ -69,7 +69,7 @@
{{ upgradeInfo.testVersion }} {{ upgradeInfo.testVersion }}
</el-radio> </el-radio>
</el-radio-group> </el-radio-group>
<MdEditor v-model="upgradeInfo.releaseNote" previewOnly :theme="isDarkTheme ? 'dark' : 'light'" /> <MarkDownEditor :content="upgradeInfo.releaseNote" />
</div> </div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
@@ -81,10 +81,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
import MarkDownEditor from '@/components/mkdown-editor/index.vue';
import { getSettingInfo, loadReleaseNotes, loadUpgradeInfo, upgrade } from '@/api/modules/setting'; import { getSettingInfo, loadReleaseNotes, loadUpgradeInfo, upgrade } from '@/api/modules/setting';
import MdEditor from 'md-editor-v3';
import i18n from '@/lang'; import i18n from '@/lang';
import 'md-editor-v3/lib/style.css';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { copyText } from '@/utils/util'; import { copyText } from '@/utils/util';
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
@@ -93,7 +93,7 @@ import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
const globalStore = GlobalStore(); const globalStore = GlobalStore();
const { isDarkTheme, docsUrl } = storeToRefs(globalStore); const { docsUrl } = storeToRefs(globalStore);
const mobile = computed(() => { const mobile = computed(() => {
return globalStore.isMobile(); return globalStore.isMobile();
+8
View File
@@ -95,6 +95,10 @@ export const Languages = [
label: 'css', label: 'css',
value: ['css'], value: ['css'],
}, },
{
label: 'ini',
value: ['ini'],
},
]; ];
export const Rewrites = [ export const Rewrites = [
@@ -208,6 +212,10 @@ export const DNSTypes = [
label: i18n.global.t('website.westCN'), label: i18n.global.t('website.westCN'),
value: 'WestCN', value: 'WestCN',
}, },
{
label: 'PorkBun',
value: 'PorkBun',
},
{ {
label: i18n.global.t('website.volcengine'), label: i18n.global.t('website.volcengine'),
value: 'Volcengine', value: 'Volcengine',
+5 -2
View File
@@ -333,6 +333,7 @@ const message = {
ssl: 'Certificate | Certificates', ssl: 'Certificate | Certificates',
database: 'Database | Databases', database: 'Database | Databases',
ai_tools: 'AI', ai_tools: 'AI',
mcp: 'MCP',
container: 'Container | Containers', container: 'Container | Containers',
cronjob: 'Cron Job | Cron Jobs', cronjob: 'Cron Job | Cron Jobs',
host: 'Host | Hosts', host: 'Host | Hosts',
@@ -833,6 +834,7 @@ const message = {
repo: 'Registries', repo: 'Registries',
createRepo: 'Add', createRepo: 'Add',
httpRepoHelper: 'Operating an HTTP-type repository requires restarting the Docker service.',
httpRepo: 'Choosing HTTP protocol requires restarting the Docker service to add it into insecure registries.', httpRepo: 'Choosing HTTP protocol requires restarting the Docker service to add it into insecure registries.',
delInsecure: 'Deletion of credit', delInsecure: 'Deletion of credit',
delInsecureHelper: delInsecureHelper:
@@ -1384,7 +1386,8 @@ const message = {
existFileTitle: 'Same name file prompt', existFileTitle: 'Same name file prompt',
existFileHelper: 'The uploaded file contains a file with the same name, do you want to overwrite it?', existFileHelper: 'The uploaded file contains a file with the same name, do you want to overwrite it?',
existFileSize: 'File size (new -> old)', existFileSize: 'File size (new -> old)',
existFileDirHelper: 'The selected file/folder has a duplicate name. Please proceed with caution!', existFileDirHelper: 'The selected file/folder has a duplicate name. Please proceed with caution! \n',
coverDirHelper: 'The selected folders to be replaced will be copied to the destination path!',
noSuchFile: 'The file or directory was not found. Please check and try again.', noSuchFile: 'The file or directory was not found. Please check and try again.',
}, },
ssh: { ssh: {
@@ -2621,7 +2624,7 @@ const message = {
domain: 'Default Access Address', domain: 'Default Access Address',
domainHelper: 'For example: 192.168.1.1 or example.com', domainHelper: 'For example: 192.168.1.1 or example.com',
bindDomain: 'Bind Website', bindDomain: 'Bind Website',
commandPlaceHolder: 'Currently only supports npx startup commands', commandPlaceHolder: 'Currently only supports npx and binary startup commands',
importMcpJson: 'Import MCP Server Configuration', importMcpJson: 'Import MCP Server Configuration',
importMcpJsonError: 'mcpServers structure is incorrect', importMcpJsonError: 'mcpServers structure is incorrect',
bindDomainHelper: bindDomainHelper:
+4 -1
View File
@@ -333,6 +333,7 @@ const message = {
ssl: '証明書|証明書', ssl: '証明書|証明書',
database: 'データベース|データベース', database: 'データベース|データベース',
ai_tools: 'AI', ai_tools: 'AI',
mcp: 'MCP',
container: 'コンテナ|コンテナ', container: 'コンテナ|コンテナ',
cronjob: 'クロンジョブ|クロンの仕事', cronjob: 'クロンジョブ|クロンの仕事',
host: 'ホスト|ホスト', host: 'ホスト|ホスト',
@@ -829,6 +830,7 @@ const message = {
repo: 'レジストリ', repo: 'レジストリ',
createRepo: '追加', createRepo: '追加',
httpRepoHelper: 'HTTPタイプのリポジトリを操作するにはDockerサービスの再起動が必要です。',
httpRepo: httpRepo:
'HTTPプロトコルを選択するには、Dockerサービスを再起動して不安定なレジストリに追加する必要があります。', 'HTTPプロトコルを選択するには、Dockerサービスを再起動して不安定なレジストリに追加する必要があります。',
delInsecure: 'クレジットの削除', delInsecure: 'クレジットの削除',
@@ -1363,6 +1365,7 @@ const message = {
existFileHelper: 'アップロードしたファイルに同じ名前のファイルが含まれています。上書きしますか?', existFileHelper: 'アップロードしたファイルに同じ名前のファイルが含まれています。上書きしますか?',
existFileSize: 'ファイルサイズ(新しい -> 古い)', existFileSize: 'ファイルサイズ(新しい -> 古い)',
existFileDirHelper: '選択したファイル/フォルダーには同じ名前のものが既に存在します。慎重に操作してください!', existFileDirHelper: '選択したファイル/フォルダーには同じ名前のものが既に存在します。慎重に操作してください!',
coverDirHelper: '上書きするフォルダを選択すると、対象パスにコピーされます!',
noSuchFile: 'ファイルまたはディレクトリが見つかりませんでした。確認して再試行してください。', noSuchFile: 'ファイルまたはディレクトリが見つかりませんでした。確認して再試行してください。',
}, },
ssh: { ssh: {
@@ -2591,7 +2594,7 @@ const message = {
domain: 'デフォルトアクセスアドレス', domain: 'デフォルトアクセスアドレス',
domainHelper: '例: 192.168.1.1 または example.com', domainHelper: '例: 192.168.1.1 または example.com',
bindDomain: 'ウェブサイトをバインド', bindDomain: 'ウェブサイトをバインド',
commandPlaceHolder: '現在、npx スタートアップコマンドのみをサポートしています', commandPlaceHolder: '現在、npx およびバイナリスタートアップコマンドのみをサポートしています',
importMcpJson: 'MCP サーバー設定をインポート', importMcpJson: 'MCP サーバー設定をインポート',
importMcpJsonError: 'mcpServers 構造が正しくありません', importMcpJsonError: 'mcpServers 構造が正しくありません',
bindDomainHelper: bindDomainHelper:
+4 -1
View File
@@ -334,6 +334,7 @@ const message = {
ssl: '인증서 | 인증서들', ssl: '인증서 | 인증서들',
database: '데이터베이스 | 데이터베이스들', database: '데이터베이스 | 데이터베이스들',
ai_tools: 'AI', ai_tools: 'AI',
mcp: 'MCP',
container: '컨테이너 | 컨테이너들', container: '컨테이너 | 컨테이너들',
cronjob: '크론 작업 | 크론 작업들', cronjob: '크론 작업 | 크론 작업들',
host: '호스트 | 호스트들', host: '호스트 | 호스트들',
@@ -819,6 +820,7 @@ const message = {
repo: '레지스트리', repo: '레지스트리',
createRepo: '추가', createRepo: '추가',
httpRepoHelper: 'HTTP 타입 저장소 작업 시 Docker 서비스 재시작이 필요합니다.',
httpRepo: 'HTTP 프로토콜을 선택하면 Docker 서비스를 재시작하여 불안정한 레지스트리에 추가해야 합니다.', httpRepo: 'HTTP 프로토콜을 선택하면 Docker 서비스를 재시작하여 불안정한 레지스트리에 추가해야 합니다.',
delInsecure: '신뢰할 수 없는 항목 삭제', delInsecure: '신뢰할 수 없는 항목 삭제',
delInsecureHelper: delInsecureHelper:
@@ -1349,6 +1351,7 @@ const message = {
existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?', existFileHelper: '업로드한 파일에 동일한 이름의 파일이 포함되어 있습니다. 덮어쓰시겠습니까?',
existFileSize: '파일 크기 (새로운 -> 오래된)', existFileSize: '파일 크기 (새로운 -> 오래된)',
existFileDirHelper: '선택한 파일/폴더에 동일한 이름이 이미 존재합니다. 신중하게 작업하세요!', existFileDirHelper: '선택한 파일/폴더에 동일한 이름이 이미 존재합니다. 신중하게 작업하세요!',
coverDirHelper: '덮어쓸 폴더를 선택하면 대상 경로로 복사됩니다!',
noSuchFile: '파일 또는 디렉터리를 찾을 수 없습니다. 확인 후 다시 시도하세요.', noSuchFile: '파일 또는 디렉터리를 찾을 수 없습니다. 확인 후 다시 시도하세요.',
}, },
ssh: { ssh: {
@@ -2550,7 +2553,7 @@ const message = {
domain: 'デフォルトアクセスアドレス', domain: 'デフォルトアクセスアドレス',
domainHelper: '例: 192.168.1.1 または example.com', domainHelper: '例: 192.168.1.1 または example.com',
bindDomain: 'ウェブサイトをバインド', bindDomain: 'ウェブサイトをバインド',
commandPlaceHolder: '현재 npx 시작 명령만 지원합니다', commandPlaceHolder: '현재 npx 및 바이너리 시작 명령만 지원합니다',
importMcpJson: 'MCP サーバー設定をインポート', importMcpJson: 'MCP サーバー設定をインポート',
importMcpJsonError: 'mcpServers 構造が正しくありません', importMcpJsonError: 'mcpServers 構造が正しくありません',
bindDomainHelper: bindDomainHelper:
+5 -2
View File
@@ -340,6 +340,7 @@ const message = {
ssl: 'Certificate | Certificates', ssl: 'Certificate | Certificates',
database: 'Database | Databases', database: 'Database | Databases',
ai_tools: 'AI', ai_tools: 'AI',
mcp: 'MCP',
container: 'Container | Containers', container: 'Container | Containers',
cronjob: 'Cron Job | Cron Jobs', cronjob: 'Cron Job | Cron Jobs',
host: 'Host | Hosts', host: 'Host | Hosts',
@@ -845,6 +846,7 @@ const message = {
repo: 'Pendaftaran', repo: 'Pendaftaran',
createRepo: 'Tambah', createRepo: 'Tambah',
httpRepoHelper: 'Mengoperasikan repositori jenis HTTP memerlukan mulakan semula perkhidmatan Docker.',
httpRepo: httpRepo:
'Memilih protokol HTTP memerlukan memulakan semula perkhidmatan Docker untuk menambahkannya ke pendaftaran tidak selamat.', 'Memilih protokol HTTP memerlukan memulakan semula perkhidmatan Docker untuk menambahkannya ke pendaftaran tidak selamat.',
delInsecure: 'Padamkan pendaftaran tidak selamat', delInsecure: 'Padamkan pendaftaran tidak selamat',
@@ -1405,7 +1407,8 @@ const message = {
existFileTitle: 'Amaran fail dengan nama yang sama', existFileTitle: 'Amaran fail dengan nama yang sama',
existFileHelper: 'Fail yang dimuat naik mengandungi fail dengan nama yang sama. Adakah anda mahu menimpanya?', existFileHelper: 'Fail yang dimuat naik mengandungi fail dengan nama yang sama. Adakah anda mahu menimpanya?',
existFileSize: 'Saiz fail (baru -> lama)', existFileSize: 'Saiz fail (baru -> lama)',
existFileDirHelper: 'Fail/folder yang dipilih mempunyai nama yang sama. Sila berhati-hati!', existFileDirHelper: 'Fail/folder yang dipilih mempunyai nama yang sama. Sila berhati-hati!\n',
coverDirHelper: 'Folder yang dipilih untuk ditimpa akan disalin ke laluan destinasi!',
noSuchFile: 'Fail atau direktori tidak ditemui. Sila periksa dan cuba lagi.', noSuchFile: 'Fail atau direktori tidak ditemui. Sila periksa dan cuba lagi.',
}, },
ssh: { ssh: {
@@ -2649,7 +2652,7 @@ const message = {
domain: 'Alamat Akses Lalai', domain: 'Alamat Akses Lalai',
domainHelper: 'Contoh: 192.168.1.1 atau example.com', domainHelper: 'Contoh: 192.168.1.1 atau example.com',
bindDomain: 'Sematkan Laman Web', bindDomain: 'Sematkan Laman Web',
commandPlaceHolder: 'Kini hanya menyokong perintah pelancaran npx', commandPlaceHolder: 'Kini hanya menyokong perintah pelancaran npx dan binari',
importMcpJson: 'Import Konfigurasi Pelayan MCP', importMcpJson: 'Import Konfigurasi Pelayan MCP',
importMcpJsonError: 'Struktur mcpServers tidak betul', importMcpJsonError: 'Struktur mcpServers tidak betul',
bindDomainHelper: bindDomainHelper:
+5 -2
View File
@@ -338,6 +338,7 @@ const message = {
ssl: 'Certificado | Certificados', ssl: 'Certificado | Certificados',
database: 'Banco de Dados | Bancos de Dados', database: 'Banco de Dados | Bancos de Dados',
ai_tools: 'AI', ai_tools: 'AI',
mcp: 'MCP',
container: 'Container | Containers', container: 'Container | Containers',
cronjob: 'Tarefa Cron | Tarefas Cron', cronjob: 'Tarefa Cron | Tarefas Cron',
host: 'Host | Hosts', host: 'Host | Hosts',
@@ -841,6 +842,7 @@ const message = {
repo: 'Registries', repo: 'Registries',
createRepo: 'Adicionar', createRepo: 'Adicionar',
httpRepoHelper: 'Operar um repositório do tipo HTTP requer reinicialização do serviço Docker.',
httpRepo: httpRepo:
'Escolher o protocolo HTTP requer reiniciar o serviço Docker para adicioná-lo a registries inseguros.', 'Escolher o protocolo HTTP requer reiniciar o serviço Docker para adicioná-lo a registries inseguros.',
delInsecure: 'Remover da lista de segurança', delInsecure: 'Remover da lista de segurança',
@@ -1392,7 +1394,8 @@ const message = {
existFileTitle: 'Aviso de arquivo com o mesmo nome', existFileTitle: 'Aviso de arquivo com o mesmo nome',
existFileHelper: 'O arquivo enviado contém um arquivo com o mesmo nome. Deseja substituí-lo?', existFileHelper: 'O arquivo enviado contém um arquivo com o mesmo nome. Deseja substituí-lo?',
existFileSize: 'Tamanho do arquivo (novo -> antigo)', existFileSize: 'Tamanho do arquivo (novo -> antigo)',
existFileDirHelper: 'O arquivo/pasta selecionado tem um nome duplicado. Por favor, prossiga com cautela!', existFileDirHelper: 'O arquivo/pasta selecionado tem um nome duplicado. Por favor, prossiga com cautela!\n',
coverDirHelper: 'As pastas selecionadas para substituição serão copiadas para o caminho de destino!',
noSuchFile: 'O arquivo ou diretório não foi encontrado. Por favor, verifique e tente novamente.', noSuchFile: 'O arquivo ou diretório não foi encontrado. Por favor, verifique e tente novamente.',
}, },
ssh: { ssh: {
@@ -2647,7 +2650,7 @@ const message = {
domain: 'Endereço de Acesso Padrão', domain: 'Endereço de Acesso Padrão',
domainHelper: 'Por exemplo: 192.168.1.1 ou example.com', domainHelper: 'Por exemplo: 192.168.1.1 ou example.com',
bindDomain: 'Vincular Site', bindDomain: 'Vincular Site',
commandPlaceHolder: 'Atualmente, apenas comandos de inicialização npx são suportados', commandPlaceHolder: 'Atualmente, apenas comandos de inicialização npx e binários são suportados',
importMcpJson: 'Importar Configuração do Servidor MCP', importMcpJson: 'Importar Configuração do Servidor MCP',
importMcpJsonError: 'A estrutura mcpServers está incorreta', importMcpJsonError: 'A estrutura mcpServers está incorreta',
bindDomainHelper: bindDomainHelper:
+5 -2
View File
@@ -334,6 +334,7 @@ const message = {
ssl: 'Сертификат | Сертификаты', ssl: 'Сертификат | Сертификаты',
database: 'База данных | Базы данных', database: 'База данных | Базы данных',
ai_tools: 'AI', ai_tools: 'AI',
mcp: 'MCP',
container: 'Контейнер | Контейнеры', container: 'Контейнер | Контейнеры',
cronjob: 'Cron | Задачи Cron', cronjob: 'Cron | Задачи Cron',
host: 'Хост | Хосты', host: 'Хост | Хосты',
@@ -843,6 +844,7 @@ const message = {
repo: 'Реестры', repo: 'Реестры',
createRepo: 'Добавить', createRepo: 'Добавить',
httpRepoHelper: 'Работа с репозиторием HTTP-типа требует перезапуска службы Docker.',
httpRepo: 'Выбор HTTP протокола требует перезапуска службы Docker для добавления в небезопасные реестры.', httpRepo: 'Выбор HTTP протокола требует перезапуска службы Docker для добавления в небезопасные реестры.',
delInsecure: 'Удаление учетных данных', delInsecure: 'Удаление учетных данных',
delInsecureHelper: 'Это перезапустит службу Docker для удаления из небезопасных реестров. Хотите продолжить?', delInsecureHelper: 'Это перезапустит службу Docker для удаления из небезопасных реестров. Хотите продолжить?',
@@ -1394,7 +1396,8 @@ const message = {
existFileTitle: 'Предупреждение о файле с тем же именем', existFileTitle: 'Предупреждение о файле с тем же именем',
existFileHelper: 'Загруженный файл содержит файл с таким же именем. Заменить его?', existFileHelper: 'Загруженный файл содержит файл с таким же именем. Заменить его?',
existFileSize: 'Размер файла (новый -> старый)', existFileSize: 'Размер файла (новый -> старый)',
existFileDirHelper: 'Выбранный файл/папка имеет дублирующееся имя. Пожалуйста, действуйте осторожно!', existFileDirHelper: 'Выбранный файл/папка имеет дублирующееся имя. Пожалуйста, действуйте осторожно!\n',
coverDirHelper: 'При выборе перезаписываемой папки она будет скопирована в целевой путь!',
noSuchFile: 'Файл или каталог не найдены. Пожалуйста, проверьте и повторите попытку.', noSuchFile: 'Файл или каталог не найдены. Пожалуйста, проверьте и повторите попытку.',
}, },
ssh: { ssh: {
@@ -2643,7 +2646,7 @@ const message = {
domain: 'Адрес доступа по умолчанию', domain: 'Адрес доступа по умолчанию',
domainHelper: 'Например: 192.168.1.1 или example.com', domainHelper: 'Например: 192.168.1.1 или example.com',
bindDomain: 'Привязать сайт', bindDomain: 'Привязать сайт',
commandPlaceHolder: 'В настоящее время поддерживаются только команды запуска npx', commandPlaceHolder: 'В настоящее время поддерживаются только команды запуска npx и двоичных файлов',
importMcpJson: 'Импортировать конфигурацию сервера MCP', importMcpJson: 'Импортировать конфигурацию сервера MCP',
importMcpJsonError: 'Структура mcpServers некорректна', importMcpJsonError: 'Структура mcpServers некорректна',
bindDomainHelper: bindDomainHelper:
+4 -1
View File
@@ -327,6 +327,7 @@ const message = {
ssl: '證書', ssl: '證書',
database: '資料庫', database: '資料庫',
ai_tools: 'AI', ai_tools: 'AI',
mcp: 'MCP',
container: '容器', container: '容器',
cronjob: '計劃任務', cronjob: '計劃任務',
host: '主機', host: '主機',
@@ -804,6 +805,7 @@ const message = {
repo: '倉庫', repo: '倉庫',
createRepo: '新增倉庫', createRepo: '新增倉庫',
httpRepoHelper: '操作 HTTP 類型倉庫需要重啟 Docker 服務。',
downloadUrl: '下載網址', downloadUrl: '下載網址',
imageRepo: '鏡像倉庫', imageRepo: '鏡像倉庫',
repoHelper: '是否包含鏡像倉庫/組織/項目?', repoHelper: '是否包含鏡像倉庫/組織/項目?',
@@ -1319,6 +1321,7 @@ const message = {
existFileHelper: '上傳的檔案存在同名檔案,是否覆蓋?', existFileHelper: '上傳的檔案存在同名檔案,是否覆蓋?',
existFileSize: '文件大小(新->舊)', existFileSize: '文件大小(新->舊)',
existFileDirHelper: '選擇的檔案/資料夾存在同名,請謹慎操作!', existFileDirHelper: '選擇的檔案/資料夾存在同名,請謹慎操作!',
coverDirHelper: '選取要覆蓋的資料夾,將複製到目標路徑!',
noSuchFile: '找不到該檔案或目錄,請檢查後重試。', noSuchFile: '找不到該檔案或目錄,請檢查後重試。',
}, },
ssh: { ssh: {
@@ -2456,7 +2459,7 @@ const message = {
domain: '默認訪問地址', domain: '默認訪問地址',
domainHelper: '例如:192.168.1.1 或者 example.com', domainHelper: '例如:192.168.1.1 或者 example.com',
bindDomain: '綁定網站', bindDomain: '綁定網站',
commandPlaceHolder: '當前僅支持 npx 啟動命令', commandPlaceHolder: '當前僅支持 npx 和 二進制啟動的命令',
importMcpJson: '導入 MCP Server配置', importMcpJson: '導入 MCP Server配置',
importMcpJsonError: 'mcpServers 結構不正確', importMcpJsonError: 'mcpServers 結構不正確',
bindDomainHelper: '綁定網站之後會修改所有已安裝 MCP Server 的訪問地址,並關閉端口的外部訪問', bindDomainHelper: '綁定網站之後會修改所有已安裝 MCP Server 的訪問地址,並關閉端口的外部訪問',
+4 -1
View File
@@ -326,6 +326,7 @@ const message = {
ssl: '证书', ssl: '证书',
database: '数据库', database: '数据库',
ai_tools: 'AI', ai_tools: 'AI',
mcp: 'MCP',
container: '容器', container: '容器',
cronjob: '计划任务', cronjob: '计划任务',
host: '主机', host: '主机',
@@ -806,6 +807,7 @@ const message = {
repo: '仓库', repo: '仓库',
createRepo: '添加仓库', createRepo: '添加仓库',
httpRepoHelper: '操作 http 类型仓库需要重启 Docker 服务。',
downloadUrl: '下载地址', downloadUrl: '下载地址',
imageRepo: '镜像仓库', imageRepo: '镜像仓库',
repoHelper: '是否包含镜像仓库/组织/项目?', repoHelper: '是否包含镜像仓库/组织/项目?',
@@ -1321,6 +1323,7 @@ const message = {
existFileHelper: '上传的文件存在同名文件,是否覆盖?', existFileHelper: '上传的文件存在同名文件,是否覆盖?',
existFileSize: '文件大小 (新 -> 旧)', existFileSize: '文件大小 (新 -> 旧)',
existFileDirHelper: '选择的文件/文件夹存在同名,请谨慎操作!', existFileDirHelper: '选择的文件/文件夹存在同名,请谨慎操作!',
coverDirHelper: '选中覆盖的文件夹,将复制到目标路径!',
noSuchFile: '未能找到该文件或目录,请检查后重试', noSuchFile: '未能找到该文件或目录,请检查后重试',
}, },
ssh: { ssh: {
@@ -2458,7 +2461,7 @@ const message = {
domain: '默认访问地址', domain: '默认访问地址',
domainHelper: '例如:192.168.1.1 或者 example.com', domainHelper: '例如:192.168.1.1 或者 example.com',
bindDomain: '绑定网站', bindDomain: '绑定网站',
commandPlaceHolder: '当前仅支持 npx 启动命令', commandPlaceHolder: '当前仅支持 npx 和 二进制启动的命令',
importMcpJson: '导入 MCP Server 配置', importMcpJson: '导入 MCP Server 配置',
importMcpJsonError: 'mcpServers 结构不正确', importMcpJsonError: 'mcpServers 结构不正确',
bindDomainHelper: '绑定网站之后会修改所有已安装 MCP Server 的访问地址,并关闭端口的外部访问', bindDomainHelper: '绑定网站之后会修改所有已安装 MCP Server 的访问地址,并关闭端口的外部访问',
+1 -1
View File
@@ -24,7 +24,7 @@ const databaseRouter = {
name: 'MCPServer', name: 'MCPServer',
component: () => import('@/views/ai/mcp/server/index.vue'), component: () => import('@/views/ai/mcp/server/index.vue'),
meta: { meta: {
title: 'MCP', title: 'menu.mcp',
requiresAuth: true, requiresAuth: true,
}, },
}, },
+1 -1
View File
@@ -49,7 +49,7 @@ const GlobalStore = defineStore({
state.themeConfig.theme === 'dark' || state.themeConfig.theme === 'dark' ||
(state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches), (state.themeConfig.theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches),
isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro, isDarkGoldTheme: (state) => state.themeConfig.primary === '#F0BE96' && state.isProductPro,
docsUrl: (state) => (state.isIntl ? 'https://docs.1panel.pro' : 'https://1panel.cn/docs'), docsUrl: (state) => (state.isIntl ? 'https://docs.1panel.pro' : 'https://1panel.cn/docs/v1'),
}, },
actions: { actions: {
setOpenMenuTabs(openMenuTabs: boolean) { setOpenMenuTabs(openMenuTabs: boolean) {
@@ -72,21 +72,18 @@
</div> </div>
</div> </div>
</div> </div>
<MdEditor previewOnly v-model="app.readMe" :theme="isDarkTheme ? 'dark' : 'light'" /> <MarkDownEditor :content="app.readMe" />
</el-drawer> </el-drawer>
<Install ref="installRef"></Install> <Install ref="installRef"></Install>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import MarkDownEditor from '@/components/mkdown-editor/index.vue';
import { GetApp, GetAppDetail } from '@/api/modules/app'; import { GetApp, GetAppDetail } from '@/api/modules/app';
import MdEditor from 'md-editor-v3';
import { ref } from 'vue'; import { ref } from 'vue';
import Install from './install/index.vue'; import Install from './install/index.vue';
import router from '@/routers'; import router from '@/routers';
import { GlobalStore } from '@/store';
import { storeToRefs } from 'pinia';
const globalStore = GlobalStore();
const { isDarkTheme } = storeToRefs(globalStore);
const app = ref<any>({}); const app = ref<any>({});
const appDetail = ref<any>({}); const appDetail = ref<any>({});
@@ -70,9 +70,10 @@ const acceptParams = async (): Promise<void> => {
unUsedList.value = []; unUsedList.value = [];
for (const item of list) { for (const item of list) {
if ( if (
!item.tags || (!item.tags ||
item.tags.length === 0 || item.tags.length === 0 ||
(item.tags.length === 1 && item.tags[0].indexOf('<none>') !== -1 && !item.isUsed) (item.tags.length === 1 && item.tags[0].indexOf('<none>') !== -1)) &&
!item.isUsed
) { ) {
unTagList.value.push(item); unTagList.value.push(item);
} }
+28 -4
View File
@@ -61,8 +61,9 @@
</template> </template>
</LayoutContent> </LayoutContent>
<OpDialog ref="opRef" @search="search" /> <OpDialog ref="opRef" @search="search" @submit="submitDelete" />
<OperatorDialog @search="search" ref="dialogRef" /> <OperatorDialog @search="search" ref="dialogRef" />
<ConfirmDialog ref="confirmDialog" @confirm="submitDelete" />
</div> </div>
</template> </template>
@@ -87,6 +88,8 @@ const paginationConfig = reactive({
const searchName = ref(); const searchName = ref();
const opRef = ref(); const opRef = ref();
const confirmDialog = ref();
const currentRepo = ref();
const dockerStatus = ref('Running'); const dockerStatus = ref('Running');
const loadStatus = async () => { const loadStatus = async () => {
@@ -141,7 +144,16 @@ const onOpenDialog = async (
}; };
const onDelete = async (row: Container.RepoInfo) => { const onDelete = async (row: Container.RepoInfo) => {
let ids = [row.id]; currentRepo.value = row.id;
if (row.protocol === 'http') {
let params = {
header: i18n.global.t('container.repo'),
operationInfo: i18n.global.t('container.httpRepoHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialog.value!.acceptParams(params);
return;
}
opRef.value.acceptParams({ opRef.value.acceptParams({
title: i18n.global.t('commons.button.delete'), title: i18n.global.t('commons.button.delete'),
names: [row.name], names: [row.name],
@@ -149,11 +161,23 @@ const onDelete = async (row: Container.RepoInfo) => {
i18n.global.t('container.repo'), i18n.global.t('container.repo'),
i18n.global.t('commons.button.delete'), i18n.global.t('commons.button.delete'),
]), ]),
api: deleteImageRepo, api: null,
params: { ids: ids }, params: null,
}); });
}; };
const submitDelete = async () => {
loading.value = true;
await deleteImageRepo(currentRepo.value)
.then(() => {
loading.value = false;
search();
})
.catch(() => {
loading.value = false;
});
};
const onCheckConn = async (row: Container.RepoInfo) => { const onCheckConn = async (row: Container.RepoInfo) => {
loading.value = true; loading.value = true;
await checkRepoStatus(row.id) await checkRepoStatus(row.id)
@@ -81,6 +81,8 @@
</span> </span>
</template> </template>
</el-drawer> </el-drawer>
<ConfirmDialog ref="confirmDialog" @confirm="submit" />
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -105,8 +107,14 @@ const drawerVisible = ref(false);
const dialogData = ref<DialogProps>({ const dialogData = ref<DialogProps>({
title: '', title: '',
}); });
const confirmDialog = ref();
const oldUrl = ref();
const oldProto = ref();
const acceptParams = (params: DialogProps): void => { const acceptParams = (params: DialogProps): void => {
dialogData.value = params; dialogData.value = params;
oldUrl.value = params.rowData.downloadUrl;
oldProto.value = params.rowData.protocol;
title.value = i18n.global.t('commons.button.' + dialogData.value.title); title.value = i18n.global.t('commons.button.' + dialogData.value.title);
drawerVisible.value = true; drawerVisible.value = true;
}; };
@@ -114,6 +122,7 @@ const emit = defineEmits<{ (e: 'search'): void }>();
const handleClose = () => { const handleClose = () => {
drawerVisible.value = false; drawerVisible.value = false;
emit('search');
}; };
const rules = reactive({ const rules = reactive({
name: [Rules.requiredInput, Rules.name], name: [Rules.requiredInput, Rules.name],
@@ -142,21 +151,33 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
formEl.validate(async (valid) => { formEl.validate(async (valid) => {
if (!valid) return; if (!valid) return;
loading.value = true; let newProto = dialogData.value.rowData.protocol;
if (dialogData.value.title === 'add') { if (newProto === 'https' && dialogData.value.title === 'add') {
await createImageRepo(dialogData.value.rowData!) submit();
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
return; return;
} }
await updateImageRepo(dialogData.value.rowData!) if (newProto === oldProto.value) {
if (
(oldProto.value === 'http' && dialogData.value.rowData.downloadUrl === oldUrl.value) ||
oldProto.value === 'https'
) {
submit();
return;
}
}
let params = {
header: i18n.global.t('container.repo'),
operationInfo: i18n.global.t('container.httpRepoHelper'),
submitInputInfo: i18n.global.t('database.restartNow'),
};
confirmDialog.value!.acceptParams(params);
});
};
const submit = async () => {
loading.value = true;
if (dialogData.value.title === 'add') {
await createImageRepo(dialogData.value.rowData!)
.then(() => { .then(() => {
loading.value = false; loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
@@ -166,9 +187,19 @@ const onSubmit = async (formEl: FormInstance | undefined) => {
.catch(() => { .catch(() => {
loading.value = false; loading.value = false;
}); });
}); return;
}
await updateImageRepo(dialogData.value.rowData!)
.then(() => {
loading.value = false;
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
emit('search');
drawerVisible.value = false;
})
.catch(() => {
loading.value = false;
});
}; };
defineExpose({ defineExpose({
acceptParams, acceptParams,
}); });
+3 -2
View File
@@ -452,10 +452,11 @@ import { storeToRefs } from 'pinia';
import { GlobalStore } from '@/store'; import { GlobalStore } from '@/store';
import LicenseImport from '@/components/license-import/index.vue'; import LicenseImport from '@/components/license-import/index.vue';
import { Codemirror } from 'vue-codemirror'; import { Codemirror } from 'vue-codemirror';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/legacy-modes/mode/javascript';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { StreamLanguage } from '@codemirror/language';
const extensions = [javascript(), oneDark]; const extensions = [StreamLanguage.define(javascript), oneDark];
const router = useRouter(); const router = useRouter();
+5 -1
View File
@@ -395,6 +395,7 @@ const search = async () => {
if (!currentRecord.value) { if (!currentRecord.value) {
currentRecord.value = records.value[0]; currentRecord.value = records.value[0];
} else { } else {
let oldRecordID = currentRecord.value.id;
let beDelete = true; let beDelete = true;
for (const item of records.value) { for (const item of records.value) {
if (item.id === currentRecord.value.id) { if (item.id === currentRecord.value.id) {
@@ -406,6 +407,9 @@ const search = async () => {
if (beDelete) { if (beDelete) {
currentRecord.value = records.value[0]; currentRecord.value = records.value[0];
} }
if (oldRecordID === currentRecord.value.id && currentRecord.value.status !== 'Waiting') {
return;
}
} }
if (currentRecord.value?.records) { if (currentRecord.value?.records) {
loadRecord(currentRecord.value); loadRecord(currentRecord.value);
@@ -419,7 +423,7 @@ const forDetail = async (row: Cronjob.Record) => {
const loadRecord = async (row: Cronjob.Record) => { const loadRecord = async (row: Cronjob.Record) => {
if (row.records) { if (row.records) {
const res = await getRecordLog(row.id); const res = await getRecordLog(row.id);
let log = res.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); let log = res.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').replace(/\r/g, '');
if (currentRecordDetail.value === log) { if (currentRecordDetail.value === log) {
return; return;
} }
@@ -74,6 +74,8 @@ const changeVisible = ref(false);
type FormInstance = InstanceType<typeof ElForm>; type FormInstance = InstanceType<typeof ElForm>;
const changeFormRef = ref<FormInstance>(); const changeFormRef = ref<FormInstance>();
const title = ref(); const title = ref();
const oldPrivilege = ref();
const oldPrivilegeIPs = ref();
const changeForm = reactive({ const changeForm = reactive({
id: 0, id: 0,
from: '', from: '',
@@ -124,6 +126,8 @@ const acceptParams = (params: DialogProps): void => {
changeForm.privilegeIPs = params.privilegeIPs; changeForm.privilegeIPs = params.privilegeIPs;
changeForm.value = params.value; changeForm.value = params.value;
changeVisible.value = true; changeVisible.value = true;
oldPrivilege.value = params.privilege;
oldPrivilegeIPs.value = params.privilegeIPs;
}; };
const emit = defineEmits<{ (e: 'search'): void }>(); const emit = defineEmits<{ (e: 'search'): void }>();
@@ -167,6 +171,10 @@ const submitChangeInfo = async (formEl: FormInstance | undefined) => {
} }
return; return;
} }
if (changeForm.privilege === oldPrivilege.value && changeForm.privilegeIPs === oldPrivilegeIPs.value) {
changeVisible.value = false;
return;
}
if (changeForm.privilege !== 'ip') { if (changeForm.privilege !== 'ip') {
param.value = changeForm.privilege; param.value = changeForm.privilege;
} else { } else {
@@ -4,9 +4,6 @@
<template #title> <template #title>
<back-button name="MySQL" :header="props.database + ' ' + $t('commons.button.set')"> <back-button name="MySQL" :header="props.database + ' ' + $t('commons.button.set')">
<template #buttons> <template #buttons>
<el-button type="primary" :plain="activeName !== 'conf'" @click="jumpToConf">
{{ $t('database.confChange') }}
</el-button>
<el-button <el-button
type="primary" type="primary"
:disabled="mysqlStatus !== 'Running'" :disabled="mysqlStatus !== 'Running'"
@@ -15,6 +12,9 @@
> >
{{ $t('database.currentStatus') }} {{ $t('database.currentStatus') }}
</el-button> </el-button>
<el-button type="primary" :plain="activeName !== 'conf'" @click="jumpToConf">
{{ $t('database.confChange') }}
</el-button>
<el-button <el-button
type="primary" type="primary"
:disabled="mysqlStatus !== 'Running'" :disabled="mysqlStatus !== 'Running'"
@@ -159,7 +159,7 @@ const globalStore = GlobalStore();
const loading = ref(false); const loading = ref(false);
const extensions = [javascript(), oneDark]; const extensions = [javascript(), oneDark];
const activeName = ref('conf'); const activeName = ref('status');
const baseInfo = reactive({ const baseInfo = reactive({
name: '', name: '',
@@ -2,9 +2,6 @@
<div v-show="settingShow" v-loading="loading"> <div v-show="settingShow" v-loading="loading">
<LayoutContent :title="database + ' ' + $t('commons.button.set')" :reload="true"> <LayoutContent :title="database + ' ' + $t('commons.button.set')" :reload="true">
<template #buttons> <template #buttons>
<el-button type="primary" :plain="activeName !== 'conf'" @click="changeTab('conf')">
{{ $t('database.confChange') }}
</el-button>
<el-button <el-button
type="primary" type="primary"
:disabled="redisStatus !== 'Running'" :disabled="redisStatus !== 'Running'"
@@ -13,6 +10,9 @@
> >
{{ $t('database.currentStatus') }} {{ $t('database.currentStatus') }}
</el-button> </el-button>
<el-button type="primary" :plain="activeName !== 'conf'" @click="changeTab('conf')">
{{ $t('database.confChange') }}
</el-button>
<el-button <el-button
type="primary" type="primary"
:disabled="redisStatus !== 'Running'" :disabled="redisStatus !== 'Running'"
@@ -170,7 +170,7 @@ const rules = reactive({
maxmemory: [Rules.number, checkNumberRange(0, 999999)], maxmemory: [Rules.number, checkNumberRange(0, 999999)],
}); });
const activeName = ref('conf'); const activeName = ref('status');
const statusRef = ref(); const statusRef = ref();
const persistenceRef = ref(); const persistenceRef = ref();
@@ -217,7 +217,7 @@ const acceptParams = (prop: DialogProps): void => {
redisStatus.value = prop.status; redisStatus.value = prop.status;
database.value = prop.database; database.value = prop.database;
settingShow.value = true; settingShow.value = true;
loadConfFile(); changeTab('status');
}; };
const portRef = ref(); const portRef = ref();
+15 -5
View File
@@ -289,6 +289,7 @@ const chartOption = ref('network');
let timer: NodeJS.Timer | null = null; let timer: NodeJS.Timer | null = null;
let isInit = ref<boolean>(true); let isInit = ref<boolean>(true);
let isActive = ref(true); let isActive = ref(true);
let isCurrentActive = ref(true);
const ioReadBytes = ref<Array<number>>([]); const ioReadBytes = ref<Array<number>>([]);
const ioWriteBytes = ref<Array<number>>([]); const ioWriteBytes = ref<Array<number>>([]);
@@ -426,12 +427,20 @@ const onLoadBaseInfo = async (isInit: boolean, range: string) => {
currentInfo.value.uptime = resData.uptime; currentInfo.value.uptime = resData.uptime;
loadAppCurrentInfo(); loadAppCurrentInfo();
statusRef.value.acceptParams(currentInfo.value, baseInfo.value); statusRef.value?.acceptParams(currentInfo.value, baseInfo.value);
appRef.value.acceptParams(); appRef.value?.acceptParams();
if (isInit) { if (isInit) {
timer = setInterval(async () => { timer = setInterval(async () => {
if (isActive.value && !globalStore.isOnRestart) { try {
loadAppCurrentInfo(); if (!isCurrentActive.value) {
throw new Error('jump out');
}
if (isActive.value && !globalStore.isOnRestart) {
await loadAppCurrentInfo();
}
} catch {
clearInterval(Number(timer));
timer = null;
} }
}, 3000); }, 3000);
} }
@@ -439,7 +448,7 @@ const onLoadBaseInfo = async (isInit: boolean, range: string) => {
const loadAppCurrentInfo = async () => { const loadAppCurrentInfo = async () => {
await Promise.all([onLoadCurrentInfo('gpu'), onLoadCurrentInfo('basic'), onLoadCurrentInfo('ioNet')]); await Promise.all([onLoadCurrentInfo('gpu'), onLoadCurrentInfo('basic'), onLoadCurrentInfo('ioNet')]);
statusRef.value.acceptParams(currentInfo.value, baseInfo.value); statusRef.value?.acceptParams(currentInfo.value, baseInfo.value);
}; };
const onLoadCurrentInfo = async (scope: string) => { const onLoadCurrentInfo = async (scope: string) => {
@@ -661,6 +670,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('focus', onFocus); window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur); window.removeEventListener('blur', onBlur);
isCurrentActive.value = false;
clearInterval(Number(timer)); clearInterval(Number(timer));
timer = null; timer = null;
}); });
@@ -328,6 +328,7 @@ const handleClose = () => {
saveContent(); saveContent();
}) })
.finally(() => { .finally(() => {
isEdit.value = false;
closeEditor(); closeEditor();
}); });
} else { } else {
@@ -4,7 +4,7 @@
:destroy-on-close="true" :destroy-on-close="true"
:close-on-click-modal="false" :close-on-click-modal="false"
:close-on-press-escape="false" :close-on-press-escape="false"
size="675" size="690"
> >
<template #header> <template #header>
<DrawerHeader :header="title" :back="handleClose" /> <DrawerHeader :header="title" :back="handleClose" />
@@ -19,6 +19,16 @@
:rules="rules" :rules="rules"
v-loading="loading" v-loading="loading"
> >
<el-alert
v-if="type == 'cut' && existFiles?.length == 0 && addForm.cover && changeName"
show-icon
type="warning"
:closable="false"
>
<template #title>
<span class="whitespace-break-spaces">{{ $t('file.coverDirHelper') }}</span>
</template>
</el-alert>
<el-form-item :label="$t('file.path')" prop="newPath"> <el-form-item :label="$t('file.path')" prop="newPath">
<el-input v-model="addForm.newPath"> <el-input v-model="addForm.newPath">
<template #prepend><FileList @choose="getPath" :dir="true"></FileList></template> <template #prepend><FileList @choose="getPath" :dir="true"></FileList></template>
@@ -34,10 +44,12 @@
</el-radio-group> </el-radio-group>
</div> </div>
<div v-if="existFiles.length > 0 && !changeName" class="text-center"> <div v-if="existFiles.length > 0 && !changeName" class="text-center">
<el-alert :show-icon="true" type="warning" :closable="false"> <el-alert show-icon type="warning" :closable="false">
<div class="whitespace-break-spaces"> <template #title>
<span>{{ $t('file.existFileDirHelper') }}</span> <span class="whitespace-break-spaces">
</div> {{ $t('file.existFileDirHelper') + $t('file.coverDirHelper') }}
</span>
</template>
</el-alert> </el-alert>
<el-transfer <el-transfer
v-model="skipFiles" v-model="skipFiles"
@@ -74,6 +86,7 @@ import FileList from '@/components/file-list/index.vue';
import DrawerHeader from '@/components/drawer-header/index.vue'; import DrawerHeader from '@/components/drawer-header/index.vue';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { getDateStr } from '@/utils/util'; import { getDateStr } from '@/utils/util';
import { File } from '@/api/interface/file';
interface MoveProps { interface MoveProps {
oldPaths: Array<string>; oldPaths: Array<string>;
@@ -90,7 +103,7 @@ const open = ref(false);
const type = ref('cut'); const type = ref('cut');
const changeName = ref(false); const changeName = ref(false);
const oldName = ref(''); const oldName = ref('');
const existFiles = ref([]); const existFiles = ref<File.ExistFileInfo[]>([]);
const skipFiles = ref([]); const skipFiles = ref([]);
const transferData = ref([]); const transferData = ref([]);
@@ -110,6 +123,7 @@ const addForm = reactive({
allNames: [] as string[], allNames: [] as string[],
isDir: false, isDir: false,
cover: false, cover: false,
coverPaths: [] as string[],
}); });
const rules = reactive<FormRules>({ const rules = reactive<FormRules>({
@@ -136,7 +150,20 @@ const getFileName = (filePath: string) => {
}; };
const coverFiles: ComputedRef<string[]> = computed(() => { const coverFiles: ComputedRef<string[]> = computed(() => {
return addForm.oldPaths.filter((item) => !skipFiles.value.includes(getFileName(item))).map((item) => item); const existingNames = new Set(
existFiles.value.filter((item) => !skipFiles.value.includes(item.name) && item.isDir).map((item) => item.name),
);
return addForm.oldPaths.filter((path) => existingNames.has(getFileName(path)));
});
const mvFiles: ComputedRef<string[]> = computed(() => {
const skipSet = new Set(skipFiles.value);
const coverSet = new Set(coverFiles.value.map(getFileName));
return addForm.oldPaths.filter((path) => {
const name = getFileName(path);
return !skipSet.has(name) && !coverSet.has(name);
});
}); });
const getPath = (path: string) => { const getPath = (path: string) => {
@@ -173,7 +200,8 @@ const submit = async (formEl: FormInstance | undefined) => {
return; return;
} }
loading.value = true; loading.value = true;
addForm.oldPaths = coverFiles.value; addForm.coverPaths = coverFiles.value;
addForm.oldPaths = mvFiles.value;
mvFile(); mvFile();
}); });
}; };
@@ -232,6 +260,7 @@ const acceptParams = async (props: MoveProps) => {
addForm.name = ''; addForm.name = '';
addForm.allNames = props.allNames; addForm.allNames = props.allNames;
type.value = props.type; type.value = props.type;
existFiles.value = [];
if (props.name && props.name != '') { if (props.name && props.name != '') {
oldName.value = props.name; oldName.value = props.name;
const res = await CheckFile(props.path + '/' + props.name, false); const res = await CheckFile(props.path + '/' + props.name, false);
@@ -261,7 +290,7 @@ defineExpose({ acceptParams });
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-transfer) { :deep(.el-transfer) {
--el-transfer-panel-width: 250px; --el-transfer-panel-width: 260px;
.el-button { .el-button {
padding: 4px 7px; padding: 4px 7px;
} }
@@ -72,6 +72,7 @@
</div> </div>
<el-form-item prop="name" class="no-border"> <el-form-item prop="name" class="no-border">
<el-input <el-input
name="username"
v-model.trim="loginForm.name" v-model.trim="loginForm.name"
:placeholder="$t('commons.login.username')" :placeholder="$t('commons.login.username')"
class="form-input" class="form-input"
@@ -85,6 +86,7 @@
</el-form-item> </el-form-item>
<el-form-item prop="password" class="no-border"> <el-form-item prop="password" class="no-border">
<el-input <el-input
name="password"
type="password" type="password"
clearable clearable
v-model.trim="loginForm.password" v-model.trim="loginForm.password"
@@ -174,7 +176,7 @@
import { ref, reactive, onMounted, computed, nextTick } from 'vue'; import { ref, reactive, onMounted, computed, nextTick } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import type { ElForm } from 'element-plus'; import type { ElForm } from 'element-plus';
import { loginApi, getCaptcha, mfaLoginApi, checkIsDemo, getLanguage, checkIsIntl } from '@/api/modules/auth'; import { loginApi, getCaptcha, mfaLoginApi, checkIsDemo, getAuthSetting, checkIsIntl } from '@/api/modules/auth';
import { GlobalStore, MenuStore, TabsStore } from '@/store'; import { GlobalStore, MenuStore, TabsStore } from '@/store';
import { MsgSuccess } from '@/utils/message'; import { MsgSuccess } from '@/utils/message';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -316,7 +318,6 @@ const login = (formEl: FormInstance | undefined) => {
let requestLoginForm = { let requestLoginForm = {
name: loginForm.name, name: loginForm.name,
password: encryptPassword(loginForm.password), password: encryptPassword(loginForm.password),
ignoreCaptcha: globalStore.ignoreCaptcha,
captcha: loginForm.captcha, captcha: loginForm.captcha,
captchaID: captcha.captchaID, captchaID: captcha.captchaID,
authMethod: 'session', authMethod: 'session',
@@ -398,11 +399,12 @@ const checkIsSystemDemo = async () => {
isDemo.value = res.data; isDemo.value = res.data;
}; };
const loadLanguage = async () => { const loadLoginSetting = async () => {
try { try {
const res = await getLanguage(); const res = await getAuthSetting();
loginForm.language = res.data; loginForm.language = res.data.language;
handleCommand(res.data); globalStore.ignoreCaptcha = !res.data.needCaptcha;
handleCommand(loginForm.language);
} catch (error) {} } catch (error) {}
}; };
@@ -424,7 +426,7 @@ onMounted(() => {
globalStore.isOnRestart = false; globalStore.isOnRestart = false;
checkIsSystemIntl(); checkIsSystemIntl();
loginVerify(); loginVerify();
loadLanguage(); loadLoginSetting();
document.title = globalStore.themeConfig.panelName; document.title = globalStore.themeConfig.panelName;
loginForm.agreeLicense = globalStore.agreeLicense; loginForm.agreeLicense = globalStore.agreeLicense;
checkIsSystemDemo(); checkIsSystemDemo();
@@ -138,6 +138,14 @@
<el-input v-model.trim="account.authorization['password']"></el-input> <el-input v-model.trim="account.authorization['password']"></el-input>
</el-form-item> </el-form-item>
</div> </div>
<div v-if="account.type === 'PorkBun'">
<el-form-item label="API Key" prop="authorization.apiKey">
<el-input v-model.trim="account.authorization['apiKey']"></el-input>
</el-form-item>
<el-form-item label="Secret Key" prop="authorization.secretKey">
<el-input v-model.trim="account.authorization['secretKey']"></el-input>
</el-form-item>
</div>
</el-form> </el-form>
</el-col> </el-col>
</el-row> </el-row>
+12 -12
View File
@@ -1,8 +1,8 @@
module github.com/1Panel-dev/1Panel module github.com/1Panel-dev/1Panel
go 1.23 go 1.23.0
toolchain go1.23.7 toolchain go1.24.1
require ( require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
@@ -54,13 +54,14 @@ require (
github.com/swaggo/swag v1.16.3 github.com/swaggo/swag v1.16.3
github.com/tencentyun/cos-go-sdk-v5 v0.7.46 github.com/tencentyun/cos-go-sdk-v5 v0.7.46
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
golang.org/x/crypto v0.32.0 go4.org v0.0.0-20230225012048-214862532bf5
golang.org/x/net v0.37.0 golang.org/x/crypto v0.35.0
golang.org/x/net v0.39.0
golang.org/x/oauth2 v0.24.0 golang.org/x/oauth2 v0.24.0
golang.org/x/sys v0.29.0 golang.org/x/sync v0.11.0
golang.org/x/term v0.28.0 golang.org/x/sys v0.30.0
golang.org/x/text v0.21.0 golang.org/x/term v0.29.0
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 golang.org/x/text v0.22.0
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.7 gorm.io/gorm v1.25.7
@@ -172,7 +173,6 @@ require (
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mark3labs/mcp-go v0.14.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/miekg/dns v1.1.62 // indirect github.com/miekg/dns v1.1.62 // indirect
@@ -200,6 +200,7 @@ require (
github.com/nrdcg/freemyip v0.3.0 // indirect github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
@@ -223,7 +224,7 @@ require (
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/stretchr/testify v1.10.0 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1115 // indirect
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 // indirect
github.com/therootcompany/xz v1.0.1 // indirect github.com/therootcompany/xz v1.0.1 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect github.com/theupdateframework/notary v0.7.0 // indirect
@@ -255,14 +256,13 @@ require (
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/arch v0.7.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
golang.org/x/image v0.18.0 // indirect golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/time v0.8.0 // indirect golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.28.0 // indirect golang.org/x/tools v0.28.0 // indirect
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/grpc v1.67.1 // indirect google.golang.org/grpc v1.67.1 // indirect
+14 -14
View File
@@ -652,8 +652,6 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mark3labs/mcp-go v0.14.1 h1:NsieyFbuWQaeZSWSHPvJ5TwJdQwu+1jmivAIVljeouY=
github.com/mark3labs/mcp-go v0.14.1/go.mod h1:xBB350hekQsJAK7gJAii8bcEoWemboLm2mRm5/+KBaU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -764,6 +762,8 @@ github.com/nrdcg/mailinabox v0.2.0 h1:IKq8mfKiVwNW2hQii/ng1dJ4yYMMv3HAP3fMFIq2CF
github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc= github.com/nrdcg/mailinabox v0.2.0/go.mod h1:0yxqeYOiGyxAu7Sb94eMxHPIOsPYXAjTeA9ZhePhGnc=
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg= github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw= github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
github.com/nrdcg/porkbun v0.4.0 h1:rWweKlwo1PToQ3H+tEO9gPRW0wzzgmI/Ob3n2Guticw=
github.com/nrdcg/porkbun v0.4.0/go.mod h1:/QMskrHEIM0IhC/wY7iTCUgINsxdT2WcOphktJ9+Q54=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk= github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -966,8 +966,8 @@ github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1065/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105 h1:lCs0dmezU6/8JcfNwEaam1Pm1RS/5MhXvNhf/X2y65s= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1115 h1:lYeFC379r76seyzN7PHmSxv1ji9knmsXQJglDB/K0WE=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1105/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1115/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065 h1:aEFtLD1ceyeljQXB1S2BjN0zjTkf0X3XmpuxFIiC29w=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1065/go.mod h1:HWvwy09hFSMXrj9SMvVRWV4U7rZO3l+WuogyNuxiT3M=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
@@ -1111,8 +1111,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1182,8 +1182,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1257,16 +1257,16 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1282,8 +1282,8 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=