2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 11:27:17 +08:00

feat: 离线模式

This commit is contained in:
耗子
2024-10-15 17:44:41 +08:00
parent 41d3d3fb97
commit b718c11f3a
11 changed files with 139 additions and 62 deletions

View File

@@ -21,6 +21,7 @@ const (
SettingKeySshPort SettingKey = "ssh_port"
SettingKeySshUser SettingKey = "ssh_user"
SettingKeySshPassword SettingKey = "ssh_password"
SettingKeyOfflineMode SettingKey = "offline_mode"
)
type Setting struct {
@@ -33,6 +34,7 @@ type Setting struct {
type SettingRepo interface {
Get(key SettingKey, defaultValue ...string) (string, error)
GetBool(key SettingKey, defaultValue ...bool) (bool, error)
Set(key SettingKey, value string) error
Delete(key SettingKey) error
GetPanelSetting(ctx context.Context) (*request.PanelSetting, error)

View File

@@ -288,7 +288,7 @@ func (r *appRepo) Update(slug string) error {
}
task := new(biz.Task)
task.Name = "更新应用 " + item.Name
task.Name = "升级应用 " + item.Name
task.Status = biz.TaskStatusWaiting
task.Shell = fmt.Sprintf(`curl -fsLm 10 --retry 3 "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
task.Log = "/tmp/" + item.Slug + ".log"

View File

@@ -4,10 +4,10 @@ import (
"context"
"errors"
"fmt"
"github.com/go-rat/utils/hash"
"path/filepath"
"slices"
"github.com/go-rat/utils/hash"
"github.com/goccy/go-yaml"
"github.com/gookit/color"
"github.com/spf13/cast"
@@ -22,10 +22,14 @@ import (
"github.com/TheTNB/panel/pkg/types"
)
type settingRepo struct{}
type settingRepo struct {
taskRepo biz.TaskRepo
}
func NewSettingRepo() biz.SettingRepo {
return &settingRepo{}
return &settingRepo{
taskRepo: NewTaskRepo(),
}
}
func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, error) {
@@ -43,6 +47,21 @@ func (r *settingRepo) Get(key biz.SettingKey, defaultValue ...string) (string, e
return setting.Value, nil
}
func (r *settingRepo) GetBool(key biz.SettingKey, defaultValue ...bool) (bool, error) {
setting := new(biz.Setting)
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return false, err
}
}
if setting.Value == "" && len(defaultValue) > 0 {
return defaultValue[0], nil
}
return cast.ToBool(setting.Value), nil
}
func (r *settingRepo) Set(key biz.SettingKey, value string) error {
setting := new(biz.Setting)
if err := app.Orm.Where("key = ?", key).First(setting).Error; err != nil {
@@ -66,16 +85,20 @@ func (r *settingRepo) Delete(key biz.SettingKey) error {
}
func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSetting, error) {
name := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyName).First(name).Error; err != nil {
name, err := r.Get(biz.SettingKeyName)
if err != nil {
return nil, err
}
websitePath := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyWebsitePath).First(websitePath).Error; err != nil {
offlineMode, err := r.Get(biz.SettingKeyOfflineMode)
if err != nil {
return nil, err
}
backupPath := new(biz.Setting)
if err := app.Orm.Where("key = ?", biz.SettingKeyBackupPath).First(backupPath).Error; err != nil {
websitePath, err := r.Get(biz.SettingKeyWebsitePath)
if err != nil {
return nil, err
}
backupPath, err := r.Get(biz.SettingKeyBackupPath)
if err != nil {
return nil, err
}
@@ -95,11 +118,12 @@ func (r *settingRepo) GetPanelSetting(ctx context.Context) (*request.PanelSettin
}
return &request.PanelSetting{
Name: name.Value,
Name: name,
Locale: app.Conf.String("app.locale"),
Entrance: app.Conf.String("http.entrance"),
WebsitePath: websitePath.Value,
BackupPath: backupPath.Value,
OfflineMode: cast.ToBool(offlineMode),
WebsitePath: websitePath,
BackupPath: backupPath,
Username: user.Username,
Email: user.Email,
Port: app.Conf.Int("http.port"),
@@ -113,6 +137,9 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
if err := r.Set(biz.SettingKeyName, setting.Name); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyOfflineMode, cast.ToString(setting.OfflineMode)); err != nil {
return false, err
}
if err := r.Set(biz.SettingKeyWebsitePath, setting.WebsitePath); err != nil {
return false, err
}
@@ -120,6 +147,37 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return false, err
}
// 用户
user := new(biz.User)
userID := cast.ToUint(ctx.Value("user_id"))
if err := app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
return false, err
}
user.Username = setting.Username
user.Email = setting.Email
if setting.Password != "" {
value, err := hash.NewArgon2id().Make(setting.Password)
if err != nil {
return false, err
}
user.Password = value
}
if err := app.Orm.Save(user).Error; err != nil {
return false, err
}
// 下面是需要需要重启的设置
// 面板HTTPS
restartFlag := false
oldCert, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.pem"))
oldKey, _ := io.Read(filepath.Join(app.Root, "panel/storage/cert.key"))
if oldCert != setting.Cert || oldKey != setting.Key {
if r.taskRepo.HasRunningTask() {
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
}
restartFlag = true
}
if err := io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), setting.Cert, 0644); err != nil {
return false, err
}
@@ -127,7 +185,7 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
return false, err
}
restartFlag := false
// 面板主配置
config := new(types.PanelConfig)
cm := yaml.CommentMap{}
raw, err := io.Read("/usr/local/etc/panel/config.yml")
@@ -147,29 +205,13 @@ func (r *settingRepo) UpdatePanelSetting(ctx context.Context, setting *request.P
if err != nil {
return false, err
}
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
return false, err
}
if raw != string(encoded) {
if r.taskRepo.HasRunningTask() {
return false, errors.New("后台任务正在运行,禁止修改部分设置,请稍后再试")
}
restartFlag = true
}
user := new(biz.User)
userID := cast.ToUint(ctx.Value("user_id"))
if err = app.Orm.Where("id = ?", userID).First(user).Error; err != nil {
return false, err
}
user.Username = setting.Username
user.Email = setting.Email
if setting.Password != "" {
value, err := hash.NewArgon2id().Make(setting.Password)
if err != nil {
return false, err
}
user.Password = value
}
if err = app.Orm.Save(user).Error; err != nil {
if err = io.Write("/usr/local/etc/panel/config.yml", string(encoded), 0644); err != nil {
return false, err
}
@@ -222,7 +264,7 @@ func (r *settingRepo) UpdatePanel(version, url, checksum string) error {
color.Greenln("|-前置检查...")
}
if io.Exists("/tmp/panel-storage.zip") {
return errors.New("检测到 /tmp 存在临时文件,可能是上次更新失败所致,请运行 panel-cli fix 修复后重试")
return errors.New("检测到 /tmp 存在临时文件,可能是上次升级失败所致,请运行 panel-cli fix 修复后重试")
}
if app.IsCli {

View File

@@ -4,6 +4,7 @@ type PanelSetting struct {
Name string `json:"name" validate:"required"`
Locale string `json:"locale" validate:"required"`
Entrance string `json:"entrance" validate:"required"`
OfflineMode bool `json:"offline_mode"`
WebsitePath string `json:"website_path" validate:"required"`
BackupPath string `json:"backup_path" validate:"required"`
Username string `json:"username" validate:"required"`

View File

@@ -14,14 +14,16 @@ import (
// PanelTask 面板每日任务
type PanelTask struct {
appRepo biz.AppRepo
backupRepo biz.BackupRepo
appRepo biz.AppRepo
backupRepo biz.BackupRepo
settingRepo biz.SettingRepo
}
func NewPanelTask() *PanelTask {
return &PanelTask{
appRepo: data.NewAppRepo(),
backupRepo: data.NewBackupRepo(),
appRepo: data.NewAppRepo(),
backupRepo: data.NewBackupRepo(),
settingRepo: data.NewSettingRepo(),
}
}
@@ -52,8 +54,10 @@ func (receiver *PanelTask) Run() {
}
// 更新商店缓存
if err = receiver.appRepo.UpdateCache(); err != nil {
app.Logger.Error("更新商店缓存失败", zap.Error(err))
if offline, err := receiver.settingRepo.GetBool(biz.SettingKeyOfflineMode); err == nil && !offline {
if err = receiver.appRepo.UpdateCache(); err != nil {
app.Logger.Error("更新商店缓存失败", zap.Error(err))
}
}
// 回收内存

View File

@@ -12,12 +12,14 @@ import (
)
type AppService struct {
appRepo biz.AppRepo
appRepo biz.AppRepo
settingRepo biz.SettingRepo
}
func NewAppService() *AppService {
return &AppService{
appRepo: data.NewAppRepo(),
appRepo: data.NewAppRepo(),
settingRepo: data.NewSettingRepo(),
}
}
@@ -162,6 +164,11 @@ func (s *AppService) IsInstalled(w http.ResponseWriter, r *http.Request) {
}
func (s *AppService) UpdateCache(w http.ResponseWriter, r *http.Request) {
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
Error(w, http.StatusForbidden, "离线模式下无法更新应用列表缓存")
return
}
if err := s.appRepo.UpdateCache(); err != nil {
Error(w, http.StatusInternalServerError, "%v", err)
return

View File

@@ -207,6 +207,11 @@ func (s *InfoService) InstalledDbAndPhp(w http.ResponseWriter, r *http.Request)
}
func (s *InfoService) CheckUpdate(w http.ResponseWriter, r *http.Request) {
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
Error(w, http.StatusForbidden, "离线模式下无法检查更新")
return
}
current := app.Version
latest, err := s.api.LatestVersion()
if err != nil {
@@ -237,6 +242,11 @@ func (s *InfoService) CheckUpdate(w http.ResponseWriter, r *http.Request) {
}
func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
Error(w, http.StatusForbidden, "离线模式下无法检查更新")
return
}
current := app.Version
latest, err := s.api.LatestVersion()
if err != nil {
@@ -261,7 +271,7 @@ func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
versions, err := s.api.IntermediateVersions()
if err != nil {
Error(w, http.StatusInternalServerError, "获取更新信息失败:%v", err)
Error(w, http.StatusInternalServerError, "获取升级信息失败:%v", err)
return
}
@@ -269,13 +279,13 @@ func (s *InfoService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
}
func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) {
if s.taskRepo.HasRunningTask() {
Error(w, http.StatusInternalServerError, "当前有任务正在执行,禁止更新")
if offline, _ := s.settingRepo.GetBool(biz.SettingKeyOfflineMode); offline {
Error(w, http.StatusForbidden, "离线模式下无法升级")
return
}
if err := app.Orm.Exec("PRAGMA wal_checkpoint(TRUNCATE)").Error; err != nil {
types.Status = types.StatusFailed
Error(w, http.StatusInternalServerError, "面板数据库异常,已终止操作:%v", err)
if s.taskRepo.HasRunningTask() {
Error(w, http.StatusInternalServerError, "后台任务正在运行,禁止升级,请稍后再试")
return
}
@@ -306,7 +316,7 @@ func (s *InfoService) Update(w http.ResponseWriter, r *http.Request) {
func (s *InfoService) Restart(w http.ResponseWriter, r *http.Request) {
if s.taskRepo.HasRunningTask() {
Error(w, http.StatusInternalServerError, "当前有任务正在行,禁止重启")
Error(w, http.StatusInternalServerError, "后台任务正在行,禁止重启,请稍后再试")
return
}

View File

@@ -226,13 +226,16 @@
"placeholder": "admin{'@'}example.com"
},
"port": {
"label": "Port (After saving, restart the panel and modify the browser address bar's port to the new port to access the panel)",
"label": "Port",
"placeholder": "8888"
},
"entrance": {
"label": "Security entrance (After saving, restart the panel and clear the browser Cookies to take effect)",
"label": "Security entrance",
"placeholder": "admin"
},
"offline": {
"label": "Offline mode"
},
"https": {
"label": "Panel HTTPS"
},

View File

@@ -88,22 +88,22 @@
}
},
"homeUpdate": {
"title": "更新面板",
"title": "升级面板",
"loading": "正在加载更新信息,稍等片刻",
"alerts": {
"success": "面板更新成功",
"info": "取消更新"
"success": "面板升级成功",
"info": "取消升级"
},
"button": {
"update": "立即更新"
"update": "立即升级"
},
"confirm": {
"update": {
"title": "更新面板",
"content": "确定更新面板吗?",
"title": "升级面板",
"content": "确定升级面板吗?",
"positiveText": "确定",
"negativeText": "取消",
"loading": "面板更新中..."
"loading": "面板升级中..."
}
}
},
@@ -226,13 +226,16 @@
"placeholder": "admin{'@'}example.com"
},
"port": {
"label": "端口(保存后需重启面板并修改浏览器地址栏的端口为新端口以访问面板)",
"label": "端口",
"placeholder": "8888"
},
"entrance": {
"label": "安全入口(保存后需重启面板并清除浏览器 Cookies 方可生效)",
"label": "安全入口",
"placeholder": "admin"
},
"offline": {
"label": "离线模式"
},
"https": {
"label": "面板 HTTPS"
},

View File

@@ -16,6 +16,7 @@ const model = ref<Setting>({
email: '',
port: 8888,
entrance: '',
offline_mode: false,
website_path: '',
backup_path: '',
https: false,
@@ -66,7 +67,7 @@ onMounted(() => {
:placeholder="$t('settingIndex.edit.fields.name.placeholder')"
/>
</n-form-item>
<n-form-item :label="$t('settingIndex.edit.fields.locale.label')">
<n-form-item v-show="false" label="$t('settingIndex.edit.fields.locale.label')">
<n-select v-model:value="model.locale" :options="locales"> </n-select>
</n-form-item>
<n-form-item :label="$t('settingIndex.edit.fields.username.label')">
@@ -99,6 +100,9 @@ onMounted(() => {
:placeholder="$t('settingIndex.edit.fields.entrance.placeholder')"
/>
</n-form-item>
<n-form-item :label="$t('settingIndex.edit.fields.offline.label')">
<n-switch v-model:value="model.offline_mode" />
</n-form-item>
<n-form-item :label="$t('settingIndex.edit.fields.path.label')">
<n-input
v-model:value="model.website_path"

View File

@@ -6,6 +6,7 @@ export interface Setting {
email: string
port: number
entrance: string
offline_mode: boolean
website_path: string
backup_path: string
https: boolean