Compare commits

...
75 Commits
Author SHA1 Message Date
wanghe 3cd0c89c4c Update CONTRIBUTING.md 2024-03-20 22:36:07 +08:00
ssongliu c27fcf18fc feat: 升级依赖 fit2cloud-ui-plus (#4250) 2024-03-20 10:50:07 +00:00
zhengkunwang b1f20d125e fix: 解决 i18n 导致页面错误的问题 (#4247) 2024-03-20 08:22:07 +00:00
zhengkunwang cd43bff44d fix: 解决文件选择框关闭再打开依然选中的问题 (#4243) 2024-03-20 03:32:07 +00:00
ssongliu 8cf8375911 feat: 镜像拉取日志 Unicode 转义 (#4239) 2024-03-19 14:40:06 +00:00
zhengkunwang ebc3195df4 feat: ACL 规则增加匹配方式 (#4237) 2024-03-19 12:58:06 +00:00
zhengkunwang ce69f6a142 fix: 解决应用安装快速跳转失败的问题 (#4236) 2024-03-19 09:12:07 +00:00
zhengkunwang f6777d51b5 fix: 解决应用安装快速跳转失败的问题 (#4235) 2024-03-19 07:36:06 +00:00
zhengkunwang d916f2ba6f fix: 解决网站配置文件更新之后参数丢失的问题 (#4231)
Refs https://github.com/1Panel-dev/1Panel/issues/4171
2024-03-19 05:50:06 +00:00
zhengkunwang 0e159f944a feat: 修改应用已安装和可升级页面的标签选择 (#4229) 2024-03-18 14:34:10 +00:00
Anye 329bb94d3c 增加PHP模板 (#4227)
* PHP模板增加“苹果CMS-V10”

* PHP增加“SeaCMS”扩展模板
2024-03-18 22:32:06 +08:00
zhengkunwang 7f2f7fa195 feat(waf): 优化配置更新接口 (#4226) 2024-03-18 13:16:10 +00:00
ssongliu 67c79bcde4 fix: 还原 Makefile 内容 (#4225) 2024-03-18 09:50:08 +00:00
zhengkunwang d7d7c7fd73 feat(waf): 增加内部接口请求安全性 (#4224) 2024-03-18 09:36:08 +00:00
ssongliu 94028d97cf style: 修改重启面板及服务器按钮位置 (#4221) 2024-03-18 09:34:08 +00:00
zhengkunwang cbfedb5d5e feat(waf): 优化请求次数记录方式 (#4210) 2024-03-16 14:44:06 +00:00
ssongliu 7784ee9575 fix: 解决快照恢复时校验 daemon.json 路径失败的问题 (#4205)
Refs #4206
2024-03-16 02:12:06 +00:00
ssongliu 29d5d82fa5 feat: 增加 xpack Makefile (#4204) 2024-03-15 09:26:08 +00:00
zhengkunwang 46f01032b8 fix: 解决回收站文件放置错误问题 (#4203)
Refs https://github.com/1Panel-dev/1Panel/issues/4199
2024-03-15 08:40:07 +00:00
zhengkunwang 341f45b4ef feat: 修改网站导入证书的描述 (#4202)
Refs https://github.com/1Panel-dev/1Panel/issues/4184
2024-03-15 08:08:07 +00:00
ssongliu a0907592b2 fix: 解决未登录情况下的页面跳转问题 (#4195) 2024-03-15 08:00:08 +00:00
zhengkunwang 825ebd19fb feat: 守护进程增加加载状态 (#4201)
Refs https://github.com/1Panel-dev/1Panel/issues/4129
2024-03-15 07:40:08 +00:00
zhengkunwang 8f24432a01 feat: 网站搜索增加网站关联域名匹配 (#4198)
Refs https://github.com/1Panel-dev/1Panel/pull/4198
2024-03-15 07:38:08 +00:00
zhengkunwang 6ce35b723b fix: 解决 Node 运行环境状态异常问题 (#4197) 2024-03-15 07:36:07 +00:00
ssongliu a33eabb4f0 fix: 移除计划任务报告中的 ANSI 转义 (#4193)
Refs #4097
2024-03-15 03:56:07 +00:00
ssongliu 34a6587ebc fix: 升级依赖 jackc/pgx/v5 (#4190) 2024-03-15 02:30:07 +00:00
ssongliu 17020ffe9f fix: 解决计划任务列表备份账号显示问题 (#4187)
Refs #4096
2024-03-14 10:24:07 +00:00
ssongliu 0f9b0d5d82 feat: 同步数据库时标记已删除数据库 (#4185)
Refs #4165
2024-03-14 10:22:07 +00:00
zhengkunwang be36103475 feat: 增加应用商店显示的过滤数量 (#4183) 2024-03-14 08:02:06 +00:00
ssongliu a3b5869c2b fix: 移除未使用依赖 2024-03-14 15:19:39 +08:00
maninhill 0807da41a5 Update README.md 2024-03-14 15:19:04 +08:00
maninhill ea6870e3c9 Update README.md 2024-03-14 15:07:13 +08:00
ssongliu 70b7daf23a fix: 升级依赖 google.golang.org/protobuf (#4176) 2024-03-14 03:24:06 +00:00
建国 1d01fa1e14 fix: 解决计划任务报告刷新异常的问题 (#4177)
Refs #4175
2024-03-14 03:14:06 +00:00
Akirami b643fa2ca3 fix: 修复应用商店详情页面部分图标比例错误 (#4168)
* fix: 修复应用商店详情页面部分图标比例错误

* fix: 替换为内联样式
2024-03-13 22:28:03 +08:00
zhengkunwang 77f4f64f0d feat(waf): 优化规则 (#4166) 2024-03-12 12:34:07 +00:00
ssongliu 3b697c7520 feat: 快照支持设置排除目录 (#4154)
Refs #4076
2024-03-12 03:04:08 +00:00
刘瑞斌 8ca6b9b6fa chore: add pnpm-lock.yaml to .gitignore (#4162) 2024-03-12 10:40:37 +08:00
zhengkunwang 0b66be7a3a fix: 解决安装应用过程中出现的状态异常问题 (#4156) 2024-03-11 12:34:08 +00:00
ssongliu f9f68a975c feat: 增加界面设置功能 (#4128) 2024-03-11 09:12:08 +00:00
L1nyz-tel ba63907a65 fix: 解决命令注入waf被绕过的问题 (#4131) 2024-03-08 22:43:56 +08:00
zhengkunwang 2687c91e65 feat: 升级 github.com/go-jose/go-jose/v3 版本到 v3.0.3 (#4126) 2024-03-08 08:50:06 +00:00
zhengkunwang eb82c168c5 feat: 升级 github.com/go-jose/go-jose/v3 版本到 v3.0.3 (#4121) 2024-03-08 08:14:07 +00:00
ssongliu 21b1242a94 fix: 解决部分 OneDrive 上传大文件失败的问题 (#4117)
Refs #4125
2024-03-08 08:12:06 +00:00
zhengkunwang a6784772b9 feat: 增加一个外部访问地址校验规则 (#4109)
Refs https://github.com/1Panel-dev/1Panel/issues/4091
2024-03-07 10:19:06 +00:00
igophper cf4fcaa07f fix: 解决编辑ssh端口同步到终端本地服务器问题 (#4043) (#4093) 2024-03-07 11:48:00 +08:00
zhengkunwang 62b1bb3191 feat: 升级 element-plus 到 2.6.0 (#4092) 2024-03-07 03:07:06 +00:00
ssongliu 8ce066a9d4 fix: 修改升级过程中的架构判断 (#4102) 2024-03-07 03:05:05 +00:00
wanghe-fit2cloud 7d8da406cf feat: 增加论坛链接 2024-03-07 10:52:00 +08:00
mobeicanyue b3d6b2b0de Issue 跳转 GitHub discussions (#4090) 2024-03-07 10:47:53 +08:00
zhengkunwang 21f24f2e10 feat(waf): 增加全局设置 (#4088) 2024-03-06 10:16:08 +00:00
ssongliu fe717d993b fix: 快照重试后更新列表状态 (#4086) 2024-03-06 07:58:07 +00:00
zhengkunwang 703da8a09a fix: 解决网站日志查看报错的问题 (#4082)
Refs https://github.com/1Panel-dev/1Panel/issues/3789
2024-03-06 03:56:07 +00:00
ssongliu a99f19471b fix: 优化计划任务脚本执行输出 (#4080) 2024-03-06 02:36:07 +00:00
zhengkunwang ca3be03ae7 feat: 本地应用同步增加校验 (#4071) 2024-03-05 10:10:08 +00:00
ssongliu 71ca8016e1 fix: 解决命令执行超时未生效的问题 (#4070) 2024-03-05 10:06:08 +00:00
ssongliu 5633bdd41e fix: 快照前清空服务器地址 (#4068) 2024-03-05 04:04:07 +00:00
ssongliu ab11d7257c fix: 快照重试时忽略已成功步骤 (#4067) 2024-03-05 03:14:07 +00:00
ssongliu 39b8de7ada fix: 限制快照跨架构恢复 (#4064)
Refs #4056
2024-03-04 14:59:11 +00:00
zhengkunwang c49c4b7863 feat(waf): 增加概览页 (#4060) 2024-03-04 08:53:06 +00:00
ssongliu d42df7184c style: 修改主机记住密码显示标题 (#4061) 2024-03-04 08:37:05 +00:00
ssongliu e7f172520a pref: 优化 MySQL 备份恢复方式 (#4059)
Refs #3955
2024-03-04 08:35:05 +00:00
zhengkunwang dca29174ea feat: 同步本地应用增加 data.yml 正确性验证 (#4058)
Refs https://github.com/1Panel-dev/1Panel/issues/4052
2024-03-04 03:53:03 +00:00
ssongliu 94181538a9 fix: 解决单位转换导致的内存限制问题 (#4055)
Refs #4040
2024-03-04 03:29:04 +00:00
zhengkunwang e9cc7bf1cf feat(waf):增加请求日志统计 (#4048) 2024-03-02 14:45:02 +00:00
ssongliu fa1e945b8d pref: 处理代码警告 (#4036) 2024-03-01 08:55:02 +00:00
zhengkunwang 94702a79a4 feat(waf): 修改攻击日志格式 (#4024) 2024-03-01 05:53:02 +00:00
ssongliu 24f573f9fd fix: 修改快照恢复逻辑 (#4031) 2024-03-01 13:51:34 +08:00
ssongliu 7e4606eda5 feat: 增加防篡改功能 (#4016) 2024-03-01 05:51:01 +00:00
ssongliu 73e118a3e5 style: 封装表格搜索组件 (#4028)
Refs #4017
2024-03-01 03:21:01 +00:00
ssongliu 514d68d395 fix: 解决容器资源使用率获取错误的问题 (#4023)
Refs #4022
2024-02-29 03:08:18 +00:00
QuentinHsu d0bb07796b fix: 初始化版本号变量为字符串类型,默认为空字符串 (#4003) 2024-02-28 17:10:47 +08:00
ssongliu 66a451fc1f fix: 解决登陆页刷新跳转的问题 (#4014)
Refs #3739
2024-02-28 09:06:15 +00:00
zhengkunwang c13e2a36cc feat: 增加 x-Pack 菜单 (#4009) 2024-02-28 08:44:15 +00:00
zhengkunwang 2affd2bc79 feat: 增加 waf 模块 (#4008) 2024-02-28 06:34:09 +00:00
243 changed files with 68462 additions and 13067 deletions
+5 -5
View File
@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 问题与讨论 / Questions & Discussions
url: https://github.com/1Panel-dev/1Panel/discussions
about: 提出针对本项目安装部署、使用及其他方面的相关问题 / Raise questions about the installation, deployment, use and other aspects of the project.
- name: 对 1Panel 项目有其他问题
url: https://1panel.cn
about: 如果想要进一步了解 1Panel 项目,欢迎发送邮件到 support@fit2cloud.com 进行提问。
- name: 反馈安全问题 (Report Security Bug)
url: mailto://support@fit2cloud.com
about: 请通过邮箱 support@fit2cloud.com 反馈安全问题 (Please report security vulnerabilities to support@fit2cloud.com)
url: https://bbs.fit2cloud.com/c/1p/7
about: 如果你对 1Panel 有其他想要提问的,我们欢迎到我们的官方社区进行提问。
-12
View File
@@ -1,12 +0,0 @@
name: 问题咨询
description: 提出针对本项目安装部署、使用及其他方面的相关问题
title: "[QUESTION]"
labels: question
assignees: wanghe-fit2cloud
body:
- type: textarea
id: description
attributes:
label: "请描述您的问题"
validations:
required: true
+5
View File
@@ -41,6 +41,11 @@ frontend/src/xpack
backend/xpack
backend/router/entry_xpack.go
backend/server/init_xpack.go
plugins/openresty/waf/db
plugins/openresty/waf/data
plugins/openresty/waf/sites
plugins/openresty/waf/conf/websites.json
.history/
dist/
+2 -2
View File
@@ -5,7 +5,7 @@ PR are always welcome, even if they only contain small fixes like typos or a few
Please submit a PR broken down into small changes bit by bit. A PR consisting of a lot features and code changes may be hard to review. It is recommended to submit PRs in an incremental fashion.
This [development guideline](https://kubeoperator.io/docs/dev/dev_manual/) contains information about repository structure, how to setup development environment, how to run it, and more.
This [development guideline](https://1panel.cn/docs/dev_manual/dev_manual/) contains information about repository structure, how to setup development environment, how to run it, and more.
Note: If you split your pull request to small changes, please make sure any of the changes goes to master will not break anything. Otherwise, it can not be merged until this feature complete.
@@ -22,4 +22,4 @@ When reporting issues, always include:
* Snapshots or log files if needed
Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can
replace those parts with "REDACTED" or other strings like "****".
replace those parts with "REDACTED" or other strings like "****".
+1 -2
View File
@@ -50,8 +50,7 @@ curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_
如果您在使用过程中发现任何安全问题,请通过以下方式直接联系我们:
- 邮箱:support@fit2cloud.com
- 电话:400-052-0755
- 邮箱:wanghe@fit2cloud.com
## Star History
+1 -5
View File
@@ -101,11 +101,7 @@ func (b *BaseApi) LoadRecordLog(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
content, err := cronjobService.LoadRecordLog(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
content := cronjobService.LoadRecordLog(req)
helper.SuccessWithData(c, content)
}
+16
View File
@@ -8,6 +8,22 @@ import (
"github.com/gin-gonic/gin"
)
// @Tags Dashboard
// @Summary Load os info
// @Description 获取服务器基础数据
// @Accept json
// @Success 200 {object} dto.OsInfo
// @Security ApiKeyAuth
// @Router /dashboard/base/os [get]
func (b *BaseApi) LoadDashboardOsInfo(c *gin.Context) {
data, err := dashboardService.LoadOsInfo()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Dashboard
// @Summary Load dashboard base info
// @Description 获取首页基础数据
+16
View File
@@ -203,3 +203,19 @@ func (b *BaseApi) OperateNodeModules(c *gin.Context) {
}
helper.SuccessWithOutData(c)
}
// @Tags Runtime
// @Summary Sync runtime status
// @Description 同步运行环境状态
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Router /runtimes/sync [post]
func (b *BaseApi) SyncStatus(c *gin.Context) {
err := runtimeService.SyncRuntimeStatus()
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
+2 -2
View File
@@ -38,8 +38,8 @@ type ContainerInfo struct {
}
type ResourceLimit struct {
CPU int `json:"cpu"`
Memory int `json:"memory"`
CPU int `json:"cpu"`
Memory uint64 `json:"memory"`
}
type ContainerOperate struct {
+8
View File
@@ -24,6 +24,14 @@ type DashboardBase struct {
CurrentInfo DashboardCurrent `json:"currentInfo"`
}
type OsInfo struct {
OS string `json:"os"`
Platform string `json:"platform"`
PlatformFamily string `json:"platformFamily"`
KernelArch string `json:"kernelArch"`
KernelVersion string `json:"kernelVersion"`
}
type DashboardCurrent struct {
Uptime uint64 `json:"uptime"`
TimeSinceUptime string `json:"timeSinceUptime"`
+1 -1
View File
@@ -41,7 +41,7 @@ type MysqlDBInfo struct {
Username string `json:"username"`
Password string `json:"password"`
Permission string `json:"permission"`
BackupCount int `json:"backupCount"`
IsDelete bool `json:"isDelete"`
Description string `json:"description"`
}
+1 -1
View File
@@ -20,7 +20,7 @@ type PostgresqlDBInfo struct {
Username string `json:"username"`
Password string `json:"password"`
SuperUser bool `json:"superUser"`
BackupCount int `json:"backupCount"`
IsDelete bool `json:"isDelete"`
Description string `json:"description"`
}
+2
View File
@@ -52,6 +52,8 @@ type SettingInfo struct {
AppStoreSyncStatus string `json:"appStoreSyncStatus"`
FileRecycleBin string `json:"fileRecycleBin"`
SnapshotIgnore string `json:"snapshotIgnore"`
}
type SettingUpdate struct {
+1
View File
@@ -9,5 +9,6 @@ type DatabaseMysql struct {
Username string `json:"username" gorm:"type:varchar(256);not null"`
Password string `json:"password" gorm:"type:varchar(256);not null"`
Permission string `json:"permission" gorm:"type:varchar(256);not null"`
IsDelete bool `json:"isDelete" gorm:"type:varchar(64)"`
Description string `json:"description" gorm:"type:varchar(256);"`
}
+2 -1
View File
@@ -8,6 +8,7 @@ type DatabasePostgresql struct {
Format string `json:"format" gorm:"type:varchar(64);not null"`
Username string `json:"username" gorm:"type:varchar(256);not null"`
Password string `json:"password" gorm:"type:varchar(256);not null"`
SuperUser bool `json:"superUser" gorm:"type:varchar(64)" `
SuperUser bool `json:"superUser" gorm:"type:varchar(64)"`
IsDelete bool `json:"isDelete" gorm:"type:varchar(64)"`
Description string `json:"description" gorm:"type:varchar(256);"`
}
+9 -2
View File
@@ -18,6 +18,7 @@ type IWebsiteRepo interface {
WithDefaultServer() DBOption
WithDomainLike(domain string) DBOption
WithRuntimeID(runtimeID uint) DBOption
WithIDs(ids []uint) DBOption
Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
List(opts ...DBOption) ([]model.Website, error)
GetFirst(opts ...DBOption) (model.Website, error)
@@ -36,9 +37,15 @@ func NewIWebsiteRepo() IWebsiteRepo {
type WebsiteRepo struct {
}
func (w *WebsiteRepo) WithAppInstallId(appInstallId uint) DBOption {
func (w *WebsiteRepo) WithAppInstallId(appInstallID uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("app_install_id = ?", appInstallId)
return db.Where("app_install_id = ?", appInstallID)
}
}
func (w *WebsiteRepo) WithIDs(ids []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("id in (?)", ids)
}
}
+6
View File
@@ -14,6 +14,7 @@ type IWebsiteDomainRepo interface {
WithWebsiteId(websiteId uint) DBOption
WithPort(port int) DBOption
WithDomain(domain string) DBOption
WithDomainLike(domain string) DBOption
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteDomain, error)
GetFirst(opts ...DBOption) (model.WebsiteDomain, error)
GetBy(opts ...DBOption) ([]model.WebsiteDomain, error)
@@ -44,6 +45,11 @@ func (w WebsiteDomainRepo) WithDomain(domain string) DBOption {
return db.Where("domain = ?", domain)
}
}
func (w WebsiteDomainRepo) WithDomainLike(domain string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("domain like ?", "%"+domain+"%")
}
}
func (w WebsiteDomainRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteDomain, error) {
var domains []model.WebsiteDomain
db := getDb(opts...).Model(&model.WebsiteDomain{})
+6 -4
View File
@@ -527,7 +527,7 @@ func (a *AppInstallService) ChangeAppPort(req request.PortUpdate) error {
return nil
}
if err := updateInstallInfoInDB(req.Key, req.Name, "port", true, strconv.FormatInt(req.Port, 10)); err != nil {
if err := updateInstallInfoInDB(req.Key, req.Name, "port", strconv.FormatInt(req.Port, 10)); err != nil {
return nil
}
@@ -749,8 +749,10 @@ func syncAppInstallStatus(appInstall *model.AppInstall) error {
switch {
case count == 0:
appInstall.Status = constant.Error
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
if appInstall.Status != constant.Error {
appInstall.Status = constant.SyncErr
appInstall.Message = buserr.WithName("ErrContainerNotFound", strings.Join(containerNames, ",")).Error()
}
case exitedCount == total:
appInstall.Status = constant.Stopped
case runningCount == total:
@@ -776,7 +778,7 @@ func syncAppInstallStatus(appInstall *model.AppInstall) error {
return nil
}
func updateInstallInfoInDB(appKey, appName, param string, isRestart bool, value interface{}) error {
func updateInstallInfoInDB(appKey, appName, param string, value interface{}) error {
if param != "password" && param != "port" && param != "user-password" {
return nil
}
+58 -25
View File
@@ -849,38 +849,34 @@ func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
func upApp(appInstall *model.AppInstall, pullImages bool) {
upProject := func(appInstall *model.AppInstall) (err error) {
if err == nil {
var (
out string
errMsg string
)
if pullImages && appInstall.App.Type != "php" {
out, err = compose.Pull(appInstall.GetComposePath())
if err != nil {
if out != "" {
if strings.Contains(out, "no such host") {
errMsg = i18n.GetMsgByKey("ErrNoSuchHost") + ":"
}
if strings.Contains(out, "timeout") {
errMsg = i18n.GetMsgByKey("ErrImagePullTimeOut") + ":"
}
appInstall.Message = errMsg + out
}
return err
}
}
out, err = compose.Up(appInstall.GetComposePath())
var (
out string
errMsg string
)
if pullImages && appInstall.App.Type != "php" {
out, err = compose.Pull(appInstall.GetComposePath())
if err != nil {
if out != "" {
if strings.Contains(out, "no such host") {
errMsg = i18n.GetMsgByKey("ErrNoSuchHost") + ":"
}
if strings.Contains(out, "timeout") {
errMsg = i18n.GetMsgByKey("ErrImagePullTimeOut") + ":"
}
appInstall.Message = errMsg + out
}
return err
}
return
} else {
return
}
out, err = compose.Up(appInstall.GetComposePath())
if err != nil {
if out != "" {
appInstall.Message = errMsg + out
}
return err
}
return
}
if err := upProject(appInstall); err != nil {
appInstall.Status = constant.Error
@@ -996,6 +992,43 @@ func handleLocalAppDetail(versionDir string, appDetail *model.AppDetail) error {
if err != nil {
return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err)
}
var appParam dto.AppForm
if err = json.Unmarshal(dataJson, &appParam); err != nil {
return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err)
}
for _, formField := range appParam.FormFields {
if strings.Contains(formField.EnvKey, " ") {
return buserr.WithName(constant.ErrAppParamKey, formField.EnvKey)
}
}
var dataMap map[string]interface{}
err = yaml.Unmarshal(paramByte, &dataMap)
if err != nil {
return buserr.WithMap(constant.ErrFileParseApp, map[string]interface{}{"name": "data.yml", "err": err.Error()}, err)
}
additionalProperties, ok := dataMap["additionalProperties"].(map[string]interface{})
if !ok {
return buserr.WithName(constant.ErrAppParamKey, "additionalProperties")
}
formFieldsInterface, ok := additionalProperties["formFields"]
if ok {
formFields, ok := formFieldsInterface.([]interface{})
if !ok {
return buserr.WithName(constant.ErrAppParamKey, "formFields")
}
for _, item := range formFields {
field := item.(map[string]interface{})
for key, value := range field {
if value == nil {
return buserr.WithName(constant.ErrAppParamKey, key)
}
}
}
}
appDetail.Params = string(dataJson)
return nil
}
+1 -1
View File
@@ -115,7 +115,7 @@ func handleAppBackup(install *model.AppInstall, backupDir, fileName string) erro
if err != nil {
return err
}
if err := handleMysqlBackup(db.MysqlName, db.Name, tmpDir, fmt.Sprintf("%s.sql.gz", install.Name)); err != nil {
if err := handleMysqlBackup(db.MysqlName, resource.Key, db.Name, tmpDir, fmt.Sprintf("%s.sql.gz", install.Name)); err != nil {
return err
}
case constant.AppPostgresql:
+12 -4
View File
@@ -29,7 +29,7 @@ func (u *BackupService) MysqlBackup(req dto.CommonBackup) error {
targetDir := path.Join(localDir, itemDir)
fileName := fmt.Sprintf("%s_%s.sql.gz", req.DetailName, timeNow+common.RandStrAndNum(5))
if err := handleMysqlBackup(req.Name, req.DetailName, targetDir, fileName); err != nil {
if err := handleMysqlBackup(req.Name, req.Type, req.DetailName, targetDir, fileName); err != nil {
return err
}
@@ -100,18 +100,20 @@ func (u *BackupService) MysqlRecoverByUpload(req dto.CommonRecover) error {
return nil
}
func handleMysqlBackup(database, dbName, targetDir, fileName string) error {
func handleMysqlBackup(database, dbType, dbName, targetDir, fileName string) error {
dbInfo, err := mysqlRepo.Get(commonRepo.WithByName(dbName), mysqlRepo.WithByMysqlName(database))
if err != nil {
return err
}
cli, _, err := LoadMysqlClientByFrom(database)
cli, version, err := LoadMysqlClientByFrom(database)
if err != nil {
return err
}
backupInfo := client.BackupInfo{
Name: dbName,
Type: dbType,
Version: version,
Format: dbInfo.Format,
TargetDir: targetDir,
FileName: fileName,
@@ -134,7 +136,7 @@ func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error {
if err != nil {
return err
}
cli, _, err := LoadMysqlClientByFrom(req.Name)
cli, version, err := LoadMysqlClientByFrom(req.Name)
if err != nil {
return err
}
@@ -143,6 +145,8 @@ func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error {
rollbackFile := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/%s/%s_%s.sql.gz", req.Type, req.DetailName, time.Now().Format("20060102150405")))
if err := cli.Backup(client.BackupInfo{
Name: req.DetailName,
Type: req.Type,
Version: version,
Format: dbInfo.Format,
TargetDir: path.Dir(rollbackFile),
FileName: path.Base(rollbackFile),
@@ -156,6 +160,8 @@ func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error {
global.LOG.Info("recover failed, start to rollback now")
if err := cli.Recover(client.RecoverInfo{
Name: req.DetailName,
Type: req.Type,
Version: version,
Format: dbInfo.Format,
SourceFile: rollbackFile,
@@ -172,6 +178,8 @@ func handleMysqlRecover(req dto.CommonRecover, isRollback bool) error {
}
if err := cli.Recover(client.RecoverInfo{
Name: req.DetailName,
Type: req.Type,
Version: version,
Format: dbInfo.Format,
SourceFile: req.File,
+5 -5
View File
@@ -5,7 +5,6 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"io"
"os"
"os/exec"
@@ -18,6 +17,8 @@ import (
"time"
"unicode/utf8"
"github.com/pkg/errors"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
@@ -336,7 +337,7 @@ func (u *ContainerService) LoadResourceLimit() (*dto.ResourceLimit, error) {
data := dto.ResourceLimit{
CPU: cpuCounts,
Memory: int(memoryInfo.Total),
Memory: memoryInfo.Total,
}
return &data, nil
}
@@ -683,7 +684,6 @@ func (u *ContainerService) ContainerLogs(wsConn *websocket.Conn, containerType,
global.LOG.Errorf("read bytes from log failed, err: %v", err)
continue
}
// check if the bytes is valid utf8
if !utf8.Valid(buffer[:n]) {
continue
}
@@ -723,7 +723,6 @@ func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error
if cache, ok := stats.MemoryStats.Stats["cache"]; ok {
data.Cache = float64(cache) / 1024 / 1024
}
data.Memory = data.Memory - data.Cache
data.NetworkRX, data.NetworkTX = calculateNetwork(stats.Networks)
data.ShotTime = stats.Read
return &data, nil
@@ -791,7 +790,7 @@ func calculateCPUPercentUnix(stats *types.StatsJSON) float64 {
}
func calculateMemPercentUnix(memStats types.MemoryStats) float64 {
memPercent := 0.0
memUsage := float64(memStats.Usage - memStats.Stats["cache"])
memUsage := float64(memStats.Usage)
memLimit := float64(memStats.Limit)
if memUsage > 0.0 && memLimit > 0.0 {
memPercent = (memUsage / memLimit) * 100.0
@@ -895,6 +894,7 @@ func loadCpuAndMem(client *client.Client, container string) dto.ContainerListSta
data.MemoryCache = stats.MemoryStats.Stats["cache"]
data.MemoryUsage = stats.MemoryStats.Usage
data.MemoryLimit = stats.MemoryStats.Limit
data.MemoryPercent = calculateMemPercentUnix(stats.MemoryStats)
return data
}
+6 -7
View File
@@ -11,7 +11,6 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/jinzhu/copier"
@@ -33,7 +32,7 @@ type ICronjobService interface {
StartJob(cronjob *model.Cronjob) (string, error)
CleanRecord(req dto.CronjobClean) error
LoadRecordLog(req dto.OperateByID) (string, error)
LoadRecordLog(req dto.OperateByID) string
}
func NewICronjobService() ICronjobService {
@@ -77,19 +76,19 @@ func (u *CronjobService) SearchRecords(search dto.SearchRecord) (int64, interfac
return total, dtoCronjobs, err
}
func (u *CronjobService) LoadRecordLog(req dto.OperateByID) (string, error) {
func (u *CronjobService) LoadRecordLog(req dto.OperateByID) string {
record, err := cronjobRepo.GetRecord(commonRepo.WithByID(req.ID))
if err != nil {
return "", err
return ""
}
if _, err := os.Stat(record.Records); err != nil {
return "", buserr.New("ErrHttpReqNotFound")
return ""
}
content, err := os.ReadFile(record.Records)
if err != nil {
return "", err
return ""
}
return string(content), nil
return string(content)
}
func (u *CronjobService) CleanRecord(req dto.CronjobClean) error {
+1 -1
View File
@@ -109,7 +109,7 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
backupDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format("20060102150405")+common.RandStrAndNum(5))
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" {
if err := handleMysqlBackup(dbInfo.Database, dbInfo.Name, backupDir, record.FileName); err != nil {
if err := handleMysqlBackup(dbInfo.Database, dbInfo.DBType, dbInfo.Name, backupDir, record.FileName); err != nil {
return err
}
} else {
+4 -1
View File
@@ -82,7 +82,10 @@ func (u *CronjobService) HandleJob(cronjob *model.Cronjob) {
}
if err != nil {
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), string(message))
if len(message) != 0 {
record.Records, _ = mkdirAndWriteFile(cronjob, record.StartTime, message)
}
cronjobRepo.EndRecords(record, constant.StatusFailed, err.Error(), record.Records)
return
}
if len(message) != 0 {
+26 -1
View File
@@ -22,6 +22,7 @@ import (
type DashboardService struct{}
type IDashboardService interface {
LoadOsInfo() (*dto.OsInfo, error)
LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error)
LoadCurrentInfo(ioOption string, netOption string) *dto.DashboardCurrent
@@ -49,6 +50,27 @@ func (u *DashboardService) Restart(operation string) error {
return nil
}
func (u *DashboardService) LoadOsInfo() (*dto.OsInfo, error) {
var baseInfo dto.OsInfo
hostInfo, err := host.Info()
if err != nil {
return nil, err
}
baseInfo.OS = hostInfo.OS
baseInfo.Platform = hostInfo.Platform
baseInfo.PlatformFamily = hostInfo.PlatformFamily
baseInfo.KernelArch = hostInfo.KernelArch
baseInfo.KernelVersion = hostInfo.KernelVersion
if baseInfo.KernelArch == "armv7l" {
baseInfo.KernelArch = "armv7"
}
if baseInfo.KernelArch == "x86_64" {
baseInfo.KernelArch = "amd64"
}
return &baseInfo, nil
}
func (u *DashboardService) LoadBaseInfo(ioOption string, netOption string) (*dto.DashboardBase, error) {
var baseInfo dto.DashboardBase
hostInfo, err := host.Info()
@@ -183,7 +205,10 @@ func loadDiskInfo() []dto.DiskInfo {
var datas []dto.DiskInfo
stdout, err := cmd.ExecWithTimeOut("df -hT -P|grep '/'|grep -v tmpfs|grep -v 'snap/core'|grep -v udev", 2*time.Second)
if err != nil {
return datas
stdout, err = cmd.ExecWithTimeOut("df -lhT -P|grep '/'|grep -v tmpfs|grep -v 'snap/core'|grep -v udev", 1*time.Second)
if err != nil {
return datas
}
}
lines := strings.Split(stdout, "\n")
+9 -4
View File
@@ -190,11 +190,13 @@ func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error {
if err != nil {
return err
}
deleteList := databases
for _, data := range datas {
hasOld := false
for _, oldData := range databases {
if strings.EqualFold(oldData.Name, data.Name) && strings.EqualFold(oldData.MysqlName, data.MysqlName) {
for i := 0; i < len(databases); i++ {
if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MysqlName, data.MysqlName) {
hasOld = true
deleteList = append(deleteList[:i], deleteList[i+1:]...)
break
}
}
@@ -208,6 +210,9 @@ func (u *MysqlService) LoadFromRemote(req dto.MysqlLoadDB) error {
}
}
}
for _, delItem := range deleteList {
_ = mysqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true})
}
return nil
}
@@ -342,7 +347,7 @@ func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error {
}
global.LOG.Infof("start to update mysql password used by app %s-%s", appModel.Key, appInstall.Name)
if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", true, req.Value); err != nil {
if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", req.Value); err != nil {
return err
}
}
@@ -355,7 +360,7 @@ func (u *MysqlService) ChangePassword(req dto.ChangeDBInfo) error {
return nil
}
if err := updateInstallInfoInDB(req.Type, req.Database, "password", false, req.Value); err != nil {
if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil {
return err
}
return nil
+9 -4
View File
@@ -216,11 +216,13 @@ func (u *PostgresqlService) LoadFromRemote(database string) error {
if err != nil {
return err
}
deleteList := databases
for _, data := range datas {
hasOld := false
for _, oldData := range databases {
if strings.EqualFold(oldData.Name, data.Name) && strings.EqualFold(oldData.PostgresqlName, data.PostgresqlName) {
for i := 0; i < len(databases); i++ {
if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].PostgresqlName, data.PostgresqlName) {
hasOld = true
deleteList = append(deleteList[:i], deleteList[i+1:]...)
break
}
}
@@ -234,6 +236,9 @@ func (u *PostgresqlService) LoadFromRemote(database string) error {
}
}
}
for _, delItem := range deleteList {
_ = postgresqlRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true})
}
return nil
}
@@ -393,7 +398,7 @@ func (u *PostgresqlService) ChangePassword(req dto.ChangeDBInfo) error {
}
global.LOG.Infof("start to update postgresql password used by app %s-%s", appModel.Key, appInstall.Name)
if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", true, req.Value); err != nil {
if err := updateInstallInfoInDB(appModel.Key, appInstall.Name, "user-password", req.Value); err != nil {
return err
}
}
@@ -406,7 +411,7 @@ func (u *PostgresqlService) ChangePassword(req dto.ChangeDBInfo) error {
return nil
}
if err := updateInstallInfoInDB(req.Type, req.Database, "password", false, req.Value); err != nil {
if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil {
return err
}
return nil
+2 -2
View File
@@ -55,10 +55,10 @@ func (u *RedisService) UpdateConf(req dto.RedisConfUpdate) error {
}
func (u *RedisService) ChangePassword(req dto.ChangeRedisPass) error {
if err := updateInstallInfoInDB("redis", "", "password", true, req.Value); err != nil {
if err := updateInstallInfoInDB("redis", "", "password", req.Value); err != nil {
return err
}
if err := updateInstallInfoInDB("redis-commander", "", "password", true, req.Value); err != nil {
if err := updateInstallInfoInDB("redis-commander", "", "password", req.Value); err != nil {
return err
}
+12 -14
View File
@@ -153,22 +153,20 @@ func (r RecycleBinService) Clear() error {
}
func getClashDir(realPath string) (string, error) {
trimmedPath := strings.Trim(realPath, "/")
parts := strings.Split(trimmedPath, "/")
dir := ""
if len(parts) > 0 {
dir = parts[0]
partitions, err := disk.Partitions(false)
if err != nil {
return "", err
partitions, err := disk.Partitions(false)
if err != nil {
return "", err
}
for _, p := range partitions {
if p.Mountpoint == "/" {
continue
}
for _, p := range partitions {
if p.Mountpoint == dir {
if err = createClashDir(path.Join(p.Mountpoint, ".1panel_clash")); err != nil {
return "", err
}
return dir, nil
if strings.HasPrefix(realPath, p.Mountpoint) {
clashDir := path.Join(p.Mountpoint, ".1panel_clash")
if err = createClashDir(path.Join(p.Mountpoint, ".1panel_clash")); err != nil {
return "", err
}
return clashDir, nil
}
}
return constant.RecycleBinDir, createClashDir(constant.RecycleBinDir)
+14
View File
@@ -42,6 +42,7 @@ type IRuntimeService interface {
GetNodeModules(req request.NodeModuleReq) ([]response.NodeModule, error)
OperateNodeModules(req request.NodeModuleOperateReq) error
SyncForRestart() error
SyncRuntimeStatus() error
}
func NewRuntimeService() IRuntimeService {
@@ -586,3 +587,16 @@ func (r *RuntimeService) SyncForRestart() error {
}
return nil
}
func (r *RuntimeService) SyncRuntimeStatus() error {
runtimes, err := runtimeRepo.List()
if err != nil {
return err
}
for _, runtime := range runtimes {
if runtime.Type == constant.RuntimeNode {
_ = SyncRuntimeContainerStatus(&runtime)
}
}
return nil
}
+12 -25
View File
@@ -181,33 +181,20 @@ func SyncRuntimeContainerStatus(runtime *model.Runtime) error {
}
container := containers[0]
interval := 10 * time.Second
retries := 60
for i := 0; i < retries; i++ {
resp, err := cli.InspectContainer(container.ID)
if err != nil {
time.Sleep(interval)
continue
switch container.State {
case "exited":
runtime.Status = constant.RuntimeError
case "running":
runtime.Status = constant.RuntimeRunning
case "paused":
runtime.Status = constant.RuntimeStopped
default:
if runtime.Status != constant.RuntimeBuildIng {
runtime.Status = constant.RuntimeStopped
}
if resp.State.Health != nil {
status := strings.ToLower(resp.State.Health.Status)
switch status {
case "starting":
runtime.Status = constant.RuntimeStarting
_ = runtimeRepo.Save(runtime)
case "healthy":
runtime.Status = constant.RuntimeRunning
_ = runtimeRepo.Save(runtime)
return nil
case "unhealthy":
runtime.Status = constant.RuntimeUnhealthy
_ = runtimeRepo.Save(runtime)
return nil
}
}
time.Sleep(interval)
}
return nil
return runtimeRepo.Save(runtime)
}
func buildRuntime(runtime *model.Runtime, oldImageID string, rebuild bool) {
+2 -2
View File
@@ -280,7 +280,7 @@ func (u *SettingService) UpdateSSL(c *gin.Context, req dto.SSLUpdate) error {
if err := fileOp.WriteFile(path.Join(secretDir, "server.key.tmp"), strings.NewReader(key), 0600); err != nil {
return err
}
if err := checkCertValid(req.Domain); err != nil {
if err := checkCertValid(); err != nil {
return err
}
if err := fileOp.Rename(path.Join(secretDir, "server.crt.tmp"), path.Join(secretDir, "server.crt")); err != nil {
@@ -418,7 +418,7 @@ func loadInfoFromCert() (*dto.SSLInfo, error) {
}, nil
}
func checkCertValid(domain string) error {
func checkCertValid() error {
certificate, err := os.ReadFile(path.Join(global.CONF.System.BaseDir, "1panel/secret/server.crt.tmp"))
if err != nil {
return err
+69 -661
View File
@@ -6,8 +6,6 @@ import (
"fmt"
"os"
"path"
"regexp"
"strconv"
"strings"
"sync"
"time"
@@ -18,10 +16,10 @@ import (
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/compose"
"github.com/1Panel-dev/1Panel/backend/utils/docker"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
"github.com/shirou/gopsutil/v3/host"
)
type SnapshotService struct {
@@ -74,7 +72,7 @@ func (u *SnapshotService) SnapshotImport(req dto.SnapshotImport) error {
for _, snap := range req.Names {
shortName := strings.TrimPrefix(snap, "snapshot_")
nameItems := strings.Split(shortName, "_")
if !strings.HasPrefix(shortName, "1panel_v") || !strings.HasSuffix(shortName, ".tar.gz") || len(nameItems) != 3 {
if !strings.HasPrefix(shortName, "1panel_v") || !strings.HasSuffix(shortName, ".tar.gz") || len(nameItems) < 3 {
return fmt.Errorf("incorrect snapshot name format of %s", shortName)
}
if strings.HasSuffix(snap, ".tar.gz") {
@@ -137,26 +135,13 @@ func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
if err != nil {
return err
}
if hasOs(snap.Name) && !strings.Contains(snap.Name, loadOs()) {
return fmt.Errorf("Restoring snapshots(%s) between different server architectures(%s) is not supported.", snap.Name, loadOs())
}
if !req.IsNew && len(snap.InterruptStep) != 0 && len(snap.RollbackStatus) != 0 {
return fmt.Errorf("the snapshot has been rolled back and cannot be restored again")
}
isNewSnapshot := isNewSnapVersion(snap.Version)
isReTry := false
if len(snap.InterruptStep) != 0 && !req.IsNew {
isReTry = true
}
backup, err := backupRepo.Get(commonRepo.WithByType(snap.DefaultDownload))
if err != nil {
return err
}
client, err := NewIBackupService().NewClient(&backup)
if err != nil {
return err
}
localDir, err := loadLocalDir()
if err != nil {
return err
}
baseDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("system/%s", snap.Name))
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
_ = os.MkdirAll(baseDir, os.ModePerm)
@@ -164,152 +149,7 @@ func (u *SnapshotService) SnapshotRecover(req dto.SnapshotRecover) error {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"recover_status": constant.StatusWaiting})
_ = settingRepo.Update("SystemStatus", "Recovering")
go func() {
_ = global.Cron.Stop()
defer func() {
global.Cron.Start()
}()
operation := "recover"
if isReTry {
operation = "re-recover"
}
if !isReTry || snap.InterruptStep == "Download" || (isReTry && req.ReDownload) {
itemBackupPath := strings.TrimPrefix(backup.BackupPath, "/")
itemBackupPath = strings.TrimSuffix(itemBackupPath, "/")
ok, err := client.Download(fmt.Sprintf("%s/system_snapshot/%s.tar.gz", itemBackupPath, snap.Name), fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name))
if err != nil || !ok {
if req.ReDownload {
updateRecoverStatus(snap.ID, snap.InterruptStep, constant.StatusFailed, fmt.Sprintf("download file %s from %s failed, err: %v", snap.Name, backup.Type, err))
return
}
updateRecoverStatus(snap.ID, "Download", constant.StatusFailed, fmt.Sprintf("download file %s from %s failed, err: %v", snap.Name, backup.Type, err))
return
}
isReTry = false
}
fileOp := files.NewFileOp()
if !isReTry || snap.InterruptStep == "Decompress" || (isReTry && req.ReDownload) {
if err := handleUnTar(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir); err != nil {
if req.ReDownload {
updateRecoverStatus(snap.ID, snap.InterruptStep, constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
updateRecoverStatus(snap.ID, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
isReTry = false
}
rootDir := fmt.Sprintf("%s/%s", baseDir, snap.Name)
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", rootDir))
if err != nil {
updateRecoverStatus(snap.ID, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
if snap.InterruptStep == "Readjson" {
isReTry = false
}
u.OriginalPath = fmt.Sprintf("%s/1panel_original/original_%s", snapJson.BaseDir, snap.Name)
_ = os.MkdirAll(u.OriginalPath, os.ModePerm)
snapJson.OldBaseDir = global.CONF.System.BaseDir
snapJson.OldPanelDataDir = path.Join(global.CONF.System.BaseDir, "1panel")
snapJson.OldBackupDataDir = localDir
recoverPanelDir := fmt.Sprintf("%s/%s/1panel", baseDir, snap.Name)
liveRestore := false
if !isNewSnapshot {
if !isReTry || snap.InterruptStep == "LoadDockerJson" {
snapJson.OldDockerDataDir, liveRestore, err = u.loadDockerDataDir()
if err != nil {
updateRecoverStatus(snap.ID, "LoadDockerJson", constant.StatusFailed, fmt.Sprintf("load docker data dir failed, err: %v", err))
return
}
isReTry = false
}
if liveRestore {
if err := u.updateLiveRestore(false); err != nil {
updateRecoverStatus(snap.ID, "UpdateLiveRestore", constant.StatusFailed, fmt.Sprintf("update docker daemon.json live-restore conf failed, err: %v", err))
return
}
isReTry = false
}
_ = u.saveJson(snapJson, u.OriginalPath)
_, _ = cmd.Exec("systemctl stop docker")
if !isReTry || snap.InterruptStep == "DockerDir" {
if err := u.handleDockerDatas(fileOp, operation, rootDir, snapJson.DockerDataDir); err != nil {
updateRecoverStatus(snap.ID, "DockerDir", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
} else {
_ = u.saveJson(snapJson, u.OriginalPath)
if !isReTry || snap.InterruptStep == "DockerDir" {
if err := u.handleDockerDatasWithSave(fileOp, operation, rootDir, ""); err != nil {
updateRecoverStatus(snap.ID, "DockerDir", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
}
if !isReTry || snap.InterruptStep == "DaemonJson" {
if err := u.handleDaemonJson(fileOp, operation, rootDir+"/docker/daemon.json", u.OriginalPath); err != nil {
updateRecoverStatus(snap.ID, "DaemonJson", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
_, _ = cmd.Exec("systemctl restart docker")
if !isReTry || snap.InterruptStep == "1PanelBinary" {
if err := u.handlePanelBinary(fileOp, operation, recoverPanelDir+"/1panel", u.OriginalPath+"/1panel"); err != nil {
updateRecoverStatus(snap.ID, "1PanelBinary", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "1PctlBinary" {
if err := u.handlePanelctlBinary(fileOp, operation, recoverPanelDir+"/1pctl", u.OriginalPath+"/1pctl"); err != nil {
updateRecoverStatus(snap.ID, "1PctlBinary", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "1PanelService" {
if err := u.handlePanelService(fileOp, operation, recoverPanelDir+"/1panel.service", u.OriginalPath+"/1panel.service"); err != nil {
updateRecoverStatus(snap.ID, "1PanelService", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "1PanelBackups" {
if err := u.handleBackupDatas(fileOp, operation, rootDir, snapJson.BackupDataDir); err != nil {
updateRecoverStatus(snap.ID, "1PanelBackups", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if !isReTry || snap.InterruptStep == "1PanelData" {
if err := u.handlePanelDatas(snap.ID, fileOp, operation, rootDir, snapJson.PanelDataDir, localDir, snapJson.OldDockerDataDir); err != nil {
updateRecoverStatus(snap.ID, "1PanelData", constant.StatusFailed, err.Error())
return
}
isReTry = false
}
if isNewSnapshot {
_ = rebuildAllAppInstall()
}
_ = os.RemoveAll(rootDir)
global.LOG.Info("recover successful")
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
}()
go u.HandleSnapshotRecover(snap, true, req)
return nil
}
@@ -319,133 +159,9 @@ func (u *SnapshotService) SnapshotRollback(req dto.SnapshotRecover) error {
if err != nil {
return err
}
if snap.InterruptStep == "Download" || snap.InterruptStep == "Decompress" || snap.InterruptStep == "Readjson" {
return nil
}
localDir, err := loadLocalDir()
if err != nil {
return err
}
fileOp := files.NewFileOp()
isNewSnapshot := isNewSnapVersion(snap.Version)
rootDir := path.Join(localDir, fmt.Sprintf("system/%s/%s", snap.Name, snap.Name))
_ = settingRepo.Update("SystemStatus", "Rollbacking")
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"rollback_status": constant.StatusWaiting})
go func() {
_ = global.Cron.Stop()
defer func() {
global.Cron.Start()
}()
u.OriginalPath = fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
if _, err := os.Stat(u.OriginalPath); err != nil && os.IsNotExist(err) {
return
}
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", u.OriginalPath))
if err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, fmt.Sprintf("read snapjson file failed, err: %v", err))
return
}
if !isNewSnapshot {
_, _ = cmd.Exec("systemctl stop docker")
if err := u.handleDockerDatas(fileOp, "rollback", u.OriginalPath, snapJson.OldDockerDataDir); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
defer func() {
_, _ = cmd.Exec("systemctl restart docker")
}()
if snap.InterruptStep == "DockerDir" {
return
}
if snapJson.LiveRestoreEnabled {
if err := u.updateLiveRestore(true); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
}
if snap.InterruptStep == "UpdateLiveRestore" {
return
}
} else {
if err := u.handleDockerDatasWithSave(fileOp, "rollback", u.OriginalPath, ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
defer func() {
_ = rebuildAllAppInstall()
}()
if snap.InterruptStep == "DockerDir" {
return
}
}
if err := u.handleDaemonJson(fileOp, "rollback", u.OriginalPath+"/daemon.json", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if snap.InterruptStep == "DaemonJson" {
return
}
if err := u.handlePanelBinary(fileOp, "rollback", u.OriginalPath+"/1panel", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if snap.InterruptStep == "1PanelBinary" {
return
}
if err := u.handlePanelctlBinary(fileOp, "rollback", u.OriginalPath+"/1pctl", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if snap.InterruptStep == "1PctlBinary" {
return
}
if err := u.handlePanelService(fileOp, "rollback", u.OriginalPath+"/1panel.service", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if snap.InterruptStep == "1PanelService" {
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
return
}
if err := u.handleBackupDatas(fileOp, "rollback", u.OriginalPath, snapJson.OldBackupDataDir); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if snap.InterruptStep == "1PanelBackups" {
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
return
}
if err := u.handlePanelDatas(snap.ID, fileOp, "rollback", u.OriginalPath, snapJson.OldPanelDataDir, "", ""); err != nil {
updateRollbackStatus(snap.ID, constant.StatusFailed, err.Error())
return
}
if snap.InterruptStep == "1PanelData" {
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
return
}
_ = os.RemoveAll(rootDir)
global.LOG.Info("rollback successful")
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
}()
return nil
}
func (u *SnapshotService) saveJson(snapJson SnapshotJson, path string) error {
remarkInfo, _ := json.MarshalIndent(snapJson, "", "\t")
if err := os.WriteFile(fmt.Sprintf("%s/snapshot.json", path), remarkInfo, 0640); err != nil {
return err
}
req.IsNew = false
snap.InterruptStep = "Readjson"
go u.HandleSnapshotRecover(snap, false, req)
return nil
}
@@ -477,9 +193,10 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
if req.ID == 0 {
versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
name := fmt.Sprintf("1panel_%s_%s", versionItem.Value, timeNow)
name := fmt.Sprintf("1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow)
if isCronjob {
name = fmt.Sprintf("snapshot_1panel_%s_%s", versionItem.Value, timeNow)
name = fmt.Sprintf("snapshot_1panel_%s_%s_%s", versionItem.Value, loadOs(), timeNow)
}
rootDir = path.Join(localDir, "system", name)
@@ -499,6 +216,7 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
if err != nil {
return "", err
}
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusWaiting})
snapStatus, _ = snapshotRepo.GetStatus(snap.ID)
if snapStatus.ID == 0 {
snapStatus.SnapID = snap.ID
@@ -548,17 +266,23 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
}
snapPanelData(itemHelper, localDir, backupPanelDir)
if snapStatus.PanelData != constant.StatusDone {
snapPanelData(itemHelper, localDir, backupPanelDir)
}
if snapStatus.PanelData != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
}
snapCompress(itemHelper, rootDir)
if snapStatus.Compress != constant.StatusDone {
snapCompress(itemHelper, rootDir)
}
if snapStatus.Compress != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
}
snapUpload(itemHelper, req.From, fmt.Sprintf("%s.tar.gz", rootDir))
if snapStatus.Upload != constant.StatusDone {
snapUpload(itemHelper, req.From, fmt.Sprintf("%s.tar.gz", rootDir))
}
if snapStatus.Upload != constant.StatusDone {
_ = snapshotRepo.Update(snap.ID, map[string]interface{}{"status": constant.StatusFailed})
return
@@ -599,263 +323,6 @@ func (u *SnapshotService) HandleSnapshot(isCronjob bool, logPath string, req dto
return snap.Name, nil
}
func (u *SnapshotService) handleDockerDatas(fileOp files.FileOp, operation string, source, target string) error {
switch operation {
case "snapshot":
if err := handleSnapTar(source, target, "docker_data.tar.gz", ""); err != nil {
return fmt.Errorf("backup docker data failed, err: %v", err)
}
case "recover":
if err := handleSnapTar(target, u.OriginalPath, "docker_data.tar.gz", ""); err != nil {
return fmt.Errorf("backup docker data failed, err: %v", err)
}
if err := u.handleUnTar(source+"/docker/docker_data.tar.gz", target); err != nil {
return fmt.Errorf("recover docker data failed, err: %v", err)
}
case "re-recover":
if err := u.handleUnTar(source+"/docker/docker_data.tar.gz", target); err != nil {
return fmt.Errorf("re-recover docker data failed, err: %v", err)
}
case "rollback":
if err := u.handleUnTar(source+"/docker_data.tar.gz", target); err != nil {
return fmt.Errorf("rollback docker data failed, err: %v", err)
}
}
global.LOG.Info("handle docker data dir successful!")
return nil
}
func (u *SnapshotService) handleDockerDatasWithSave(fileOp files.FileOp, operation, source, target string) error {
switch operation {
case "snapshot":
var wg sync.WaitGroup
wg.Add(1)
var status model.SnapshotStatus
go snapAppData(snapHelper{Wg: &wg, Status: &status}, u.OriginalPath)
wg.Wait()
if status.AppData != constant.StatusDone {
return errors.New(status.AppData)
}
case "recover":
if err := u.handleDockerDatasWithSave(fileOp, "snapshot", "", u.OriginalPath); err != nil {
return fmt.Errorf("backup docker data failed, err: %v", err)
}
if _, err := os.Stat(path.Join(source, "docker/docker_image.tar")); err != nil {
global.LOG.Debug("no such docker images in snapshot")
return nil
}
std, err := cmd.Execf("docker load < %s", path.Join(source, "docker/docker_image.tar"))
if err != nil {
return errors.New(std)
}
case "re-recover":
if _, err := os.Stat(path.Join(source, "docker/docker_image.tar")); err != nil {
global.LOG.Debug("no such docker images in snapshot")
return nil
}
std, err := cmd.Execf("docker load < %s", path.Join(source, "docker/docker_image.tar"))
if err != nil {
return errors.New(std)
}
case "rollback":
if _, err := os.Stat(path.Join(source, "docker/docker_image.tar")); err != nil {
global.LOG.Debug("no such docker images in snapshot")
return nil
}
std, err := cmd.Execf("docker load < %s", path.Join(source, "docker_image.tar"))
if err != nil {
return errors.New(std)
}
}
global.LOG.Info("handle docker data successful!")
return nil
}
func (u *SnapshotService) handleDaemonJson(fileOp files.FileOp, operation string, source, target string) error {
daemonJsonPath := "/etc/docker/daemon.json"
if operation == "snapshot" || operation == "recover" {
_, err := os.Stat(daemonJsonPath)
if os.IsNotExist(err) {
global.LOG.Info("no daemon.json in snapshot and system now, nothing happened")
}
if err == nil {
if err := fileOp.CopyFile(daemonJsonPath, target); err != nil {
return fmt.Errorf("backup docker daemon.json failed, err: %v", err)
}
}
}
if operation == "recover" || operation == "rollback" || operation == "re-recover" {
_, sourceErr := os.Stat(source)
if os.IsNotExist(sourceErr) {
_ = os.Remove(daemonJsonPath)
}
if sourceErr == nil {
if err := fileOp.CopyFile(source, "/etc/docker"); err != nil {
return fmt.Errorf("recover docker daemon.json failed, err: %v", err)
}
}
}
global.LOG.Info("handle docker daemon.json successful!")
return nil
}
func (u *SnapshotService) handlePanelBinary(fileOp files.FileOp, operation string, source, target string) error {
panelPath := "/usr/local/bin/1panel"
if operation == "snapshot" || operation == "recover" {
if _, err := os.Stat(panelPath); err != nil {
return fmt.Errorf("1panel binary is not found in %s, err: %v", panelPath, err)
} else {
if err := cpBinary([]string{panelPath}, target); err != nil {
return fmt.Errorf("backup 1panel binary failed, err: %v", err)
}
}
}
if operation == "recover" || operation == "rollback" || operation == "re-recover" {
if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1panel binary is not found in snapshot, err: %v", err)
} else {
if err := cpBinary([]string{source}, "/usr/local/bin/1panel"); err != nil {
return fmt.Errorf("recover 1panel binary failed, err: %v", err)
}
}
}
global.LOG.Info("handle binary panel successful!")
return nil
}
func (u *SnapshotService) handlePanelctlBinary(fileOp files.FileOp, operation string, source, target string) error {
panelctlPath := "/usr/local/bin/1pctl"
if operation == "snapshot" || operation == "recover" {
if _, err := os.Stat(panelctlPath); err != nil {
return fmt.Errorf("1pctl binary is not found in %s, err: %v", panelctlPath, err)
} else {
if err := cpBinary([]string{panelctlPath}, target); err != nil {
return fmt.Errorf("backup 1pctl binary failed, err: %v", err)
}
}
}
if operation == "recover" || operation == "rollback" || operation == "re-recover" {
if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1pctl binary is not found in snapshot, err: %v", err)
} else {
if err := cpBinary([]string{source}, "/usr/local/bin/1pctl"); err != nil {
return fmt.Errorf("recover 1pctl binary failed, err: %v", err)
}
}
}
global.LOG.Info("handle binary 1pactl successful!")
return nil
}
func (u *SnapshotService) handlePanelService(fileOp files.FileOp, operation string, source, target string) error {
panelServicePath := "/etc/systemd/system/1panel.service"
if operation == "snapshot" || operation == "recover" {
if _, err := os.Stat(panelServicePath); err != nil {
return fmt.Errorf("1panel service is not found in %s, err: %v", panelServicePath, err)
} else {
if err := cpBinary([]string{panelServicePath}, target); err != nil {
return fmt.Errorf("backup 1panel service failed, err: %v", err)
}
}
}
if operation == "recover" || operation == "rollback" || operation == "re-recover" {
if _, err := os.Stat(source); err != nil {
return fmt.Errorf("1panel service is not found in snapshot, err: %v", err)
} else {
if err := cpBinary([]string{source}, "/etc/systemd/system/1panel.service"); err != nil {
return fmt.Errorf("recover 1panel service failed, err: %v", err)
}
}
}
global.LOG.Info("handle panel service successful!")
return nil
}
func (u *SnapshotService) handleBackupDatas(fileOp files.FileOp, operation string, source, target string) error {
switch operation {
case "recover":
if err := handleSnapTar(target, u.OriginalPath, "1panel_backup.tar.gz", "./system;"); err != nil {
return fmt.Errorf("restore original local backup dir data failed, err: %v", err)
}
if err := u.handleUnTar(source+"/1panel/1panel_backup.tar.gz", target); err != nil {
return fmt.Errorf("recover local backup dir data failed, err: %v", err)
}
case "re-recover":
if err := u.handleUnTar(source+"/1panel/1panel_backup.tar.gz", target); err != nil {
return fmt.Errorf("retry recover local backup dir data failed, err: %v", err)
}
case "rollback":
if err := u.handleUnTar(source+"/1panel_backup.tar.gz", target); err != nil {
return fmt.Errorf("rollback local backup dir data failed, err: %v", err)
}
}
global.LOG.Info("handle backup data successful!")
return nil
}
func (u *SnapshotService) handlePanelDatas(snapID uint, fileOp files.FileOp, operation string, source, target, backupDir, dockerDir string) error {
switch operation {
case "snapshot":
exclusionRules := "./tmp;./log;./cache;"
if strings.Contains(backupDir, source) {
exclusionRules += ("." + strings.ReplaceAll(backupDir, source, "") + ";")
}
if strings.Contains(dockerDir, source) {
exclusionRules += ("." + strings.ReplaceAll(dockerDir, source, "") + ";")
}
if err := handleSnapTar(source, target, "1panel_data.tar.gz", exclusionRules); err != nil {
return fmt.Errorf("backup panel data failed, err: %v", err)
}
case "recover":
exclusionRules := "./tmp;./log;./cache;./db/1Panel.db-*;"
if strings.Contains(backupDir, target) {
exclusionRules += ("." + strings.ReplaceAll(backupDir, target, "") + ";")
}
if strings.Contains(dockerDir, target) {
exclusionRules += ("." + strings.ReplaceAll(dockerDir, target, "") + ";")
}
_ = snapshotRepo.Update(snapID, map[string]interface{}{"recover_status": ""})
checkPointOfWal()
if err := handleSnapTar(target, u.OriginalPath, "1panel_data.tar.gz", exclusionRules); err != nil {
return fmt.Errorf("restore original panel data failed, err: %v", err)
}
_ = snapshotRepo.Update(snapID, map[string]interface{}{"recover_status": constant.StatusWaiting})
_ = fileOp.Fs.RemoveAll(path.Join(target, "apps"))
checkPointOfWal()
if err := u.handleUnTar(source+"/1panel/1panel_data.tar.gz", target); err != nil {
return fmt.Errorf("recover panel data failed, err: %v", err)
}
case "re-recover":
_ = fileOp.Fs.RemoveAll(path.Join(target, "apps"))
checkPointOfWal()
if err := u.handleUnTar(source+"/1panel/1panel_data.tar.gz", target); err != nil {
return fmt.Errorf("retry recover panel data failed, err: %v", err)
}
case "rollback":
_ = fileOp.Fs.RemoveAll(path.Join(target, "apps"))
checkPointOfWal()
if err := u.handleUnTar(source+"/1panel_data.tar.gz", target); err != nil {
return fmt.Errorf("rollback panel data failed, err: %v", err)
}
}
return nil
}
func (u *SnapshotService) loadDockerDataDir() (string, bool, error) {
client, err := docker.NewDockerClient()
if err != nil {
return "", false, fmt.Errorf("new docker client failed, err: %v", err)
}
info, err := client.Info(context.Background())
if err != nil {
return "", false, fmt.Errorf("load docker info failed, err: %v", err)
}
return info.DockerRootDir, info.LiveRestoreEnabled, nil
}
func (u *SnapshotService) Delete(req dto.BatchDeleteReq) error {
backups, _ := snapshotRepo.GetList(commonRepo.WithIdsIn(req.Ids))
localDir, err := loadLocalDir()
@@ -878,21 +345,22 @@ func (u *SnapshotService) Delete(req dto.BatchDeleteReq) error {
return nil
}
func updateRecoverStatus(id uint, interruptStep, status string, message string) {
if status != constant.StatusSuccess {
global.LOG.Errorf("recover failed, err: %s", message)
func updateRecoverStatus(id uint, isRecover bool, interruptStep, status, message string) {
if isRecover {
if status != constant.StatusSuccess {
global.LOG.Errorf("recover failed, err: %s", message)
}
if err := snapshotRepo.Update(id, map[string]interface{}{
"interrupt_step": interruptStep,
"recover_status": status,
"recover_message": message,
"last_recovered_at": time.Now().Format("2006-01-02 15:04:05"),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
_ = settingRepo.Update("SystemStatus", "Free")
return
}
if err := snapshotRepo.Update(id, map[string]interface{}{
"interrupt_step": interruptStep,
"recover_status": status,
"recover_message": message,
"last_recovered_at": time.Now().Format("2006-01-02 15:04:05"),
}); err != nil {
global.LOG.Errorf("update snap recover status failed, err: %v", err)
}
_ = settingRepo.Update("SystemStatus", "Free")
}
func updateRollbackStatus(id uint, status string, message string) {
_ = settingRepo.Update("SystemStatus", "Free")
if status == constant.StatusSuccess {
if err := snapshotRepo.Update(id, map[string]interface{}{
@@ -926,53 +394,6 @@ func cpBinary(src []string, dst string) error {
return nil
}
func (u *SnapshotService) updateLiveRestore(enabled bool) error {
if _, err := os.Stat(constant.DaemonJsonPath); err != nil {
return fmt.Errorf("load docker daemon.json conf failed, err: %v", err)
}
file, err := os.ReadFile(constant.DaemonJsonPath)
if err != nil {
return err
}
daemonMap := make(map[string]interface{})
_ = json.Unmarshal(file, &daemonMap)
if !enabled {
delete(daemonMap, "live-restore")
} else {
daemonMap["live-restore"] = enabled
}
newJson, err := json.MarshalIndent(daemonMap, "", "\t")
if err != nil {
return err
}
if err := os.WriteFile(constant.DaemonJsonPath, newJson, 0640); err != nil {
return err
}
stdout, err := cmd.Exec("systemctl restart docker")
if err != nil {
return errors.New(stdout)
}
ticker := time.NewTicker(3 * time.Second)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
for range ticker.C {
select {
case <-ctx.Done():
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 live-restore successful!")
return nil
}
}
}
return nil
}
func (u *SnapshotService) handleUnTar(sourceDir, targetDir string) error {
if _, err := os.Stat(targetDir); err != nil && os.IsNotExist(err) {
if err = os.MkdirAll(targetDir, os.ModePerm); err != nil {
@@ -1025,64 +446,32 @@ func rebuildAllAppInstall() error {
return nil
}
func isNewSnapVersion(version string) bool {
versionItem := "v1.5.0"
if version == versionItem {
return true
}
version1s := strings.Split(version, ".")
version2s := strings.Split(versionItem, ".")
n := min(len(version1s), len(version2s))
re := regexp.MustCompile("[0-9]+")
for i := 0; i < n; i++ {
sVersion1s := re.FindAllString(version1s[i], -1)
sVersion2s := re.FindAllString(version2s[i], -1)
if len(sVersion1s) == 0 {
return false
}
if len(sVersion2s) == 0 {
return false
}
v1num, _ := strconv.Atoi(sVersion1s[0])
v2num, _ := strconv.Atoi(sVersion2s[0])
if v1num == v2num {
continue
} else {
return v1num > v2num
}
}
return true
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func checkIsAllDone(snapID uint) bool {
status, err := snapshotRepo.GetStatus(snapID)
if err != nil {
return false
}
isOK, _ := checkAllDone(status)
return isOK
}
func checkAllDone(status model.SnapshotStatus) (bool, string) {
if status.Panel != constant.StatusDone {
return false
return false, status.Panel
}
if status.PanelInfo != constant.StatusDone {
return false
return false, status.PanelInfo
}
if status.DaemonJson != constant.StatusDone {
return false
return false, status.DaemonJson
}
if status.AppData != constant.StatusDone {
return false
return false, status.AppData
}
if status.BackupData != constant.StatusDone {
return false
return false, status.BackupData
}
return true
return true, ""
}
func loadLogByStatus(status model.SnapshotStatus, logPath string) {
@@ -1099,9 +488,28 @@ func loadLogByStatus(status model.SnapshotStatus, logPath string) {
file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
global.LOG.Errorf("write snapshot logs failed, err: %v", err)
return
}
defer file.Close()
_, _ = file.Write([]byte(logs))
}
func hasOs(name string) bool {
return strings.Contains(name, "amd64") ||
strings.Contains(name, "arm64") ||
strings.Contains(name, "armv7") ||
strings.Contains(name, "ppc64le") ||
strings.Contains(name, "s390x")
}
func loadOs() string {
hostInfo, _ := host.Info()
switch hostInfo.KernelArch {
case "x86_64":
return "amd64"
case "armv7l":
return "armv7"
default:
return hostInfo.KernelArch
}
}
+22 -10
View File
@@ -138,8 +138,17 @@ func snapPanelData(snap snapHelper, localDir, targetDir string) {
if strings.Contains(localDir, dataDir) {
exclusionRules += ("." + strings.ReplaceAll(localDir, dataDir, "") + ";")
}
ignoreVal, _ := settingRepo.Get(settingRepo.WithByKey("SnapshotIgnore"))
rules := strings.Split(ignoreVal.Value, ",")
for _, ignore := range rules {
if len(ignore) == 0 || cmd.CheckIllegal(ignore) {
continue
}
exclusionRules += ("." + strings.ReplaceAll(ignore, dataDir, "") + ";")
}
_ = snapshotRepo.Update(snap.SnapID, map[string]interface{}{"status": "OnSaveData"})
sysIP, _ := settingRepo.Get(settingRepo.WithByKey("SystemIP"))
_ = settingRepo.Update("SystemIP", "")
checkPointOfWal()
if err := handleSnapTar(dataDir, targetDir, "1panel_data.tar.gz", exclusionRules); err != nil {
status = err.Error()
@@ -148,13 +157,10 @@ func snapPanelData(snap snapHelper, localDir, targetDir string) {
snap.Status.PanelData = status
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"panel_data": status})
_ = settingRepo.Update("SystemIP", sysIP.Value)
}
func snapCompress(snap snapHelper, rootDir string) {
defer func() {
global.LOG.Debugf("remove snapshot file %s", rootDir)
_ = os.RemoveAll(rootDir)
}()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusRunning})
tmpDir := path.Join(global.CONF.System.TmpDir, "system")
fileName := fmt.Sprintf("%s.tar.gz", path.Base(rootDir))
@@ -175,15 +181,13 @@ func snapCompress(snap snapHelper, rootDir string) {
snap.Status.Compress = constant.StatusDone
snap.Status.Size = size
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"compress": constant.StatusDone, "size": size})
global.LOG.Debugf("remove snapshot file %s", rootDir)
_ = os.RemoveAll(rootDir)
}
func snapUpload(snap snapHelper, accounts string, file string) {
source := path.Join(global.CONF.System.TmpDir, "system", path.Base(file))
defer func() {
global.LOG.Debugf("remove snapshot file %s", source)
_ = os.Remove(source)
}()
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusUploading})
accountMap, err := loadClientMap(accounts)
if err != nil {
@@ -204,6 +208,9 @@ func snapUpload(snap snapHelper, accounts string, file string) {
}
snap.Status.Upload = constant.StatusDone
_ = snapshotRepo.UpdateStatus(snap.Status.ID, map[string]interface{}{"upload": constant.StatusDone})
global.LOG.Debugf("remove snapshot file %s", source)
_ = os.Remove(source)
}
func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error {
@@ -213,6 +220,7 @@ func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error {
}
}
exMap := make(map[string]struct{})
exStr := ""
excludes := strings.Split(exclusionRules, ";")
excludes = append(excludes, "*.sock")
@@ -220,8 +228,12 @@ func handleSnapTar(sourceDir, targetDir, name, exclusionRules string) error {
if len(exclude) == 0 {
continue
}
if _, ok := exMap[exclude]; ok {
continue
}
exStr += " --exclude "
exStr += exclude
exMap[exclude] = struct{}{}
}
commands := fmt.Sprintf("tar --warning=no-file-changed --ignore-failed-read -zcf %s %s -C %s .", targetDir+"/"+name, exStr, sourceDir)
+263
View File
@@ -0,0 +1,263 @@
package service
import (
"context"
"fmt"
"os"
"path"
"strings"
"sync"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/pkg/errors"
)
func (u *SnapshotService) HandleSnapshotRecover(snap model.Snapshot, isRecover bool, req dto.SnapshotRecover) {
_ = global.Cron.Stop()
defer func() {
global.Cron.Start()
}()
snapFileDir := ""
if isRecover {
baseDir := path.Join(global.CONF.System.TmpDir, fmt.Sprintf("system/%s", snap.Name))
if _, err := os.Stat(baseDir); err != nil && os.IsNotExist(err) {
_ = os.MkdirAll(baseDir, os.ModePerm)
}
if req.IsNew || snap.InterruptStep == "Backup" {
if err := backupBeforeRecover(snap); err != nil {
updateRecoverStatus(snap.ID, isRecover, "Backup", constant.StatusFailed, fmt.Sprintf("handle backup before recover failed, err: %v", err))
return
}
global.LOG.Debug("handle backup before recover successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "Download" || req.ReDownload {
if err := handleDownloadSnapshot(snap, baseDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "Backup", constant.StatusFailed, err.Error())
return
}
global.LOG.Debugf("download snapshot file to %s successful!", baseDir)
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "Decompress" {
if err := handleUnTar(fmt.Sprintf("%s/%s.tar.gz", baseDir, snap.Name), baseDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "Decompress", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
global.LOG.Debug("decompress snapshot file successful!", baseDir)
req.IsNew = true
}
snapFileDir = fmt.Sprintf("%s/%s", baseDir, snap.Name)
} else {
snapFileDir = fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
if _, err := os.Stat(snapFileDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "", constant.StatusFailed, fmt.Sprintf("cannot find the backup file %s, please try to recover again.", snapFileDir))
return
}
}
snapJson, err := u.readFromJson(fmt.Sprintf("%s/snapshot.json", snapFileDir))
if err != nil {
updateRecoverStatus(snap.ID, isRecover, "Readjson", constant.StatusFailed, fmt.Sprintf("decompress file failed, err: %v", err))
return
}
if snap.InterruptStep == "Readjson" {
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "AppData" {
if err := recoverAppData(snapFileDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "DockerDir", constant.StatusFailed, fmt.Sprintf("handle recover app data failed, err: %v", err))
return
}
global.LOG.Debug("recover app data from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "DaemonJson" {
fileOp := files.NewFileOp()
if err := recoverDaemonJson(snapFileDir, fileOp); err != nil {
updateRecoverStatus(snap.ID, isRecover, "DaemonJson", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover daemon.json from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PanelBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel"), "/usr/local/bin/1panel"); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelBinary", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1panel binary from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PctlBinary" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1pctl"), "/usr/local/bin/1pctl"); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PctlBinary", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1pctl from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PanelService" {
if err := recoverPanel(path.Join(snapFileDir, "1panel/1panel.service"), "/etc/systemd/system/1panel.service"); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelService", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1panel service from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PanelBackups" {
if err := u.handleUnTar(path.Join(snapFileDir, "/1panel/1panel_backup.tar.gz"), snapJson.BackupDataDir); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelBackups", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1panel backups from snapshot file successful!")
req.IsNew = true
}
if req.IsNew || snap.InterruptStep == "1PanelData" {
checkPointOfWal()
if err := u.handleUnTar(path.Join(snapFileDir, "/1panel/1panel_data.tar.gz"), path.Join(snapJson.BaseDir, "1panel")); err != nil {
updateRecoverStatus(snap.ID, isRecover, "1PanelData", constant.StatusFailed, err.Error())
return
}
global.LOG.Debug("recover 1panel data from snapshot file successful!")
req.IsNew = true
}
_ = rebuildAllAppInstall()
restartCompose(path.Join(snapJson.BaseDir, "1panel/docker/compose"))
global.LOG.Info("recover successful")
if !isRecover {
oriPath := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
global.LOG.Debugf("remove the file %s after the operation is successful", oriPath)
_ = os.RemoveAll(oriPath)
} else {
global.LOG.Debugf("remove the file %s after the operation is successful", path.Dir(snapFileDir))
_ = os.RemoveAll(path.Dir(snapFileDir))
}
_, _ = cmd.Exec("systemctl daemon-reload && systemctl restart 1panel.service")
}
func backupBeforeRecover(snap model.Snapshot) error {
baseDir := fmt.Sprintf("%s/1panel_original/original_%s", global.CONF.System.BaseDir, snap.Name)
var wg sync.WaitGroup
var status model.SnapshotStatus
itemHelper := snapHelper{SnapID: 0, Status: &status, Wg: &wg, FileOp: files.NewFileOp(), Ctx: context.Background()}
jsonItem := SnapshotJson{
BaseDir: global.CONF.System.BaseDir,
BackupDataDir: global.CONF.System.Backup,
PanelDataDir: path.Join(global.CONF.System.BaseDir, "1panel"),
}
_ = os.MkdirAll(path.Join(baseDir, "1panel"), os.ModePerm)
_ = os.MkdirAll(path.Join(baseDir, "docker"), os.ModePerm)
wg.Add(5)
itemHelper.Wg = &wg
go snapJson(itemHelper, jsonItem, baseDir)
go snapPanel(itemHelper, path.Join(baseDir, "1panel"))
go snapDaemonJson(itemHelper, path.Join(baseDir, "docker"))
go snapAppData(itemHelper, path.Join(baseDir, "docker"))
go snapBackup(itemHelper, global.CONF.System.Backup, path.Join(baseDir, "1panel"))
wg.Wait()
allDone, msg := checkAllDone(status)
if !allDone {
return errors.New(msg)
}
snapPanelData(itemHelper, global.CONF.System.BaseDir, path.Join(baseDir, "1panel"))
if status.PanelData != constant.StatusDone {
return errors.New(status.PanelData)
}
return nil
}
func handleDownloadSnapshot(snap model.Snapshot, targetDir string) error {
backup, err := backupRepo.Get(commonRepo.WithByType(snap.DefaultDownload))
if err != nil {
return err
}
client, err := NewIBackupService().NewClient(&backup)
if err != nil {
return err
}
pathItem := backup.BackupPath
if backup.BackupPath != "/" {
pathItem = strings.TrimPrefix(backup.BackupPath, "/")
}
filePath := fmt.Sprintf("%s/%s.tar.gz", targetDir, snap.Name)
_ = os.RemoveAll(filePath)
ok, err := client.Download(path.Join(pathItem, fmt.Sprintf("system_snapshot/%s.tar.gz", snap.Name)), filePath)
if err != nil || !ok {
return fmt.Errorf("download file %s from %s failed, err: %v", snap.Name, backup.Type, err)
}
return nil
}
func recoverAppData(src string) error {
if _, err := os.Stat(path.Join(src, "docker/docker_image.tar")); err != nil {
global.LOG.Debug("no such docker images in snapshot")
return nil
}
std, err := cmd.Execf("docker load < %s", path.Join(src, "docker/docker_image.tar"))
if err != nil {
return errors.New(std)
}
return err
}
func recoverDaemonJson(src string, fileOp files.FileOp) error {
daemonJsonPath := "/etc/docker/daemon.json"
_, errSrc := os.Stat(path.Join(src, "docker/daemon.json"))
_, errPath := os.Stat(daemonJsonPath)
if os.IsNotExist(errSrc) && os.IsNotExist(errPath) {
global.LOG.Debug("the daemon.json file does not exist, nothing happens.")
return nil
}
if errSrc == nil {
if err := fileOp.CopyFile(path.Join(src, "docker/daemon.json"), "/etc/docker"); err != nil {
return fmt.Errorf("recover docker daemon.json failed, err: %v", err)
}
}
_, _ = cmd.Exec("systemctl restart docker")
return nil
}
func recoverPanel(src string, dst string) error {
if _, err := os.Stat(src); err != nil {
return fmt.Errorf("file is not found in %s, err: %v", src, err)
}
global.LOG.Debugf(fmt.Sprintf("\\cp -f %s %s", src, dst))
stdout, err := cmd.Exec(fmt.Sprintf("\\cp -f %s %s", src, dst))
if err != nil {
return fmt.Errorf("cp file failed, stdout: %v, err: %v", stdout, err)
}
return nil
}
func restartCompose(composePath string) {
composes, err := composeRepo.ListRecord()
if err != nil {
return
}
for _, compose := range composes {
pathItem := path.Join(composePath, compose.Name, "docker-compose.yml")
if _, err := os.Stat(pathItem); err != nil {
continue
}
upCmd := fmt.Sprintf("docker-compose -f %s up -d", pathItem)
stdout, err := cmd.Exec(upCmd)
if err != nil {
global.LOG.Debugf("%s failed, err: %v", upCmd, stdout)
}
}
global.LOG.Debug("restart all compose successful!")
}
+5 -1
View File
@@ -177,7 +177,11 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
},
}
if err := NewIFirewallService().UpdatePortRule(ruleItem); err != nil {
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", req.OldValue, req.OldValue, err)
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", req.OldValue, req.NewValue, err)
}
if err = NewIHostService().Update(1, map[string]interface{}{"port": req.NewValue}); err != nil {
global.LOG.Errorf("reset host port %s -> %s failed, err: %v", req.OldValue, req.NewValue, err)
}
}
+24 -20
View File
@@ -7,7 +7,6 @@ import (
"net/http"
"os"
"path"
"runtime"
"strings"
"time"
@@ -133,24 +132,24 @@ func (u *UpgradeService) Upgrade(req dto.Upgrade) error {
if err := cpBinary([]string{tmpDir + "/1panel"}, "/usr/local/bin/1panel"); err != nil {
global.LOG.Errorf("upgrade 1panel failed, err: %v", err)
u.handleRollback(fileOp, originalDir, 1)
u.handleRollback(originalDir, 1)
return
}
if err := cpBinary([]string{tmpDir + "/1pctl"}, "/usr/local/bin/1pctl"); err != nil {
global.LOG.Errorf("upgrade 1pctl failed, err: %v", err)
u.handleRollback(fileOp, originalDir, 2)
u.handleRollback(originalDir, 2)
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("upgrade basedir in 1pctl failed, err: %v", err)
u.handleRollback(fileOp, originalDir, 2)
u.handleRollback(originalDir, 2)
return
}
if err := cpBinary([]string{tmpDir + "/1panel.service"}, "/etc/systemd/system/1panel.service"); err != nil {
global.LOG.Errorf("upgrade 1panel.service failed, err: %v", err)
u.handleRollback(fileOp, originalDir, 3)
u.handleRollback(originalDir, 3)
return
}
@@ -181,7 +180,7 @@ func (u *UpgradeService) handleBackup(fileOp files.FileOp, originalDir string) e
return nil
}
func (u *UpgradeService) handleRollback(fileOp files.FileOp, originalDir string, errStep int) {
func (u *UpgradeService) handleRollback(originalDir string, errStep int) {
dbPath := global.CONF.System.DbPath + "/1Panel.db"
_ = settingRepo.Update("SystemStatus", "Free")
if err := cpBinary([]string{originalDir + "/1Panel.db"}, dbPath); err != nil {
@@ -250,19 +249,24 @@ func (u *UpgradeService) loadReleaseNotes(path string) (string, error) {
}
func loadArch() (string, error) {
switch runtime.GOARCH {
case "amd64", "ppc64le", "s390x", "arm64":
return runtime.GOARCH, nil
case "arm":
std, err := cmd.Exec("uname -m")
if err != nil {
return "", fmt.Errorf("std: %s, err: %s", std, err.Error())
}
if std == "armv7l\n" {
return "armv7", nil
}
return "", fmt.Errorf("unsupported such arch: arm-%s", std)
default:
return "", fmt.Errorf("unsupported such arch: %s", runtime.GOARCH)
std, err := cmd.Exec("uname -a")
if err != nil {
return "", fmt.Errorf("std: %s, err: %s", std, err.Error())
}
if strings.Contains(std, "x86_64") {
return "amd64", nil
}
if strings.Contains(std, "arm64") || strings.Contains(std, "aarch64") {
return "arm64", nil
}
if strings.Contains(std, "armv7l") {
return "armv7", nil
}
if strings.Contains(std, "ppc64le") {
return "ppc64le", nil
}
if strings.Contains(std, "s390x") {
return "s390x", nil
}
return "", fmt.Errorf("unsupported such arch: %s", std)
}
+10 -1
View File
@@ -119,7 +119,16 @@ func (w WebsiteService) PageWebsite(req request.WebsiteSearch) (int64, []respons
}
opts = append(opts, commonRepo.WithOrderRuleBy(req.OrderBy, req.Order))
if req.Name != "" {
opts = append(opts, websiteRepo.WithDomainLike(req.Name))
domains, _ := websiteDomainRepo.GetBy(websiteDomainRepo.WithDomainLike(req.Name))
if len(domains) > 0 {
var websiteIds []uint
for _, domain := range domains {
websiteIds = append(websiteIds, domain.WebsiteID)
}
opts = append(opts, websiteRepo.WithIDs(websiteIds))
} else {
opts = append(opts, websiteRepo.WithDomainLike(req.Name))
}
}
if req.WebsiteGroupID != 0 {
opts = append(opts, websiteRepo.WithGroupID(req.WebsiteGroupID))
+1 -1
View File
@@ -1,13 +1,13 @@
package service
import (
"encoding/json"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"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/model"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/constant"
"gopkg.in/square/go-jose.v2/json"
)
type WebsiteDnsAccountService struct {
+10 -9
View File
@@ -2,13 +2,6 @@ package service
import (
"fmt"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/components"
"gopkg.in/yaml.v3"
"log"
"os"
"path"
@@ -18,6 +11,14 @@ import (
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/buserr"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/i18n"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/common"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/components"
"gopkg.in/yaml.v3"
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
"github.com/1Panel-dev/1Panel/backend/app/dto"
@@ -114,7 +115,7 @@ func createIndexFile(website *model.Website, runtime *model.Runtime) error {
return nil
}
func createProxyFile(website *model.Website, runtime *model.Runtime) error {
func createProxyFile(website *model.Website) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
@@ -191,7 +192,7 @@ func createWebsiteFolder(nginxInstall model.AppInstall, website *model.Website,
}
}
if website.Type == constant.Proxy {
if err := createProxyFile(website, runtime); err != nil {
if err := createProxyFile(website); err != nil {
return err
}
}
+1
View File
@@ -13,6 +13,7 @@ const (
Syncing = "Syncing"
SyncSuccess = "SyncSuccess"
Paused = "Paused"
SyncErr = "SyncErr"
ContainerPrefix = "1Panel-"
+1
View File
@@ -62,6 +62,7 @@ var (
ErrAppNameExist = "ErrAppNameExist"
ErrFileNotFound = "ErrFileNotFound"
ErrFileParseApp = "ErrFileParseApp"
ErrAppParamKey = "ErrAppParamKey"
)
// website
+7
View File
@@ -60,6 +60,7 @@ ErrAppNameExist: 'App name is already exist'
AppStoreIsSyncing: 'The App Store is syncing, please try again later'
ErrGetCompose: "Failed to obtain docker-compose.yml file! {{ .detail }}"
ErrAppWarn: "Abnormal status, please check the log"
ErrAppParamKey: "Parameter {{ .name }} field exception"
#file
ErrFileCanNotRead: "File can not read"
@@ -165,3 +166,9 @@ CutWebsiteLogSuccess: "{{ .name }} website log cut successfully, backup path {{
#toolbox
ErrNotExistUser: "The current user does not exist. Please modify and retry!"
ErrBanAction: "Setting failed, the current {{ .name }} service is unavailable, please check and try again!"
#waf
ErrScope: "Modification of this configuration is not supported"
ErrStateChange: "State modification failed"
ErrRuleExist: "Rule is Exist"
ErrRuleNotExist: "Rule is not Exist"
+7
View File
@@ -60,6 +60,7 @@ ErrAppNameExist: '應用名稱已存在'
AppStoreIsSyncing: '應用程式商店正在同步中,請稍後再試'
ErrGetCompose: "docker-compose.yml 檔案取得失敗!{{ .detail }}"
ErrAppWarn: "狀態異常,請查看日誌"
ErrAppParamKey: "參數 {{ .name }} 欄位異常"
#file
ErrFileCanNotRead: "此文件不支持預覽"
@@ -166,3 +167,9 @@ CutWebsiteLogSuccess: "{{ .name }} 網站日誌切割成功,備份路徑 {{ .p
#toolbox
ErrNotExistUser: "當前使用者不存在,請修改後重試!"
ErrBanAction: "設置失敗,當前 {{ .name }} 服務不可用,請檢查後重試!"
#waf
ErrScope: "不支援修改此配置"
ErrStateChange: "狀態修改失敗"
ErrRuleExist: "規則名稱已存在"
ErrRuleNotExist: "規則不存在"
+8 -1
View File
@@ -60,6 +60,7 @@ ErrAppNameExist: '应用名称已存在'
AppStoreIsSyncing: '应用商店正在同步中,请稍后再试'
ErrGetCompose: "docker-compose.yml 文件获取失败!{{ .detail }}"
ErrAppWarn: "状态异常,请查看日志"
ErrAppParamKey: "参数 {{ .name }} 字段异常"
#file
ErrFileCanNotRead: "此文件不支持预览"
@@ -164,4 +165,10 @@ CutWebsiteLogSuccess: "{{ .name }} 网站日志切割成功,备份路径 {{ .p
#toolbox
ErrNotExistUser: "当前用户不存在,请修改后重试!"
ErrBanAction: "设置失败,当前 {{ .name }} 服务不可用,请检查后重试!"
ErrBanAction: "设置失败,当前 {{ .name }} 服务不可用,请检查后重试!"
#waf
ErrScope: "不支持修改此配置"
ErrStateChange: "状态修改失败"
ErrRuleExist: "规则名称已存在"
ErrRuleNotExist: "规则不存在"
+2 -4
View File
@@ -6,12 +6,10 @@ import (
"os"
"time"
"gorm.io/gorm/logger"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"github.com/1Panel-dev/1Panel/backend/global"
"gorm.io/gorm/logger"
)
func Init() {
+3
View File
@@ -72,6 +72,9 @@ func Init() {
migrations.UpdateSnapshotRecords,
migrations.UpdateWebDavConf,
migrations.AddSnapshotIgnore,
migrations.AddDatabaseIsDelete,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)
@@ -0,0 +1,27 @@
package migrations
import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
var AddSnapshotIgnore = &gormigrate.Migration{
ID: "20240311-add-snapshot-ignore",
Migrate: func(tx *gorm.DB) error {
if err := tx.Create(&model.Setting{Key: "SnapshotIgnore", Value: "*.sock"}).Error; err != nil {
return err
}
return nil
},
}
var AddDatabaseIsDelete = &gormigrate.Migration{
ID: "20240314-add-database-is-delete",
Migrate: func(tx *gorm.DB) error {
if err := tx.AutoMigrate(&model.DatabaseMysql{}, &model.DatabasePostgresql{}); err != nil {
return err
}
return nil
},
}
@@ -124,6 +124,12 @@ var AddTablePHPExtensions = &gormigrate.Migration{
if err := tx.Create(&model.PHPExtensions{Name: "默认", Extensions: "bcmath,gd,gettext,intl,pcntl,shmop,soap,sockets,sysvsem,xmlrpc,zip"}).Error; err != nil {
return err
}
if err := tx.Create(&model.PHPExtensions{Name: "苹果CMS-V10", Extensions: "mysqli,pdo_mysql,zip,gd,redis,memcache,memcached"}).Error; err != nil {
return err
}
if err := tx.Create(&model.PHPExtensions{Name: "SeaCMS", Extensions: "mysqli,pdo_mysql,gd,curl"}).Error; err != nil {
return err
}
return nil
},
}
+1
View File
@@ -16,6 +16,7 @@ func (s *DashboardRouter) InitRouter(Router *gin.RouterGroup) {
Use(middleware.PasswordExpired())
baseApi := v1.ApiGroupApp.BaseApi
{
cmdRouter.GET("/base/os", baseApi.LoadDashboardOsInfo)
cmdRouter.GET("/base/:ioOption/:netOption", baseApi.LoadDashboardBaseInfo)
cmdRouter.GET("/current/:ioOption/:netOption", baseApi.LoadDashboardCurrentInfo)
cmdRouter.POST("/system/restart/:operation", baseApi.SystemRestart)
+1
View File
@@ -20,6 +20,7 @@ func (r *RuntimeRouter) InitRouter(Router *gin.RouterGroup) {
groupRouter.POST("/del", baseApi.DeleteRuntime)
groupRouter.POST("/update", baseApi.UpdateRuntime)
groupRouter.GET("/:id", baseApi.GetRuntime)
groupRouter.POST("/sync", baseApi.SyncStatus)
groupRouter.POST("/node/package", baseApi.GetNodePackageRunScript)
groupRouter.POST("/operate", baseApi.OperateRuntime)
+18 -8
View File
@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
@@ -14,6 +15,7 @@ import (
"path"
"strconv"
"strings"
"time"
"github.com/1Panel-dev/1Panel/backend/utils/files"
odsdk "github.com/goh-chunlin/go-onedrive/onedrive"
@@ -116,7 +118,7 @@ func (o oneDriveClient) Upload(src, target string) (bool, error) {
}
var isOk bool
if fileInfo.Size() < 4*1024*1024 {
isOk, err = o.upSmall(ctx, src, folderID, fileInfo.Size())
isOk, err = o.upSmall(src, folderID, fileInfo.Size())
} else {
isOk, err = o.upBig(ctx, src, folderID, fileInfo.Size())
}
@@ -300,7 +302,7 @@ func (o *oneDriveClient) NewSessionFileUploadRequest(absoluteUrl string, grandOf
return req, err
}
func (o *oneDriveClient) upSmall(ctx context.Context, srcPath, folderID string, fileSize int64) (bool, error) {
func (o *oneDriveClient) upSmall(srcPath, folderID string, fileSize int64) (bool, error) {
file, err := os.Open(srcPath)
if err != nil {
return false, err
@@ -321,7 +323,6 @@ func (o *oneDriveClient) upSmall(ctx context.Context, srcPath, folderID string,
if err := o.client.Do(context.Background(), req, false, &response); err != nil {
return false, fmt.Errorf("do request for list failed, err: %v", err)
}
fmt.Println(response)
return true, nil
}
@@ -362,7 +363,13 @@ func (o *oneDriveClient) upBig(ctx context.Context, srcPath, folderID string, fi
splitCount += 1
}
bfReader := bufio.NewReader(file)
var fileUploadResp *UploadSessionUploadResponse
httpClient := http.Client{
Timeout: time.Minute * 10,
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
for splitNow := int64(0); splitNow < splitCount; splitNow++ {
length, err := bfReader.Read(buffer)
if err != nil {
@@ -376,12 +383,15 @@ func (o *oneDriveClient) upBig(ctx context.Context, srcPath, folderID string, fi
if err != nil {
return false, err
}
if err := o.client.Do(ctx, sessionFileUploadReq, false, &fileUploadResp); err != nil {
res, err := httpClient.Do(sessionFileUploadReq)
if err != nil {
return false, err
}
}
if fileUploadResp.Id == "" {
return false, errors.New("something went wrong. file upload incomplete. consider upload the file in a step-by-step manner")
if res.StatusCode != 201 && res.StatusCode != 202 && res.StatusCode != 200 {
data, _ := io.ReadAll(res.Body)
res.Body.Close()
return false, errors.New(string(data))
}
}
return true, nil
}
+53 -40
View File
@@ -2,7 +2,6 @@ package cmd
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
@@ -14,20 +13,7 @@ import (
)
func Exec(cmdStr string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
cmd := exec.Command("bash", "-c", cmdStr)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
return "", buserr.New(constant.ErrCmdTimeout)
}
if err != nil {
return handleErr(stdout, stderr, err)
}
return stdout.String(), nil
return ExecWithTimeOut(cmdStr, 20*time.Second)
}
func handleErr(stdout, stderr bytes.Buffer, err error) (string, error) {
@@ -46,19 +32,28 @@ func handleErr(stdout, stderr bytes.Buffer, err error) (string, error) {
}
func ExecWithTimeOut(cmdStr string, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
cmd := exec.Command("bash", "-c", cmdStr)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
if err := cmd.Start(); err != nil {
return "", err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
after := time.After(timeout)
select {
case <-after:
_ = cmd.Process.Kill()
return "", buserr.New(constant.ErrCmdTimeout)
case err := <-done:
if err != nil {
return handleErr(stdout, stderr, err)
}
}
if err != nil {
return handleErr(stdout, stderr, err)
}
return stdout.String(), nil
}
@@ -75,9 +70,6 @@ func ExecContainerScript(containerName, cmdStr string, timeout time.Duration) er
}
func ExecCronjobWithTimeOut(cmdStr, workdir, outPath string, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
file, err := os.OpenFile(outPath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
return err
@@ -88,13 +80,24 @@ func ExecCronjobWithTimeOut(cmdStr, workdir, outPath string, timeout time.Durati
cmd.Dir = workdir
cmd.Stdout = file
cmd.Stderr = file
err = cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
return buserr.New(constant.ErrCmdTimeout)
if err := cmd.Start(); err != nil {
return err
}
return err
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
after := time.After(timeout)
select {
case <-after:
_ = cmd.Process.Kill()
return buserr.New(constant.ErrCmdTimeout)
case err := <-done:
if err != nil {
return err
}
}
return nil
}
func Execf(cmdStr string, a ...interface{}) (string, error) {
@@ -122,20 +125,29 @@ func ExecWithCheck(name string, a ...string) (string, error) {
}
func ExecScript(scriptPath, workDir string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
cmd := exec.Command("bash", scriptPath)
cmd.Dir = workDir
var stdout, stderr bytes.Buffer
cmd.Dir = workDir
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if ctx.Err() == context.DeadlineExceeded {
if err := cmd.Start(); err != nil {
return "", err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
after := time.After(10 * time.Minute)
select {
case <-after:
_ = cmd.Process.Kill()
return "", buserr.New(constant.ErrCmdTimeout)
case err := <-done:
if err != nil {
return handleErr(stdout, stderr, err)
}
}
if err != nil {
return handleErr(stdout, stderr, err)
}
return stdout.String(), nil
}
@@ -165,7 +177,8 @@ func CheckIllegal(args ...string) bool {
for _, arg := range args {
if strings.Contains(arg, "&") || strings.Contains(arg, "|") || strings.Contains(arg, ";") ||
strings.Contains(arg, "$") || strings.Contains(arg, "'") || strings.Contains(arg, "`") ||
strings.Contains(arg, "(") || strings.Contains(arg, ")") || strings.Contains(arg, "\"") {
strings.Contains(arg, "(") || strings.Contains(arg, ")") || strings.Contains(arg, "\"") ||
strings.Contains(arg, "\n") || strings.Contains(arg, "\r") {
return true
}
}
+15
View File
@@ -126,6 +126,21 @@ func (f FileOp) SaveFile(dst string, content string, mode fs.FileMode) error {
return nil
}
func (f FileOp) SaveFileWithByte(dst string, content []byte, mode fs.FileMode) error {
if !f.Stat(path.Dir(dst)) {
_ = f.CreateDir(path.Dir(dst), mode.Perm())
}
file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return err
}
defer file.Close()
write := bufio.NewWriter(file)
_, _ = write.Write(content)
write.Flush()
return nil
}
func (f FileOp) ChownR(dst string, uid string, gid string, sub bool) error {
cmdStr := fmt.Sprintf(`chown %s:%s "%s"`, uid, gid, dst)
if sub {
+8 -7
View File
@@ -3,6 +3,7 @@ package files
import (
"bufio"
"github.com/spf13/afero"
"io"
"net/http"
"os"
"os/user"
@@ -87,16 +88,20 @@ func ReadFileByLine(filename string, page, pageSize int) ([]string, bool, error)
}
defer file.Close()
scanner := bufio.NewScanner(file)
reader := bufio.NewReaderSize(file, 8192)
var lines []string
currentLine := 0
startLine := (page - 1) * pageSize
endLine := startLine + pageSize
for scanner.Scan() {
for {
line, _, err := reader.ReadLine()
if err == io.EOF {
break
}
if currentLine >= startLine && currentLine < endLine {
lines = append(lines, scanner.Text())
lines = append(lines, string(line))
}
currentLine++
if currentLine >= endLine {
@@ -104,10 +109,6 @@ func ReadFileByLine(filename string, page, pageSize int) ([]string, bool, error)
}
}
if err := scanner.Err(); err != nil {
return nil, false, err
}
isEndOfFile := currentLine < endLine
return lines, isEndOfFile, nil
+4
View File
@@ -70,6 +70,8 @@ type AccessChangeInfo struct {
type BackupInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Version string `json:"version"`
Format string `json:"format"`
TargetDir string `json:"targetDir"`
FileName string `json:"fileName"`
@@ -79,6 +81,8 @@ type BackupInfo struct {
type RecoverInfo struct {
Name string `json:"name"`
Type string `json:"type"`
Version string `json:"version"`
Format string `json:"format"`
SourceFile string `json:"sourceFile"`
+90 -37
View File
@@ -1,11 +1,14 @@
package client
import (
"compress/gzip"
"context"
"database/sql"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
"time"
@@ -13,7 +16,8 @@ import (
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/mysql/helper"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
)
type Remote struct {
@@ -229,56 +233,63 @@ func (r *Remote) Backup(info BackupInfo) error {
return fmt.Errorf("mkdir %s failed, err: %v", info.TargetDir, err)
}
}
fileNameItem := info.TargetDir + "/" + strings.TrimSuffix(info.FileName, ".gz")
tlsItem, err := ConnWithSSL(r.SSL, r.SkipVerify, r.ClientKey, r.ClientCert, r.RootCert)
outfile, _ := os.OpenFile(path.Join(info.TargetDir, info.FileName), os.O_RDWR|os.O_CREATE, 0755)
global.LOG.Infof("start to mysqldump | gzip > %s.gzip", info.TargetDir+"/"+info.FileName)
image, err := loadImage(info.Type, info.Version)
if err != nil {
return err
}
dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai%s", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F", tlsItem)
backupCmd := fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'mysqldump -h %s -P %d -u%s -p%s %s --default-character-set=%s %s'",
image, r.Address, r.Port, r.User, r.Password, sslSkip(info.Version), info.Format, info.Name)
f, _ := os.OpenFile(fileNameItem, os.O_RDWR|os.O_CREATE, 0755)
defer f.Close()
if err := helper.Dump(dns, helper.WithData(), helper.WithDropTable(), helper.WithWriter(f)); err != nil {
return err
}
global.LOG.Debug(backupCmd)
cmd := exec.Command("bash", "-c", backupCmd)
gzipCmd := exec.Command("gzip", fileNameItem)
stdout, err := gzipCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("gzip file %s failed, stdout: %v, err: %v", strings.TrimSuffix(info.FileName, ".gz"), string(stdout), err)
}
gzipCmd := exec.Command("gzip", "-cf")
gzipCmd.Stdin, _ = cmd.StdoutPipe()
gzipCmd.Stdout = outfile
_ = gzipCmd.Start()
_ = cmd.Run()
_ = gzipCmd.Wait()
return nil
}
func (r *Remote) Recover(info RecoverInfo) error {
fileName := info.SourceFile
if strings.HasSuffix(info.SourceFile, ".sql.gz") {
fileName = strings.TrimSuffix(info.SourceFile, ".gz")
gzipCmd := exec.Command("gunzip", info.SourceFile)
stdout, err := gzipCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("gunzip file %s failed, stdout: %v, err: %v", info.SourceFile, string(stdout), err)
}
defer func() {
gzipCmd := exec.Command("gzip", fileName)
_, _ = gzipCmd.CombinedOutput()
}()
}
tlsItem, err := ConnWithSSL(r.SSL, r.SkipVerify, r.ClientKey, r.ClientCert, r.RootCert)
if err != nil {
return err
}
dns := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=%s&parseTime=true&loc=Asia%sShanghai%s", r.User, r.Password, r.Address, r.Port, info.Name, info.Format, "%2F", tlsItem)
fi, _ := os.Open(info.SourceFile)
defer fi.Close()
f, err := os.Open(fileName)
image, err := loadImage(info.Type, info.Version)
if err != nil {
return err
}
defer f.Close()
if err := helper.Source(dns, f, helper.WithMergeInsert(1000)); err != nil {
return err
recoverCmd := fmt.Sprintf("docker run --rm --net=host -i %s /bin/bash -c 'mysql -h %s -P %d -u%s -p%s %s --default-character-set=%s %s'",
image, r.Address, r.Port, r.User, r.Password, sslSkip(info.Version), info.Format, info.Name)
global.LOG.Debug(recoverCmd)
cmd := exec.Command("bash", "-c", recoverCmd)
if strings.HasSuffix(info.SourceFile, ".gz") {
gzipFile, err := os.Open(info.SourceFile)
if err != nil {
return err
}
defer gzipFile.Close()
gzipReader, err := gzip.NewReader(gzipFile)
if err != nil {
return err
}
defer gzipReader.Close()
cmd.Stdin = gzipReader
} else {
cmd.Stdin = fi
}
stdout, err := cmd.CombinedOutput()
stdStr := strings.ReplaceAll(string(stdout), "mysql: [Warning] Using a password on the command line interface can be insecure.\n", "")
if err != nil || strings.HasPrefix(string(stdStr), "ERROR ") {
return errors.New(stdStr)
}
return nil
}
@@ -389,3 +400,45 @@ func (r *Remote) ExecSQLForHosts(timeout uint) ([]string, error) {
}
return rows, nil
}
func loadImage(dbType, version string) (string, error) {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
fmt.Println(err)
}
images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
if err != nil {
fmt.Println(err)
}
for _, image := range images {
for _, tag := range image.RepoTags {
if !strings.HasPrefix(tag, dbType+":") {
continue
}
if dbType == "mariadb" && strings.HasPrefix(tag, "mariadb:") {
return tag, nil
}
if strings.HasPrefix(version, "5.6") && strings.HasPrefix(tag, "mysql:5.6") {
return tag, nil
}
if strings.HasPrefix(version, "5.7") && strings.HasPrefix(tag, "mysql:5.7") {
return tag, nil
}
if strings.HasPrefix(version, "8.") && strings.HasPrefix(tag, "mysql:8.") {
return tag, nil
}
}
}
if dbType == "mariadb" || version == "8.x" {
return "mysql:8.2.0", nil
}
return "mysql:" + version, nil
}
func sslSkip(version string) string {
if strings.HasPrefix(version, "5.6") || strings.HasPrefix(version, "5.7") {
return "--skip-ssl"
}
return "--ssl-mode=DISABLED"
}
+2 -2
View File
@@ -61,7 +61,7 @@ func NewParserFromLexer(lexer *lexer) *Parser {
parser.directiveWrappers = map[string]func(*components.Directive) components.IDirective{
"server": func(directive *components.Directive) components.IDirective {
return parser.parseUpstreamServer(directive)
return parser.wrapServer(directive)
},
}
@@ -113,7 +113,7 @@ parsingloop:
case p.curTokenIs(flag.LuaCode):
context.IsLuaBlock = true
context.LiteralCode = p.currentToken.Literal
case p.curTokenIs(flag.Keyword):
case p.curTokenIs(flag.Keyword) || p.curTokenIs(flag.QuotedString):
s, err := p.parseStatement()
if err != nil {
return nil, err
+48 -1
View File
@@ -3830,6 +3830,31 @@ const docTemplate = `{
}
}
},
"/dashboard/base/os": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取服务器基础数据",
"consumes": [
"application/json"
],
"tags": [
"Dashboard"
],
"summary": "Load os info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.OsInfo"
}
}
}
}
},
"/dashboard/current/:ioOption/:netOption": {
"get": {
"security": [
@@ -16828,6 +16853,26 @@ const docTemplate = `{
}
}
},
"dto.OsInfo": {
"type": "object",
"properties": {
"kernelArch": {
"type": "string"
},
"kernelVersion": {
"type": "string"
},
"os": {
"type": "string"
},
"platform": {
"type": "string"
},
"platformFamily": {
"type": "string"
}
}
},
"dto.PageContainer": {
"type": "object",
"required": [
@@ -17773,6 +17818,9 @@ const docTemplate = `{
"sessionTimeout": {
"type": "string"
},
"snapshotIgnore": {
"type": "string"
},
"ssl": {
"type": "string"
},
@@ -18967,7 +19015,6 @@ const docTemplate = `{
"request.FileEdit": {
"type": "object",
"required": [
"content",
"path"
],
"properties": {
+48 -1
View File
@@ -3823,6 +3823,31 @@
}
}
},
"/dashboard/base/os": {
"get": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "获取服务器基础数据",
"consumes": [
"application/json"
],
"tags": [
"Dashboard"
],
"summary": "Load os info",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/dto.OsInfo"
}
}
}
}
},
"/dashboard/current/:ioOption/:netOption": {
"get": {
"security": [
@@ -16821,6 +16846,26 @@
}
}
},
"dto.OsInfo": {
"type": "object",
"properties": {
"kernelArch": {
"type": "string"
},
"kernelVersion": {
"type": "string"
},
"os": {
"type": "string"
},
"platform": {
"type": "string"
},
"platformFamily": {
"type": "string"
}
}
},
"dto.PageContainer": {
"type": "object",
"required": [
@@ -17766,6 +17811,9 @@
"sessionTimeout": {
"type": "string"
},
"snapshotIgnore": {
"type": "string"
},
"ssl": {
"type": "string"
},
@@ -18960,7 +19008,6 @@
"request.FileEdit": {
"type": "object",
"required": [
"content",
"path"
],
"properties": {
+30 -1
View File
@@ -1887,6 +1887,19 @@ definitions:
option:
type: string
type: object
dto.OsInfo:
properties:
kernelArch:
type: string
kernelVersion:
type: string
os:
type: string
platform:
type: string
platformFamily:
type: string
type: object
dto.PageContainer:
properties:
excludeAppStore:
@@ -2525,6 +2538,8 @@ definitions:
type: string
sessionTimeout:
type: string
snapshotIgnore:
type: string
ssl:
type: string
sslType:
@@ -3319,7 +3334,6 @@ definitions:
path:
type: string
required:
- content
- path
type: object
request.FileMove:
@@ -7412,6 +7426,21 @@ paths:
summary: Load dashboard base info
tags:
- Dashboard
/dashboard/base/os:
get:
consumes:
- application/json
description: 获取服务器基础数据
responses:
"200":
description: OK
schema:
$ref: '#/definitions/dto.OsInfo'
security:
- ApiKeyAuth: []
summary: Load os info
tags:
- Dashboard
/dashboard/current/:ioOption/:netOption:
get:
consumes:
+2
View File
@@ -6,6 +6,8 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
pnpm-lock.yaml
package-lock.json
node_modules
dist
-11334
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -30,7 +30,7 @@
"echarts": "^5.3.0",
"echarts-liquidfill": "^3.1.0",
"element-plus": "^2.3.4",
"fit2cloud-ui-plus": "^1.0.9",
"fit2cloud-ui-plus": "^1.1.4",
"js-base64": "^3.7.2",
"js-md5": "^0.7.3",
"md-editor-v3": "^2.7.2",
@@ -46,7 +46,7 @@
"vue": "^3.2.25",
"vue-clipboard3": "^2.0.0",
"vue-codemirror": "^6.1.1",
"vue-i18n": "^9.1.9",
"vue-i18n": "^9.10.2",
"vue-router": "^4.0.12",
"vue3-seamless-scroll": "^1.2.0",
"xterm": "^4.19.0",
@@ -77,7 +77,7 @@
"sass": "^1.49.7",
"standard-version": "^9.5.0",
"stylelint": "^15.10.1",
"tailwindcss": "^3.3.2",
"tailwindcss": "^3.4.1",
"typescript": "^4.5.4",
"unplugin-auto-import": "^0.16.4",
"unplugin-vue-components": "^0.25.0",
+7
View File
@@ -1,4 +1,11 @@
export namespace Dashboard {
export interface OsInfo {
os: string;
platform: string;
platformFamily: string;
kernelArch: string;
kernelVersion: string;
}
export interface BaseInfo {
websiteNumber: number;
databaseNumber: number;
+2
View File
@@ -23,6 +23,7 @@ export namespace Database {
username: string;
password: string;
permission: string;
isDelete: string;
description: string;
}
export interface BaseInfo {
@@ -207,6 +208,7 @@ export namespace Database {
username: string;
password: string;
superUser: boolean;
isDelete: string;
description: string;
}
export interface ChangeInfo {
+1
View File
@@ -45,6 +45,7 @@ export namespace Setting {
emailVars: string;
weChatVars: string;
dingVars: string;
snapshotIgnore: string;
}
export interface SettingUpdate {
key: string;
+4
View File
@@ -1,6 +1,10 @@
import http from '@/api';
import { Dashboard } from '../interface/dashboard';
export const loadOsInfo = () => {
return http.get<Dashboard.OsInfo>(`/dashboard/base/os`);
};
export const loadBaseInfo = (ioOption: string, netOption: string) => {
return http.get<Dashboard.BaseInfo>(`/dashboard/base/${ioOption}/${netOption}`);
};
+4
View File
@@ -58,3 +58,7 @@ export const UpdatePHPExtensions = (req: Runtime.PHPExtensionsUpdate) => {
export const DeletePHPExtensions = (req: Runtime.PHPExtensionsDelete) => {
return http.post<any>(`/runtimes/php/extensions/del`, req);
};
export const SyncRuntime = () => {
return http.post(`/runtimes/sync`, {});
};
+36
View File
@@ -0,0 +1,36 @@
{
"Anhui": "安徽",
"Beijing": "北京",
"Fujian": "福建",
"Gansu": "甘肃",
"Guangdong": "广东",
"Guangxi": "广西",
"Guizhou": "贵州",
"Hainan": "海南",
"Hebei": "河北",
"Henan": "河南",
"Heilongjiang": "黑龙江",
"Hubei": "湖北",
"Hunan": "湖南",
"Jilin": "吉林",
"Jiangsu": "江苏",
"Jiangxi": "江西",
"Liaoning": "辽宁",
"Inner Mongolia": "内蒙古",
"Ningxia": "宁夏",
"Qinghai": "青海",
"Shandong": "山东",
"Shanxi": "山西",
"Shaanxi": "陕西",
"Shanghai": "上海",
"Sichuan": "四川",
"Tianjin": "天津",
"Tibet": "西藏",
"Xinjiang": "新疆",
"Yunnan": "云南",
"Zhejiang": "浙江",
"Chongqing": "重庆",
"HongKong": "香港",
"Macao": "澳门",
"Taiwan": "台湾"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+194
View File
@@ -0,0 +1,194 @@
{
"Afghanistan": "阿富汗",
"Albania": "阿尔巴尼亚",
"Algeria": "阿尔及利亚",
"Angola": "安哥拉",
"Argentina": "阿根廷",
"Armenia": "亚美尼亚",
"Australia": "澳大利亚",
"Austria": "奥地利",
"Azerbaijan": "阿塞拜疆",
"Bahamas": "巴哈马",
"Bahrain": "巴林",
"Bangladesh": "孟加拉国",
"Belarus": "白俄罗斯",
"Belgium": "比利时",
"Belize": "伯利兹",
"Benin": "贝宁",
"Bhutan": "不丹",
"Bolivia": "玻利维亚",
"Bosnia and Herz.": "波斯尼亚和黑塞哥维那",
"Botswana": "博茨瓦纳",
"Brazil": "巴西",
"British Virgin Islands": "英属维京群岛",
"Brunei": "文莱",
"Bulgaria": "保加利亚",
"Burkina Faso": "布基纳法索",
"Burundi": "布隆迪",
"Cambodia": "柬埔寨",
"Cameroon": "喀麦隆",
"Canada": "加拿大",
"Cape Verde": "佛得角",
"Cayman Islands": "开曼群岛",
"Central African Rep.": "中非共和国",
"Chad": "乍得",
"Chile": "智利",
"China": "中国",
"Colombia": "哥伦比亚",
"Comoros": "科摩罗",
"Congo": "刚果",
"Costa Rica": "哥斯达黎加",
"Croatia": "克罗地亚",
"Cuba": "古巴",
"Cyprus": "塞浦路斯",
"Czech Rep.": "捷克共和国",
"Côte d'Ivoire": "科特迪瓦",
"Dem. Rep. Congo": "刚果民主共和国",
"Dem. Rep. Korea": "朝鲜",
"Denmark": "丹麦",
"Djibouti": "吉布提",
"Dominican Rep.": "多米尼加共和国",
"Ecuador": "厄瓜多尔",
"Egypt": "埃及",
"El Salvador": "萨尔瓦多",
"Equatorial Guinea": "赤道几内亚",
"Eritrea": "厄立特里亚",
"Estonia": "爱沙尼亚",
"Ethiopia": "埃塞俄比亚",
"Falkland Is.": "福克兰群岛",
"Fiji": "斐济",
"Finland": "芬兰",
"Fr. S. Antarctic Lands": "所罗门群岛",
"France": "法国",
"Gabon": "加蓬",
"Gambia": "冈比亚",
"Georgia": "格鲁吉亚",
"Germany": "德国",
"Ghana": "加纳",
"Greece": "希腊",
"Greenland": "格陵兰",
"Guatemala": "危地马拉",
"Guinea": "几内亚",
"Guinea-Bissau": "几内亚比绍",
"Guyana": "圭亚那",
"Haiti": "海地",
"Honduras": "洪都拉斯",
"Hungary": "匈牙利",
"Iceland": "冰岛",
"India": "印度",
"Indonesia": "印度尼西亚",
"Iran": "伊朗",
"Iraq": "伊拉克",
"Ireland": "爱尔兰",
"Isle of Man": "马恩岛",
"Israel": "以色列",
"Italy": "意大利",
"Jamaica": "牙买加",
"Japan": "日本",
"Jordan": "约旦",
"Kazakhstan": "哈萨克斯坦",
"Kenya": "肯尼亚",
"Korea": "韩国",
"Kuwait": "科威特",
"Kyrgyzstan": "吉尔吉斯斯坦",
"Lao PDR": "老挝",
"Latvia": "拉脱维亚",
"Lebanon": "黎巴嫩",
"Lesotho": "莱索托",
"Liberia": "利比里亚",
"Libya": "利比亚",
"Lithuania": "立陶宛",
"Luxembourg": "卢森堡",
"Macedonia": "马其顿",
"Madagascar": "马达加斯加",
"Malawi": "马拉维",
"Malaysia": "马来西亚",
"Maldives": "马尔代夫",
"Mali": "马里",
"Malta": "马耳他",
"Mauritania": "毛利塔尼亚",
"Mauritius": "毛里求斯",
"Mexico": "墨西哥",
"Moldova": "摩尔多瓦",
"Monaco": "摩纳哥",
"Mongolia": "蒙古",
"Montenegro": "黑山共和国",
"Morocco": "摩洛哥",
"Mozambique": "莫桑比克",
"Myanmar": "缅甸",
"Namibia": "纳米比亚",
"Nepal": "尼泊尔",
"Netherlands": "荷兰",
"New Caledonia": "新喀里多尼亚",
"New Zealand": "新西兰",
"Nicaragua": "尼加拉瓜",
"Niger": "尼日尔",
"Nigeria": "尼日利亚",
"Norway": "挪威",
"Oman": "阿曼",
"Pakistan": "巴基斯坦",
"Panama": "巴拿马",
"Papua New Guinea": "巴布亚新几内亚",
"Paraguay": "巴拉圭",
"Peru": "秘鲁",
"Philippines": "菲律宾",
"Poland": "波兰",
"Portugal": "葡萄牙",
"Puerto Rico": "波多黎各",
"Qatar": "卡塔尔",
"Reunion": "留尼旺",
"Romania": "罗马尼亚",
"Russia": "俄罗斯",
"Rwanda": "卢旺达",
"S. Geo. and S. Sandw. Is.": "南乔治亚和南桑威奇群岛",
"S. Sudan": "南苏丹",
"San Marino": "圣马力诺",
"Saudi Arabia": "沙特阿拉伯",
"Senegal": "塞内加尔",
"Serbia": "塞尔维亚",
"Sierra Leone": "塞拉利昂",
"Singapore": "新加坡",
"Slovakia": "斯洛伐克",
"Slovenia": "斯洛文尼亚",
"Solomon Is.": "所罗门群岛",
"Somalia": "索马里",
"South Africa": "南非",
"Spain": "西班牙",
"Sri Lanka": "斯里兰卡",
"Sudan": "苏丹",
"Suriname": "苏里南",
"Swaziland": "斯威士兰",
"Sweden": "瑞典",
"Switzerland": "瑞士",
"Syria": "叙利亚",
"Taiwan": "中国台湾",
"Tajikistan": "塔吉克斯坦",
"Tanzania": "坦桑尼亚",
"Thailand": "泰国",
"Togo": "多哥",
"Tonga": "汤加",
"Trinidad and Tobago": "特立尼达和多巴哥",
"Tunisia": "突尼斯",
"Turkey": "土耳其",
"Turkmenistan": "土库曼斯坦",
"U.S. Virgin Islands": "美属维尔京群岛",
"Uganda": "乌干达",
"Ukraine": "乌克兰",
"United Arab Emirates": "阿拉伯联合酋长国",
"United Kingdom": "英国",
"United States": "美国",
"Uruguay": "乌拉圭",
"Uzbekistan": "乌兹别克斯坦",
"Vanuatu": "瓦努阿图",
"Vatican City": "梵蒂冈城",
"Venezuela": "委内瑞拉",
"Vietnam": "越南",
"W. Sahara": "西撒哈拉",
"Yemen": "也门",
"Yugoslavia": "南斯拉夫",
"Zaire": "扎伊尔",
"Zambia": "赞比亚",
"Zimbabwe": "津巴布韦",
"Hong Kong": "中国香港",
"Macau": "中国澳门"
}
File diff suppressed because it is too large Load Diff
-1
View File
@@ -59,7 +59,6 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import OpDialog from '@/components/del-dialog/index.vue';
import { computeSize, dateFormat, downloadFile } from '@/utils/util';
import { handleBackup, handleRecover } from '@/api/modules/setting';
import i18n from '@/lang';
@@ -0,0 +1,55 @@
<template>
<el-card class="config-card">
<div class="config-header">
<span>{{ header }}</span>
<slot name="header-r" />
</div>
<el-text type="info">{{ description }}</el-text>
<div class="config-content">
<slot name="content-r" />
</div>
<div class="config-form">
<slot name="content-form" />
</div>
</el-card>
</template>
<script setup lang="ts">
defineOptions({ name: 'ConfigCard' });
defineProps({
header: String,
description: String,
});
</script>
<style lang="scss" scoped>
.config-card {
cursor: pointer;
.config-header {
margin-bottom: 18px;
display: flex;
justify-content: space-between;
align-items: center;
span {
font-weight: normal;
font-size: 18px;
}
}
.config-content {
display: flex;
justify-content: flex-end;
}
.config-form {
margin-top: 20px;
}
}
.config-card {
border: var(--panel-border) !important;
&:hover {
cursor: pointer;
border: 1px solid var(--el-color-primary) !important;
}
}
</style>
+15 -2
View File
@@ -8,7 +8,7 @@
popper-class="file-list"
>
<template #reference>
<el-button :icon="Folder" :disabled="disabled" @click="popoverVisible = true"></el-button>
<el-button :icon="Folder" :disabled="disabled" @click="openPage()"></el-button>
</template>
<div>
<el-button class="close" link @click="closePage">
@@ -57,7 +57,7 @@
<template #default="{ row }">
<el-checkbox
v-model="rowName"
:true-label="row.name"
:true-value="row.name"
:disabled="disabledDir(row)"
@change="checkFile(row)"
/>
@@ -116,6 +116,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
isAll: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
@@ -141,7 +145,16 @@ const closePage = () => {
selectRow.value = {};
};
const openPage = () => {
popoverVisible.value = true;
selectRow.value = {};
rowName.value = '';
};
const disabledDir = (row: File.File) => {
if (props.isAll) {
return false;
}
if (!props.dir) {
return row.isDir;
}
+4
View File
@@ -4,6 +4,8 @@ import RouterButton from './router-button/index.vue';
import ComplexTable from './complex-table/index.vue';
import ErrPrompt from './error-prompt/index.vue';
import OpDialog from './del-dialog/index.vue';
import TableSearch from './table-search/index.vue';
import TableSetting from './table-setting/index.vue';
import Tooltip from '@/components/tooltip/index.vue';
import CopyButton from '@/components/copy-button/index.vue';
export default {
@@ -15,5 +17,7 @@ export default {
app.component(OpDialog.name, OpDialog);
app.component(Tooltip.name, Tooltip);
app.component(CopyButton.name, CopyButton);
app.component(TableSearch.name, TableSearch);
app.component(TableSetting.name, TableSetting);
},
};
@@ -162,6 +162,9 @@ const getContent = () => {
if (!end.value && res.data.end) {
lastContent.value = content.value;
}
res.data.content = res.data.content.replace(/\\u(\w{4})/g, function (match, grp) {
return String.fromCharCode(parseInt(grp, 16));
});
data.value = res.data;
if (res.data.content != '') {
if (stopSignals.some((signal) => res.data.content.endsWith(signal))) {
@@ -4,6 +4,7 @@
<el-radio-button
class="router_card_button"
:label="button.label"
:value="button.label"
v-for="(button, index) in buttonArray"
size="large"
:key="index"
+1 -1
View File
@@ -36,7 +36,7 @@ const getType = (status: string) => {
case 'removing':
return 'warning';
default:
return '';
return 'primary';
}
};
@@ -44,10 +44,10 @@
<h2 class="inline-block">{{ $t('app.version') }}</h2>
</div>
<el-radio-group class="inline-block tag" v-model="upgradeVersion" @change="changeOption">
<el-radio v-if="upgradeInfo.newVersion" :label="upgradeInfo.newVersion">
<el-radio v-if="upgradeInfo.newVersion" :value="upgradeInfo.newVersion">
{{ upgradeInfo.newVersion }} {{ $t('setting.newVersion') }}
</el-radio>
<el-radio :label="upgradeInfo.latestVersion">
<el-radio :value="upgradeInfo.latestVersion">
{{ upgradeInfo.latestVersion }} {{ $t('setting.latestVersion') }}
</el-radio>
</el-radio-group>
@@ -77,7 +77,7 @@ import { GlobalStore } from '@/store';
import { ElMessageBox } from 'element-plus';
const globalStore = GlobalStore();
const version = ref();
const version = ref<string>('');
const loading = ref(false);
const drawerVisible = ref(false);
const upgradeInfo = ref();
@@ -0,0 +1,30 @@
<template>
<div>
<div class="search-button">
<el-input
clearable
v-model="searchInfo"
@clear="search()"
suffix-icon="Search"
@change="search()"
:placeholder="props.placeholder || $t('commons.button.search')"
></el-input>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
defineOptions({ name: 'TableSearch' });
const emit = defineEmits(['search', 'update:searchName']);
const searchInfo = ref();
const props = defineProps({
placeholder: String,
});
const search = () => {
emit('update:searchName', searchInfo.value);
emit('search');
};
</script>
-1
View File
@@ -90,7 +90,6 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { computeSize } from '@/utils/util';
import OpDialog from '@/components/del-dialog/index.vue';
import { handleRecoverByUpload } from '@/api/modules/setting';
import i18n from '@/lang';
import { UploadFile, UploadFiles, UploadInstance } from 'element-plus';
+19
View File
@@ -487,6 +487,19 @@ const checkPHPExtensions = (rule, value, callback) => {
}
};
const checkHttpOrHttps = (rule, value, callback) => {
if (value === '' || typeof value === 'undefined' || value == null) {
callback(new Error(i18n.global.t('commons.rule.paramHttp')));
} else {
const regHttpHttps = /^(http|https):\/\//;
if (!regHttpHttps.test(value)) {
callback(new Error(i18n.global.t('commons.rule.paramHttp')));
} else {
callback();
}
}
};
interface CommonRule {
requiredInput: FormItemRule;
requiredSelect: FormItemRule;
@@ -527,6 +540,7 @@ interface CommonRule {
paramPort: FormItemRule;
paramExtUrl: FormItemRule;
paramSimple: FormItemRule;
paramHttp: FormItemRule;
}
export const Rules: CommonRule = {
@@ -730,4 +744,9 @@ export const Rules: CommonRule = {
validator: checkPHPExtensions,
trigger: 'blur',
},
paramHttp: {
required: true,
validator: checkHttpOrHttps,
trigger: 'blur',
},
};
+76
View File
@@ -181,3 +181,79 @@ export const DNSTypes = [
value: 'Godaddy',
},
];
export const Fields = [
{
label: 'URL',
value: 'URL',
},
{
label: 'IP',
value: 'IP',
},
{
label: 'Header',
value: 'Header',
},
{
label: 'Host',
value: 'Host',
},
];
export const Patterns = [
{
label: i18n.global.t('xpack.waf.contain'),
value: 'contain',
},
{
label: i18n.global.t('xpack.waf.equal'),
value: 'eq',
},
{
label: i18n.global.t('xpack.waf.regex'),
value: 'regex',
},
{
label: i18n.global.t('xpack.waf.notEqual'),
value: 'notEq',
},
];
export const HttpCodes = [
{
label: i18n.global.t('xpack.waf.badReq'),
value: 400,
},
{
label: i18n.global.t('xpack.waf.forbidden'),
value: 403,
},
{
label: i18n.global.t('xpack.waf.noRes'),
value: 444,
},
{
label: i18n.global.t('xpack.waf.serverErr'),
value: 500,
},
];
export const Actions = [
{
label: i18n.global.t('xpack.waf.actionAllow'),
value: 'allow',
},
{
label: i18n.global.t('xpack.waf.deny'),
value: 'deny',
},
{
label: i18n.global.t('xpack.waf.captcha'),
value: 'captcha',
},
{
label: i18n.global.t('xpack.waf.fiveSeconds'),
value: 'five_seconds',
},
];
+36
View File
@@ -0,0 +1,36 @@
import { GlobalStore } from '@/store';
import { searchXSetting } from '@/xpack/frontend/api/modules/setting';
import { computed } from 'vue';
export const useLogo = async () => {
const globalStore = GlobalStore();
const themeConfig = computed(() => globalStore.themeConfig);
if (!themeConfig.value.logo) {
const res = await searchXSetting();
globalStore.setThemeConfig({
...themeConfig.value,
title: res.data.title,
logo: res.data.logo,
logoWithText: res.data.logoWithText,
favicon: res.data.favicon,
});
}
let link = document.querySelector("link[rel*='icon']") as HTMLLinkElement | null;
if (link) {
if (globalStore.themeConfig.favicon) {
link.href = globalStore.themeConfig.favicon;
} else {
link.href = '/public/favicon.png';
}
} else {
const newLink = document.createElement('link');
newLink.rel = 'icon';
if (globalStore.themeConfig.favicon) {
newLink.href = globalStore.themeConfig.favicon;
} else {
newLink.href = '/public/favicon.png';
}
document.head.appendChild(newLink);
}
};
+197 -9
View File
@@ -51,6 +51,7 @@ const message = {
uninstall: 'Uninstall',
fullscreen: 'Fullscreen',
quitFullscreen: 'Quit Fullscreen',
update: 'Edit',
},
search: {
timeStart: 'Time start',
@@ -195,6 +196,7 @@ const message = {
filePermission: 'File Permission Error',
formatErr: 'Format error, please check and retry',
phpExtension: 'Only supports , _ lowercase English and numbers',
paramHttp: 'Must start with http:// or https://',
},
res: {
paramError: 'The request failed, please try again later!',
@@ -245,6 +247,7 @@ const message = {
ready: 'normal',
applying: 'Applying',
applyerror: 'Failure',
syncerr: 'Error',
},
units: {
second: 'Second',
@@ -291,6 +294,7 @@ const message = {
process: 'Process',
network: 'Network',
supervisor: 'Supervisor',
name: 'Tamper Proof',
},
home: {
restart_1panel: 'Restart Panel',
@@ -360,6 +364,7 @@ const message = {
goUpgrade: 'Go for upgrade',
goInstall: 'Go for install',
source: 'Source',
isDelete: 'Deleted',
permission: 'Permission',
permissionForIP: 'IP',
permissionAll: 'All of them(%)',
@@ -384,7 +389,7 @@ const message = {
portHelper:
'This port is the exposed port of the container. You need to save the modification separately and restart the container!',
loadFromRemote: 'Load from server',
loadFromRemote: 'Sync from Server',
userBind: 'Bind User',
pgBindHelper:
'This operation is used to create a new user and bind it to the target database. Currently, selecting users already existing in the database is not supported.',
@@ -805,7 +810,7 @@ const message = {
database: 'Backup database',
missBackupAccount: 'The backup account could not be found',
syncDate: 'Synchronization time ',
releaseMemory: 'Free memory',
clean: 'Cache Clean',
curl: 'Access URL',
taskName: 'Cronjob name',
cronSpec: 'Lifecycle',
@@ -908,7 +913,7 @@ const message = {
ip: 'Address',
authMode: 'Auth Mode',
passwordMode: 'Password',
rememberPassword: 'Remember password',
rememberPassword: 'Remember Authentication Information',
keyMode: 'PrivateKey',
key: 'Private key',
keyPassword: 'Private key password',
@@ -1392,6 +1397,10 @@ const message = {
snapshot: 'Snapshot',
status: 'Snapshot status',
ignoreRule: 'Ignore Rule',
ignoreHelper:
'This rule will be used to compress and backup the 1Panel data directory during snapshots, please modify with caution.',
ignoreHelper1: 'One item per line, e.g.: \n*.log\n/opt/1panel/cache',
panelInfo: 'Write 1Panel basic information',
panelBin: 'Backup 1Panel system files',
daemonJson: 'Backup Docker configuration file',
@@ -1404,19 +1413,20 @@ const message = {
createSnapshot: 'Create snapshot',
importSnapshot: 'Sync snapshot',
recover: 'Recover',
noRecoverRecord: 'No recovery record has been recorded',
lastRecoverAt: 'Last recovery time',
lastRollbackAt: 'Last rollback time',
noRollbackRecord: 'No rollback record has been recorded',
reDownload: 'Download the backup file again',
recoverRecord: 'Recover record',
recoverHelper:
'The recovery is about to start from snapshot {0}, and the recovery needs to restart docker and 1panel service, do you want to continue?',
recoverHelper1:
'Will start restoring from snapshot {0}, please ensure that the server architecture matches the one where the snapshot was created.',
recoverHelper2: 'Restoring snapshots between different server architectures is not supported.',
rollback: 'Rollback',
rollbackHelper:
'This recovery is about to be rolled back, which will replace all the files recovered this time. In the process, docker and 1panel services may need to be restarted. Do you want to continue?',
upgradeHelper: 'The upgrade requires a service restart. Do you want to continue?',
upgradeHelper: 'The upgrade requires restarting the 1Panel service. Do you want to continue?',
noUpgrade: 'It is currently the latest version',
versionHelper:
'Name rules: [major version].[functional version].[Bug fix version], as shown in the following example:',
@@ -1436,8 +1446,8 @@ const message = {
doc: 'Official document',
star: 'Star',
description: '1Panel, Modern, open source Linux server operation and maintenance management panel.',
forum: 'Forum',
doc2: 'Document',
forum: 'Forum Help',
doc2: 'User Manual',
currentVersion: 'Version',
},
clean: {
@@ -1814,7 +1824,7 @@ const message = {
imported: 'Manually Created',
importType: 'Import Type',
pasteSSL: 'Paste code',
localSSL: 'Select local file',
localSSL: 'Select server file',
privateKeyPath: 'Private key file',
certificatePath: 'Certificate file',
ipWhiteListHelper: 'The role of IP whitelist: all rules are invalid for IP whitelist',
@@ -2142,6 +2152,184 @@ const message = {
manage: 'Management',
},
},
xpack: {
name: 'Professional',
waf: {
name: 'WAF',
blackWhite: 'black and white list',
globalSetting: 'Global Setting',
websiteSetting: 'Website Settings',
blockRecords: 'Block records',
world: 'world',
china: 'China',
intercept: 'interception',
request: 'request',
count4xx: '4xx quantity',
count5xx: '5xx quantity',
todayStatus: 'Today Status',
reqMap: 'Attack map (30 days)',
resource: 'source',
count: 'Quantity',
hight: 'high',
low: 'low',
reqCount: 'Number of requests',
interceptCount: 'Interception number',
requestTrends: 'Request Trends (7 days)',
interceptTrends: 'Intercept Trends (7 days)',
whiteList: 'whitelist',
blackList: 'blacklist',
ipBlackListHelper: 'IPs in the blacklist cannot access the website',
ipWhiteListHelper: 'IPs in the whitelist are not restricted by any rules',
uaBlackListHelper: 'Requests carrying User-Agent in the blacklist will be intercepted',
uaWhiteListHelper: 'Requests carrying User-Agent in the whitelist are not restricted by any rules',
urlBlackListHelper: 'Requests for URLs in the blacklist will be intercepted',
urlWhiteListHelper: 'Requests for URLs in the whitelist are not restricted by any rules',
ccHelper:
'If any website has been requested more than {1} times in {0} seconds, this IP will be blocked for {2} seconds',
blockTime: 'Block time',
attackHelper:
'The cumulative interception exceeds {1} times within {0} seconds, block this IP for {2} seconds',
notFoundHelper:
'The cumulative request returned 404 more than {1} times within {0} seconds, block this IP for {2} seconds',
frequencyLimit: 'frequency limit',
regionLimit: 'region limit',
defaultRule: 'Default rule',
accessFrequencyLmit: 'Access frequency limit',
attackLimit: 'Attack frequency limit',
notFoundLimit: '404 frequency limit',
urlLimit: 'URL frequency limit',
urlLimitHelper: 'Set access frequency for a single URL',
sqliDefense: 'SQL injection defense',
sqliHelper: 'Identify SQL injection in requests and intercept',
xssHelper: 'Identify XSS in the request and intercept it',
xssDefense: 'XSS Defense',
uaDefense: 'Malicious User-Agent Rule',
uaHelper: 'Contains common malicious crawler rules',
argsDefense: 'Malicious parameter rules',
argsHelper: 'Prohibit malicious parameters in requests',
cookieDefense: 'Malicious Cookie Rule',
cookieHelper: 'Prohibit malicious cookies from being carried in requests',
headerDefense: 'Malicious Header Rule',
headerHelper: 'Prohibit requests from containing malicious headers',
httpRule: 'HTTP request method rules',
httpHelper: 'Restrict the request method type of the website',
geoRule: 'Regional access restrictions',
geoHelper: 'Restrict access to your website from certain regions',
ipLocation: 'IP home location',
action: 'action',
ruleType: 'attack type',
ipHelper: 'Please enter IP',
attackLog: 'Attack Log',
rule: 'Rule',
ipArr: 'IPV4 range',
ipStart: 'Start IP',
ipEnd: 'End IP',
ipv4: 'IPV4',
ipv6: 'IPV6',
urlDefense: 'URL rules',
urlHelper: 'Forbidden URL',
dirFilter: 'Directory filter',
sqlInject: 'SQL injection',
xss: 'XSS',
phpExec: 'PHP script execution',
oneWordTrojan: 'One word Trojan',
appFilter: 'Apply dangerous directory filtering',
webshell: 'Webshell',
args: 'Malicious parameters',
protocolFilter: 'Protocol filter',
javaFileter: 'Java Dangerous File Filtering',
scannerFilter: 'Scanner filter',
escapeFilter: 'escape filter',
customRule: 'custom rule',
httpMethod: 'HTTP method filter',
fileExt: 'File upload limit',
fileExtHelper: 'File extensions that are prohibited from uploading',
deny: 'forbidden',
allow: 'only allow',
field: 'match object',
pattern: 'matching condition',
ruleContent: 'match content',
contain: 'include',
equal: 'equal',
regex: 'regular expression',
notEqual: 'Not equal to',
customRuleHelper: 'Perform corresponding actions based on condition matching',
actionAllow: 'Allow',
blockIP: 'Block IP',
code: 'return status code',
noRes: 'Disconnect 444',
badReq: 'Parameter error 400',
forbidden: 'Access Forbidden 403',
serverErr: 'Server error 500',
resHtml: 'Response page',
allowHelper: 'Allowing access will skip subsequent WAF rules, please use with caution',
captcha: 'human-machine verification',
fiveSeconds: '5 seconds verification',
location: 'Region',
redisConfig: 'Redis configuration',
redisHelper: 'Enable Redis to persist temporarily blocked IPs',
wafHelper: 'All websites will lose protection after closing',
attackIP: 'Attack IP',
attackParam: 'Attack information',
execRule: 'Hit rule',
acl: 'ACL',
sql: 'SQL injection',
cc: 'CC attack',
isBlocking: 'Blocking',
isFree: 'Unblocked',
},
monitor: {
name: 'Website Monitor',
},
tamper: {
tamper: 'Tamper',
tamperHelper1:
'One-click deployment type of website, it is recommended to enable the application directory anti-tampering function;',
tamperHelper2:
'If the website cannot be used normally or backup and restore fail after enabling the anti-tampering function, please disable the anti-tampering function first;',
tamperHelper3:
'Enabling tamper-proofing will restrict reading, writing, deleting, permission, and owner modification operations of protected files under non-excluded directories. Please choose carefully when setting [Exclude Directory] and [Protect].',
op: 'Operation',
create: 'Create',
file: 'File',
tamperPath: 'Protection Directory',
tamperPathEdit: 'Modify Path',
log: 'Intercept Logs',
totalProtect: 'Total Protection',
todayProtect: 'Today’s Protection',
addRule: 'Add Rule',
ignore: 'Exclude Directory',
ignoreHelper: 'One per line, e.g., \ntmp\n./tmp',
ignoreHelper1: 'Add folder names or specific paths to ignore',
ignoreHelper2: 'To ignore specific folders, use relative paths starting with ./',
protect: 'Protect',
protectHelper: 'One per line, e.g., \npng\n./test.css',
protectHelper1: 'Specify file names, suffixes, or specific files for protection',
protectHelper2: 'To protect specific files, use relative paths starting with ./',
enableHelper:
'The anti-tampering function of website {0} is about to be enabled to enhance website security. Do you want to continue?',
disableHelper:
'The anti-tampering function of website {0} is about to be disabled. Do you want to continue?',
},
setting: {
setting: 'Interface Settings',
title: 'Panel Description',
titleHelper:
'Will be displayed on the user login page (e.g., Linux Server Operation and Maintenance Management Panel)',
logo: 'Logo',
logoHelper:
'Will be displayed on the top left of the management page when the menu is collapsed (recommended image size: 82px*82px)',
logoWithText: 'Logo (with text)',
logoWithTextHelper:
'Will be displayed on the top left of the management page when the menu is expanded (recommended image size: 185px*55px)',
favicon: 'Website Icon',
faviconHelper: 'Website icon (recommended image size: 16px*16px)',
reUpload: 'Re-upload',
supportType: 'Only jpg/png/jpeg files are allowed!',
setDefault: 'Restore Default',
reset: 'Reset',
},
},
};
export default {
+185 -12
View File
@@ -51,6 +51,7 @@ const message = {
uninstall: '卸載',
fullscreen: '全屏',
quitFullscreen: '退出全屏',
update: '編輯',
},
search: {
timeStart: '開始時間',
@@ -193,6 +194,7 @@ const message = {
filePermission: '權限錯誤',
formatErr: '格式錯誤,檢查後重試',
phpExtension: '僅支持 , _ 小寫英文和數字',
paramHttp: '必須以 http:// 或 https:// 開頭',
},
res: {
paramError: '請求失敗,請稍後重試!',
@@ -243,6 +245,7 @@ const message = {
ready: '正常',
applying: '申請中',
applyerror: '失敗',
syncerr: '失敗',
},
units: {
second: '秒',
@@ -288,6 +291,7 @@ const message = {
process: '進程',
network: '網絡',
supervisor: '進程守護',
tamper: '防篡改',
},
home: {
restart_1panel: '重啟面板',
@@ -356,6 +360,7 @@ const message = {
goUpgrade: '去應用商店升級',
goInstall: '去應用商店安裝',
source: '來源',
isDelete: '已刪除',
permission: '權限',
permissionForIP: '指定 IP',
permissionAll: '所有人(%)',
@@ -376,7 +381,7 @@ const message = {
confChange: '配置修改',
confNotFound: '未能找到該應用配置文件,請在應用商店升級該應用至最新版本後重試!',
loadFromRemote: '從服務器獲取',
loadFromRemote: '從伺服器同步',
userBind: '綁定使用者',
pgBindHelper: '此操作用於創建新使用者並將其綁定到目標資料庫,暫不支援選擇已存在於資料庫中的使用者。',
pgSuperUser: '超級使用者',
@@ -767,7 +772,7 @@ const message = {
database: '備份數據庫',
missBackupAccount: '未能找到備份賬號',
syncDate: '同步時間 ',
releaseMemory: '釋放內存',
clean: '缓存清理',
curl: '訪問 URL',
taskName: '任務名稱',
cronSpec: '執行周期',
@@ -866,7 +871,7 @@ const message = {
ip: '主機地址',
authMode: '認證方式',
passwordMode: '密碼認證',
rememberPassword: '記住密碼',
rememberPassword: '記住認證資訊',
keyMode: '私鑰認證',
key: '私鑰',
keyPassword: '私鑰密碼',
@@ -1233,6 +1238,9 @@ const message = {
snapshot: '快照',
status: '快照狀態',
ignoreRule: '排除規則',
ignoreHelper: '快照時將使用該規則對 1Panel 數據目錄進行壓縮備份,請謹慎修改。',
ignoreHelper1: '一行一個,例: \n*.log\n/opt/1panel/cache',
panelInfo: '寫入 1Panel 基礎信息',
panelBin: '備份 1Panel 系統文件',
daemonJson: '備份 Docker 配置文件',
@@ -1245,23 +1253,20 @@ const message = {
createSnapshot: '創建快照',
importSnapshot: '同步快照',
recover: '恢復',
noRecoverRecord: '暫無恢復記錄',
lastRecoverAt: '上次恢復時間',
lastRollbackAt: '上次回滾時間',
noRollbackRecord: '暫無回滾記錄',
reDownload: '重新下載備份文件',
statusAll: '全部',
statusSuccess: '成功',
statusFailed: '失敗',
versionChange: '版本變化',
snapshotFrom: '快照存儲位置',
recoverHelper: '即將從快照 {0} 開始恢復,恢復需要重啟 docker 以及 1panel 服務,是否繼續?',
recoverHelper1: '即將從快照 {0} 開始恢復,請確保伺服器架構與創建快照伺服器架構信息保持一致。',
recoverHelper2: '不支持在不同伺服器架構之間進行快照恢復操作。',
rollback: '回滾',
rollbackHelper:
'即將回滾本次恢復,回滾將替換所有本次恢復的文件,過程中可能需要重啟 docker 以及 1panel 服務,是否繼續?',
upgrading: '正在升級中,請稍候...',
upgradeHelper: '升級操作需要重啟服務,是否繼續?',
upgradeHelper: '升級操作需要重啟 1Panel 服務,是否繼續?',
noUpgrade: '當前已經是最新版本',
versionHelper: '1Panel 版本號命名規則為: [大版本].[功能版本].[Bug 修復版本],例:',
versionHelper1: 'v1.0.1 是 v1.0.0 之後的 Bug 修復版本',
@@ -1342,8 +1347,8 @@ const message = {
doc: '官方文檔',
star: '點亮 Star',
description: '1Panel,現代化、開源的 Linux 服務器運維管理面板。',
forum: '論壇',
doc2: '文檔',
forum: '論壇求助',
doc2: '使用手冊',
currentVersion: '當前運行版本:',
},
clean: {
@@ -1701,7 +1706,7 @@ const message = {
imported: '手動創建',
importType: '導入方式',
pasteSSL: '粘貼代碼',
localSSL: '選擇本地文件',
localSSL: '選擇伺服器文件',
privateKeyPath: '私鑰文件',
certificatePath: '證書文件',
ipWhiteListHelper: 'IP白名單的作用:所有規則對IP白名單無效',
@@ -2004,6 +2009,174 @@ const message = {
manage: '管理',
},
},
xpack: {
name: '專業版',
waf: {
name: 'WAF',
blackWhite: '黑白名單',
globalSetting: '全域設定',
websiteSetting: '網站設定',
blockRecords: '封鎖記錄',
world: '世界',
china: '中國',
intercept: '攔截',
request: '請求',
count4xx: '4xx 數量',
count5xx: '5xx 數量',
todayStatus: '今日狀態',
reqMap: '攔截地圖(30日)',
resource: '來源',
count: '數量',
hight: '高',
low: '低',
reqCount: '請求數',
interceptCount: '攔截數',
requestTrends: '請求趨勢(7天)',
interceptTrends: '攔截趨勢(7天)',
whiteList: '白名單',
blackList: '黑名單',
ipBlackListHelper: '黑名單中的 IP 無法存取網站',
ipWhiteListHelper: '白名單中的 IP 不受任何規則限制',
uaBlackListHelper: '攜帶黑名單中的 User-Agent 的請求將被攔截',
uaWhiteListHelper: '攜帶白名單中的 User-Agent 的請求不受任何規則限制',
urlBlackListHelper: '請求黑名單中的 URL 將被攔截',
urlWhiteListHelper: '請求白名單中的 URL 請求不受任何規則限制',
ccHelper: '{0} 秒內累積請求任意網站超過 {1} 次,封鎖此 IP {2} 秒',
blockTime: '封鎖時間',
attackHelper: '{0} 秒內累計攔截超過 {1} 次,封鎖此 IP {2} 秒',
notFoundHelper: '{0} 秒內累計請求回傳 404 超過 {1} 次,封鎖此 IP {2} 秒',
frequencyLimit: '頻率限制',
regionLimit: '地區限制',
defaultRule: '預設規則',
accessFrequencyLmit: '存取頻率限制',
attackLimit: '攻擊頻率限制',
notFoundLimit: '404 頻率限制',
urlLimit: 'URL 頻率限制',
urlLimitHelper: '為單一 URL 設定存取頻率',
sqliDefense: 'SQL 注入防禦',
sqliHelper: '辨識請求中的 SQL 注入並攔截',
xssHelper: '辨識請求中的 XSS 並攔截',
xssDefense: 'XSS 防禦',
uaDefense: '惡意 User-Agent 規則',
uaHelper: '包含常見的惡意爬蟲規則',
argsDefense: '惡意參數規則',
argsHelper: '在禁止請求中攜帶惡意參數',
cookieDefense: '惡意 Cookie 規則',
cookieHelper: '禁止請求中攜帶惡意 Cookie',
headerDefense: '惡意 Header 規則',
headerHelper: '禁止請求中攜帶惡意 Header',
httpRule: 'HTTP 請求方法規則',
httpHelper: '限制網站的請求方法類型',
geoRule: '地區存取限制',
geoHelper: '限制某些地區造訪你的網站',
ipLocation: 'IP 歸屬地',
action: '動作',
ruleType: '攻擊類型',
ipHelper: '請輸入 IP',
attackLog: '攻擊日誌',
rule: '規則',
ipArr: 'IPV4 範圍',
ipStart: '起始 IP',
ipEnd: '結束 IP',
ipv4: 'IPV4',
ipv6: 'IPV6',
urlDefense: 'URL 規則',
urlHelper: '禁止存取的 URL',
dirFilter: '目錄過濾',
sqlInject: 'SQL 注入',
xss: 'XSS',
phpExec: 'PHP 腳本執行',
oneWordTrojan: '一句話木馬',
appFilter: '套用危險目錄過濾',
webshell: 'Webshell',
args: '惡意參數',
protocolFilter: '協議過濾',
javaFileter: 'Java 危險檔案過濾',
scannerFilter: '掃描器過濾',
escapeFilter: '轉義過濾',
customRule: '自訂規則',
httpMethod: 'HTTP 方法過濾',
fileExt: '檔案上傳限制',
fileExtHelper: '禁止上傳的檔案副檔名',
deny: '禁止',
allow: '只允許',
field: '匹配對象',
pattern: '符合條件',
ruleContent: '符合內容',
contain: '包含',
equal: '等於',
regex: '正規表示式',
notEqual: '不等於',
customRuleHelper: '根據條件匹配執行對應動作',
actionAllow: '允許',
blockIP: '封鎖 IP',
code: '返回狀態碼',
noRes: '斷開連線 444',
badReq: '參數錯誤 400',
forbidden: '禁止訪問 403',
serverErr: '伺服器錯誤 500',
resHtml: '回應頁面',
allowHelper: '允許訪問會跳過後續的 WAF 規則,請謹慎使用',
captcha: '人機驗證',
fiveSeconds: '5 秒驗證',
location: '地區',
redisConfig: 'Redis 配置',
redisHelper: '開啟 Redis 可以將暫時拉黑的 IP 持久化',
wafHelper: '關閉之後所有網站將失去防護',
attackIP: '攻擊 IP',
attackParam: '攻擊訊息',
execRule: '命中規則',
acl: 'ACL',
sql: 'SQL 注入',
cc: 'CC 攻擊',
isBlocking: '封鎖中',
isFree: '已解封',
},
monitor: {
name: '網站監控',
},
tamper: {
tamper: '防篡改',
tamperHelper1: '一鍵部署類型的網站,建議啟用應用目錄防篡改功能;',
tamperHelper2: '如果在啟用防篡改功能後出現網站無法正常使用或備份、恢復失敗的情況,請先關閉防篡改功能;',
tamperHelper3:
'啟用防篡改,將限制非排除目錄下受保護文件的讀寫、刪除、權限和所有者修改操作,請在設定 [排除目錄] 和 [保護] 時謹慎選擇。',
op: '操作',
create: '創建',
file: '文件',
tamperPath: '防護目錄',
tamperPathEdit: '修改路徑',
log: '攔截日誌',
totalProtect: '總防護',
todayProtect: '今日防護',
addRule: '添加規則',
ignore: '排除目錄',
ignoreHelper: '一行一個,例: \ntmp\n./tmp',
ignoreHelper1: '添加要忽略的文件夾名或特定路徑',
ignoreHelper2: '要忽略特定文件夾,請使用以 ./ 開頭的相對路徑',
protect: '保護',
protectHelper: '一行一個,例: \npng\n./test.css',
protectHelper1: '可指定文件名、後綴名或特定文件進行保護',
protectHelper2: '要保護特定文件,請使用以 ./ 開頭的相對路徑',
enableHelper: '即將啟用 {0} 網站的防窜改功能,以提升網站安全性,是否繼續?',
disableHelper: '即將關閉 {0} 網站的防窜改功能,是否繼續?',
},
setting: {
setting: '界面設置',
title: '面板描述',
titleHelper: '將會顯示在使用者登錄頁面 (例: Linux 伺服器運維管理面板)',
logo: 'Logo',
logoHelper: '將會顯示在菜單收縮時管理頁面左上方 (建議圖片大小為: 82px*82px)',
logoWithText: 'Logo (帶文字)',
logoWithTextHelper: '將會顯示在菜單展開時管理頁面左上方 (建議圖片大小為: 185px*55px)',
favicon: '網站圖標',
faviconHelper: '網站圖標 (建議圖片大小為: 16px*16px)',
reUpload: '重新上傳',
supportType: '只能上傳 jpg/png/jpeg 檔案!',
setDefault: '恢復默認',
reset: '重置',
},
},
};
export default {
...fit2cloudTwLocale,
+186 -12
View File
@@ -51,6 +51,7 @@ const message = {
uninstall: '卸载',
fullscreen: '全屏',
quitFullscreen: '退出全屏',
update: '编辑',
},
search: {
timeStart: '开始时间',
@@ -193,6 +194,7 @@ const message = {
filePermission: '权限错误',
formatErr: '格式错误,检查后重试',
phpExtension: '仅支持 , _ 小写英文和数字',
paramHttp: '必须以 http:// 或 https:// 开头',
},
res: {
paramError: '请求失败,请稍后重试!',
@@ -243,6 +245,7 @@ const message = {
ready: '正常',
applying: '申请中',
applyerror: '失败',
syncerr: '失败',
},
units: {
second: '秒',
@@ -288,6 +291,7 @@ const message = {
process: '进程',
network: '网络',
supervisor: '进程守护',
tamper: '防篡改',
},
home: {
restart_1panel: '重启面板',
@@ -356,6 +360,7 @@ const message = {
goUpgrade: '去应用列表升级',
goInstall: '去应用商店安装',
source: '来源',
isDelete: '已删除',
permission: '权限',
permissionForIP: '指定 IP',
permissionAll: '所有人(%)',
@@ -376,7 +381,7 @@ const message = {
confChange: '配置修改',
confNotFound: '未能找到该应用配置文件,请在应用商店升级该应用至最新版本后重试!',
loadFromRemote: '从服务器获取',
loadFromRemote: '从服务器同步',
userBind: '绑定用户',
pgBindHelper: '该操作用于创建新用户并将其绑定到目标数据库,暂不支持选择已存在于数据库中的用户。',
pgSuperUser: '超级用户',
@@ -768,7 +773,7 @@ const message = {
database: '备份数据库',
missBackupAccount: '未能找到备份账号',
syncDate: '同步时间 ',
releaseMemory: '释放内存',
clean: '缓存清理',
curl: '访问 URL',
taskName: '任务名称',
cronSpec: '执行周期',
@@ -867,7 +872,7 @@ const message = {
ip: '主机地址',
authMode: '认证方式',
passwordMode: '密码认证',
rememberPassword: '记住密码',
rememberPassword: '记住认证信息',
keyMode: '私钥认证',
key: '私钥',
keyPassword: '私钥密码',
@@ -1233,6 +1238,9 @@ const message = {
path: '路径',
snapshot: '快照',
ignoreRule: '排除规则',
ignoreHelper: '快照时将使用该规则对 1Panel 数据目录进行压缩备份,请谨慎修改。',
ignoreHelper1: '一行一个,例: \n*.log\n/opt/1panel/cache',
status: '快照状态',
panelInfo: '写入 1Panel 基础信息',
panelBin: '备份 1Panel 系统文件',
@@ -1246,23 +1254,20 @@ const message = {
createSnapshot: '创建快照',
importSnapshot: '同步快照',
recover: '恢复',
noRecoverRecord: '暂无恢复记录',
lastRecoverAt: '上次恢复时间',
lastRollbackAt: '上次回滚时间',
noRollbackRecord: '暂无回滚记录',
reDownload: '重新下载备份文件',
statusAll: '全部',
statusSuccess: '成功',
statusFailed: '失败',
versionChange: '版本变化',
snapshotFrom: '快照存储位置',
recoverHelper: '即将从快照 {0} 开始恢复,恢复需要重启 docker 以及 1panel 服务,是否继续?',
recoverHelper1: '即将从快照 {0} 开始恢复,请确保服务器架构与创建快照服务器架构信息保持一致。',
recoverHelper2: '不支持在不同服务器架构之间进行快照恢复操作。',
rollback: '回滚',
rollbackHelper:
'即将回滚本次恢复,回滚将替换所有本次恢复的文件,过程中可能需要重启 docker 以及 1panel 服务,是否继续?',
upgrading: '正在升级中,请稍候...',
upgradeHelper: '升级操作需要重启服务,是否继续?',
upgradeHelper: '升级操作需要重启 1Panel 服务,是否继续?',
noUpgrade: '当前已经是最新版本',
versionHelper: '1Panel 版本号命名规则为: [大版本].[功能版本].[Bug 修复版本],例:',
versionHelper1: 'v1.0.1 是 v1.0.0 之后的 Bug 修复版本',
@@ -1343,8 +1348,8 @@ const message = {
doc: '官方文档',
star: '点亮 Star',
description: '1Panel,现代化、开源的 Linux 服务器运维管理面板。',
forum: '论坛',
doc2: '文档',
forum: '论坛求助',
doc2: '使用手冊',
currentVersion: '当前运行版本:',
},
clean: {
@@ -1701,7 +1706,7 @@ const message = {
imported: '手动创建',
importType: '导入方式',
pasteSSL: '粘贴代码',
localSSL: '选择本地文件',
localSSL: '选择服务器文件',
privateKeyPath: '私钥文件',
certificatePath: '证书文件',
ipWhiteListHelper: 'IP 白名单的作用:所有规则对IP白名单无效',
@@ -2004,6 +2009,175 @@ const message = {
manage: '管理',
},
},
xpack: {
name: '专业版',
menu: '高级功能',
waf: {
name: 'WAF',
blackWhite: '黑白名单',
globalSetting: '全局设置',
websiteSetting: '网站设置',
blockRecords: '封锁记录',
world: '世界',
china: '中国',
intercept: '拦截',
request: '请求',
count4xx: '4xx 数量',
count5xx: '5xx 数量',
todayStatus: '今日状态',
reqMap: '拦截地图(30日)',
resource: '来源',
count: '数量',
hight: '高',
low: '低',
reqCount: '请求数',
interceptCount: '拦截数',
requestTrends: '请求趋势(7日)',
interceptTrends: '拦截趋势(7日)',
whiteList: '白名单',
blackList: '黑名单',
ipBlackListHelper: '黑名单中的 IP 无法访问网站',
ipWhiteListHelper: '白名单中的 IP 不受任何规则限制',
uaBlackListHelper: '携带黑名单中的 User-Agent 的请求将被拦截',
uaWhiteListHelper: '携带白名单中的 User-Agent 的请求不受任何规则限制',
urlBlackListHelper: '请求黑名单中的 URL 将被拦截',
urlWhiteListHelper: '请求白名单中的 URL 请求不受任何规则限制',
ccHelper: '{0} 秒内累计请求任意网站超过 {1} 次,封锁此 IP {2} 秒',
blockTime: '封禁时间',
attackHelper: '{0} 秒内累计拦截超过 {1} 次,封锁此 IP {2} 秒',
notFoundHelper: '{0} 秒内累计请求返回 404 超过 {1} 次,封锁此 IP {2} 秒',
frequencyLimit: '频率限制',
regionLimit: '地区限制',
defaultRule: '默认规则',
accessFrequencyLmit: '访问频率限制',
attackLimit: '攻击频率限制',
notFoundLimit: '404 频率限制',
urlLimit: 'URL 频率限制',
urlLimitHelper: '为单个 URL 设置访问频率',
sqliDefense: 'SQL 注入防御',
sqliHelper: '识别请求中的 SQL 注入并拦截',
xssHelper: '识别请求中的 XSS 并拦截',
xssDefense: 'XSS 防御',
uaDefense: 'User-Agent 规则',
uaHelper: '包含常见的恶意爬虫规则',
argsDefense: '参数规则',
argsHelper: '禁止请求中携带恶意参数',
cookieDefense: 'Cookie 规则',
cookieHelper: '禁止请求中携带恶意 Cookie',
headerDefense: 'Header 规则',
headerHelper: '禁止请求中携带恶意 Header',
httpRule: 'HTTP 规则',
httpHelper: '限制网站的请求方法类型',
geoRule: '地区访问限制',
geoHelper: '限制某些地区访问你的网站',
ipLocation: 'IP 归属地',
action: '动作',
ruleType: '攻击类型',
ipHelper: '请输入 IP',
attackLog: '攻击日志',
rule: '规则',
ipArr: 'IPV4 范围',
ipStart: '起始 IP',
ipEnd: '结束 IP',
ipv4: 'IPV4',
ipv6: 'IPV6',
urlDefense: 'URL 规则',
urlHelper: '禁止访问的 URL',
dirFilter: '目录过滤',
sqlInject: 'SQL 注入',
xss: 'XSS',
phpExec: 'PHP 脚本执行',
oneWordTrojan: '一句话木马',
appFilter: '应用危险目录过滤',
webshell: 'Webshell',
args: '恶意参数',
protocolFilter: '协议过滤',
javaFileter: 'Java 危险文件过滤',
scannerFilter: '扫描器过滤',
escapeFilter: '转义过滤',
customRule: '自定义规则',
httpMethod: 'HTTP 方法过滤',
fileExt: '文件上传限制',
fileExtHelper: '禁止上传的文件扩展名',
deny: '禁止',
allow: '仅允许',
field: '匹配对象',
pattern: '匹配条件',
ruleContent: '匹配内容',
contain: '包含',
equal: '等于',
regex: '正则表达式',
notEqual: '不等于',
customRuleHelper: '根据条件匹配执行相应动作',
actionAllow: '允许',
blockIP: '封禁 IP',
code: '返回状态码',
noRes: '断开连接 444',
badReq: '参数错误 400',
forbidden: '禁止访问 403',
serverErr: '服务器错误 500',
resHtml: '响应页面',
allowHelper: '允许访问会跳过后续的 WAF 规则,请谨慎使用',
captcha: '人机验证',
fiveSeconds: '5 秒验证',
location: '地区',
redisConfig: 'Redis 配置',
redisHelper: '开启 Redis 可以将临时拉黑的 IP 持久化',
wafHelper: '关闭之后所有网站将失去防护',
attackIP: '攻击 IP',
attackParam: '攻击信息',
execRule: '命中规则',
acl: 'ACL',
sql: 'SQL 注入',
cc: 'CC 攻击',
isBlocking: '封禁中',
isFree: '已解封',
},
monitor: {
name: '网站监控',
},
tamper: {
tamper: '防篡改',
tamperHelper1: '一键部署类型的网站,建议启用应用目录防篡改功能;',
tamperHelper2: '如果在启用防篡改功能后出现网站无法正常使用或备份、恢复失败的情况,请先关闭防篡改功能;',
tamperHelper3:
'启用防篡改,将限制非排除目录下受保护文件的读写、删除、权限和所有者修改操作,请在设定 [排除目录] 和 [保护] 时谨慎选择。',
op: '操作',
create: '创建',
file: '文件',
tamperPath: '防护目录',
tamperPathEdit: '修改路径',
log: '拦截日志',
totalProtect: '总防护',
todayProtect: '今日防护',
addRule: '添加规则',
ignore: '排除目录',
ignoreHelper: '一行一个,例: \ntmp\n./tmp',
ignoreHelper1: '添加要忽略的文件夹名或特定路径',
ignoreHelper2: '要忽略特定文件夹,请使用以 ./ 开头的相对路径',
protect: '保护',
protectHelper: '一行一个,例: \npng\n./test.css',
protectHelper1: '可指定文件名、后缀名或特定文件进行保护',
protectHelper2: '要保护特定文件,请使用以 ./ 开头的相对路径',
enableHelper: '即将启用 {0} 网站的防窜改功能,以提升网站安全性,是否继续?',
disableHelper: '即将关闭 {0} 网站的防窜改功能,是否继续?',
},
setting: {
setting: '界面设置',
title: '面板描述',
titleHelper: '将会显示在用户登录页面 (例: Linux 服务器运维管理面板)',
logo: 'Logo',
logoHelper: '将会显示在菜单收缩时管理页面左上方 (建议图片大小为: 82px*82px)',
logoWithText: 'Logo (带文字)',
logoWithTextHelper: '将会显示在菜单展开时管理页面左上方 (建议图片大小为: 185px*55px)',
favicon: '网站图标',
faviconHelper: '网站图标 (建议图片大小为: 16px*16px)',
reUpload: '重新上传',
supportType: '只能上传 jpg/png/jpeg 文件!',
setDefault: '恢复默认',
reset: '重置',
},
},
};
export default {
...fit2cloudZhLocale,
+1 -2
View File
@@ -10,10 +10,9 @@
<script setup lang="ts">
import cacheRouter from '@/routers/cache-router';
import { useRoute } from 'vue-router';
import { computed } from 'vue';
const key = computed(() => {
return useRoute().path + Math.random();
return Math.random();
});
</script>
+8 -3
View File
@@ -5,14 +5,19 @@ import { AxiosCanceler } from '@/api/helper/axios-cancel';
const axiosCanceler = new AxiosCanceler();
/**
* @description 路由拦截 beforeEach(路由配置无数种方法,个人觉得最简便)
* */
router.beforeEach((to, from, next) => {
NProgress.start();
axiosCanceler.removeAllPending();
const globalStore = GlobalStore();
if (to.name !== 'entrance' && !globalStore.isLogin) {
next({
name: 'entrance',
params: { code: globalStore.entrance },
});
NProgress.done();
return;
}
if (to.name === 'entrance' && globalStore.isLogin) {
if (to.params.code === globalStore.entrance) {
next({
+5
View File
@@ -20,6 +20,11 @@ export const GlobalStore = defineStore({
primary: '#005EEB',
theme: 'auto',
footer: true,
title: '',
logo: '',
logoWithText: '',
favicon: '',
},
isFullScreen: false,
isOnRestart: false,
+5
View File
@@ -5,6 +5,11 @@ export interface ThemeConfigProp {
primary: string;
theme: string; // dark | bright | auto
footer: boolean;
title: string;
logo: string;
logoWithText: string;
favicon: string;
}
export interface GlobalState {
+25 -1
View File
@@ -412,5 +412,29 @@ html {
}
.p-mb-5 {
margin-bottom: 5px !important;
margin-bottom: 5px !important;
}
.p-w-200 {
width: 200px !important;
}
.p-w-100 {
width: 100px !important;
}
.p-w-150 {
width: 150px !important;
}
.p-w-400 {
width: 400px !important;
}
.p-ml-20 {
margin-left: 20px !important;
}
.p-mt-20 {
margin-top: 20px !important;
}
-3
View File
@@ -160,9 +160,6 @@ html {
.el-dialog {
.el-dialog__header {
padding: 15px 20px;
margin: 0;
border-bottom: 1px solid #f0f0f0;
.el-dialog__title {
font-size: 17px;
}
+45
View File
@@ -0,0 +1,45 @@
import i18n from '@/lang';
export const shortcuts = [
{
text: i18n.global.t('monitor.today'),
value: () => {
const end = new Date();
const start = new Date(new Date().setHours(0, 0, 0, 0));
return [start, end];
},
},
{
text: i18n.global.t('monitor.yesterday'),
value: () => {
const yesterday = new Date(new Date().getTime() - 3600 * 1000 * 24 * 1);
const end = new Date(yesterday.setHours(23, 59, 59, 999));
const start = new Date(yesterday.setHours(0, 0, 0, 0));
return [start, end];
},
},
{
text: i18n.global.t('monitor.lastNDay', [3]),
value: () => {
const start = new Date(new Date().getTime() - 3600 * 1000 * 24 * 3);
const end = new Date();
return [start, end];
},
},
{
text: i18n.global.t('monitor.lastNDay', [7]),
value: () => {
const start = new Date(new Date().getTime() - 3600 * 1000 * 24 * 7);
const end = new Date();
return [start, end];
},
},
{
text: i18n.global.t('monitor.lastNDay', [30]),
value: () => {
const start = new Date(new Date().getTime() - 3600 * 1000 * 24 * 30);
const end = new Date();
return [start, end];
},
},
];
+18
View File
@@ -500,3 +500,21 @@ export async function copyText(content: string) {
MsgError(i18n.global.t('commons.msg.copyFailed'));
}
}
export function getRuleType(ruleType: string) {
if (ruleType == '') {
return '';
}
return i18n.global.t(`xpack.waf.${ruleType}`);
}
export function getAction(action: string) {
if (action == '') {
return '';
}
return i18n.global.t(`xpack.waf.${action}`);
}
export function getLanguage() {
return localStorage.getItem('lang') || 'zh';
}
+9 -19
View File
@@ -12,7 +12,7 @@
>
{{ $t('app.all') }}
</el-button>
<div v-for="item in tags.slice(0, 6)" :key="item.key" class="inline">
<div v-for="item in tags.slice(0, 7)" :key="item.key" class="inline">
<el-button
class="tag-button"
:class="activeTag === item.key ? '' : 'no-active'"
@@ -38,7 +38,7 @@
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in tags.slice(6)"
v-for="item in tags.slice(7)"
@click="changeTag(item.key)"
:key="item.key"
>
@@ -50,23 +50,14 @@
</div>
</el-col>
<el-col :xs="24" :sm="4" :md="4" :lg="4" :xl="4">
<div class="search-button">
<el-input
v-model="req.name"
clearable
@clear="searchByName('')"
suffix-icon="Search"
@change="searchByName(req.name)"
:placeholder="$t('commons.button.search')"
></el-input>
</div>
<TableSearch @search="searchByName()" v-model:searchName="req.name" />
</el-col>
</el-row>
</template>
<template #rightButton>
<div class="flex justify-end">
<div class="mr-10">
<el-checkbox v-model="req.resource" true-label="all" false-label="remote" @change="search(req)">
<el-checkbox v-model="req.resource" true-value="all" false-value="remote" @change="search(req)">
{{ $t('app.showLocal') }}
</el-checkbox>
</div>
@@ -115,7 +106,7 @@
<div class="app-content">
<div class="app-header">
<span class="app-title">{{ app.name }}</span>
<el-text type="success" style="margin-left: 10px" v-if="app.installed">
<el-text type="success" class="!ml-2" v-if="app.installed">
{{ $t('app.allReadyInstalled') }}
</el-text>
<el-button
@@ -183,8 +174,8 @@ import Detail from '../detail/index.vue';
import Install from '../detail/install/index.vue';
import router from '@/routers';
import { MsgSuccess } from '@/utils/message';
import { useI18n } from 'vue-i18n';
import { GlobalStore } from '@/store';
import { getLanguage } from '@/utils/util';
const globalStore = GlobalStore();
@@ -192,7 +183,7 @@ const mobile = computed(() => {
return globalStore.isMobile();
});
const language = useI18n().locale.value;
const language = getLanguage();
const paginationConfig = reactive({
cacheSizeKey: 'app-page-size',
@@ -283,7 +274,7 @@ const changeTag = (key: string) => {
req.tags = [key];
}
const index = tags.value.findIndex((tag) => tag.key === key);
if (index > 5) {
if (index > 6) {
moreTag.value = key;
} else {
moreTag.value = '';
@@ -298,8 +289,7 @@ const getTagValue = (key: string) => {
}
};
const searchByName = (name: string) => {
req.name = name;
const searchByName = () => {
search(req);
};

Some files were not shown because too many files have changed in this diff Show More